diff options
Diffstat (limited to 'web/source/settings/admin')
6 files changed, 607 insertions, 313 deletions
diff --git a/web/source/settings/admin/emoji/remote/parse-from-toot.js b/web/source/settings/admin/emoji/remote/parse-from-toot.js index 309619ea4..84bbbdc92 100644 --- a/web/source/settings/admin/emoji/remote/parse-from-toot.js +++ b/web/source/settings/admin/emoji/remote/parse-from-toot.js @@ -129,14 +129,16 @@ function CopyEmojiForm({ localEmojiCodes, type, emojiList }) { title: "No emoji selected, cannot perform any actions" }; + const checkListExtraProps = React.useCallback(() => ({ localEmojiCodes }), [localEmojiCodes]); + return ( <div className="parsed"> <span>This {type == "statuses" ? "toot" : "account"} uses the following custom emoji, select the ones you want to copy/disable:</span> <form onSubmit={formSubmit}> <CheckList field={form.selectedEmoji} - Component={EmojiEntry} - localEmojiCodes={localEmojiCodes} + EntryComponent={EmojiEntry} + getExtraProps={checkListExtraProps} /> <CategorySelect @@ -170,7 +172,7 @@ function ErrorList({ errors }) { ); } -function EmojiEntry({ entry: emoji, localEmojiCodes, onChange }) { +function EmojiEntry({ entry: emoji, onChange, extraProps: { localEmojiCodes } }) { const shortcodeField = useTextInput("shortcode", { defaultValue: emoji.shortcode, validator: function validateShortcode(code) { @@ -181,9 +183,16 @@ function EmojiEntry({ entry: emoji, localEmojiCodes, onChange }) { }); React.useEffect(() => { - onChange({ valid: shortcodeField.valid }); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [shortcodeField.valid]); + if (emoji.valid != shortcodeField.valid) { + onChange({ valid: shortcodeField.valid }); + } + }, [onChange, emoji.valid, shortcodeField.valid]); + + React.useEffect(() => { + shortcodeField.validate(); + // only need this update if it's the emoji.checked that updated, not shortcodeField + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [emoji.checked]); return ( <> diff --git a/web/source/settings/admin/federation/import-export.js b/web/source/settings/admin/federation/import-export.js deleted file mode 100644 index a11174b1f..000000000 --- a/web/source/settings/admin/federation/import-export.js +++ /dev/null @@ -1,307 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see <http://www.gnu.org/licenses/>. -*/ - -"use strict"; - -const React = require("react"); -const { Switch, Route, Redirect, useLocation } = require("wouter"); - -const query = require("../../lib/query"); - -const { - useTextInput, - useBoolInput, - useRadioInput, - useCheckListInput -} = require("../../lib/form"); - -const useFormSubmit = require("../../lib/form/submit"); - -const { - TextInput, - TextArea, - Checkbox, - Select, - RadioGroup -} = require("../../components/form/inputs"); - -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 [updateFromFile, setUpdateFromFile] = React.useState(false); - const form = { - domains: useTextInput("domains"), - exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true }) - }; - - const [submitParse, parseResult] = useFormSubmit(form, query.useProcessDomainListMutation()); - const [submitExport, exportResult] = useFormSubmit(form, query.useExportDomainListMutation()); - - 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]); - } - - React.useEffect(() => { - if (exportResult.isSuccess) { - form.domains.setter(exportResult.data); - } - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [exportResult]); - - const [_location, setLocation] = useLocation(); - - if (updateFromFile) { - setUpdateFromFile(false); - submitParse(); - } - - return ( - <Switch> - <Route path={`${baseUrl}/list`}> - {!parseResult.isSuccess && <Redirect to={baseUrl} />} - - <h1> - <span className="button" onClick={() => { - parseResult.reset(); - setLocation(baseUrl); - }}> - < back - </span> Confirm import: - </h1> - <FormWithData - dataQuery={query.useInstanceBlocksQuery} - DataForm={ImportList} - list={parseResult.data} - /> - </Route> - - <Route> - {parseResult.isSuccess && <Redirect to={`${baseUrl}/list`} />} - <h2>Import / Export suspended domains</h2> - - <div> - <form onSubmit={submitParse}> - <TextArea - field={form.domains} - label="Domains, one per line (plaintext) or JSON" - placeholder={`google.com\nfacebook.com`} - rows={8} - /> - - <div className="row"> - <MutationButton label="Import" result={parseResult} showError={false} /> - <button type="button" className="with-padding"> - <label> - Import file - <input className="hidden" type="file" onChange={fileChanged} accept="application/json,text/plain" /> - </label> - </button> - </div> - </form> - <form onSubmit={submitExport}> - <div className="row"> - <MutationButton name="export" label="Export" result={exportResult} showError={false} /> - <MutationButton name="export-file" label="Export file" result={exportResult} showError={false} /> - <Select - field={form.exportType} - options={<> - <option value="plain">Text</option> - <option value="json">JSON</option> - </>} - /> - </div> - </form> - {parseResult.error && <Error error={parseResult.error} />} - {exportResult.error && <Error error={exportResult.error} />} - </div> - </Route> - </Switch> - ); -}; - -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 ( - <> - <form onSubmit={importDomains} className="suspend-import-list"> - <span>{list.length} domain{list.length != 1 ? "s" : ""} in this list</span> - - {hasComment.both && - <Select field={showComment} options={ - <> - <option value="public_comment">Show public comments</option> - <option value="private_comment">Show private comments</option> - </> - } /> - } - - <CheckList - field={form.domains} - Component={DomainEntry} - header={ - <> - <b>Domain</b> - <b></b> - <b>{commentName}</b> - </> - } - blockedInstances={blockedInstances} - commentType={showComment.value} - /> - - <TextArea - field={form.privateComment} - label="Private comment" - rows={3} - /> - <RadioGroup - field={form.privateCommentBehavior} - label="imported private comment" - /> - - <TextArea - field={form.publicComment} - label="Public comment" - rows={3} - /> - <RadioGroup - field={form.publicCommentBehavior} - label="imported public comment" - /> - - <Checkbox - field={form.obfuscate} - label="Obfuscate domains in public lists" - /> - - <MutationButton label="Import" result={importResult} /> - </form> - </> - ); -} - -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 = ( - <> - <i className="fa fa-history already-blocked" aria-hidden="true" title="Domain block already exists"></i> - <span className="sr-only">Domain block already exists.</span> - </> - ); - } - - return ( - <> - <TextInput - field={domainField} - onChange={(e) => { - domainField.onChange(e); - onChange({ domain: e.target.value, checked: true }); - }} - /> - <span id="icon">{icon}</span> - <p>{entry[commentType]}</p> - </> - ); -}
\ No newline at end of file diff --git a/web/source/settings/admin/federation/import-export/export-format-table.jsx b/web/source/settings/admin/federation/import-export/export-format-table.jsx new file mode 100644 index 000000000..72b2db04f --- /dev/null +++ b/web/source/settings/admin/federation/import-export/export-format-table.jsx @@ -0,0 +1,64 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const React = require("react"); + +module.exports = function ExportFormatTable() { + return ( + <table className="export-format-table"> + <thead> + <tr> + <th rowSpan={2} /> + <th colSpan={2}>Includes</th> + <th colSpan={2}>Importable by</th> + </tr> + <tr> + <th>Domain</th> + <th>Public comment</th> + <th>GoToSocial</th> + <th>Mastodon</th> + </tr> + </thead> + <tbody> + <Format name="Text" info={[true, false, true, false]} /> + <Format name="JSON" info={[true, true, true, false]} /> + <Format name="CSV" info={[true, true, true, true]} /> + </tbody> + </table> + ); +}; + +function Format({ name, info }) { + return ( + <tr> + <td><b>{name}</b></td> + {info.map((b, key) => <td key={key} className="bool">{bool(b)}</td>)} + </tr> + ); +} + +function bool(val) { + return ( + <> + <i className={`fa fa-${val ? "check" : "times"}`} aria-hidden="true"></i> + <span className="sr-only">{val ? "Yes" : "No"}</span> + </> + ); +}
\ No newline at end of file diff --git a/web/source/settings/admin/federation/import-export/form.jsx b/web/source/settings/admin/federation/import-export/form.jsx new file mode 100644 index 000000000..afd2d775d --- /dev/null +++ b/web/source/settings/admin/federation/import-export/form.jsx @@ -0,0 +1,123 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const React = require("react"); + +const query = require("../../../lib/query"); +const useFormSubmit = require("../../../lib/form/submit"); + +const { + TextArea, + Select, +} = require("../../../components/form/inputs"); + +const MutationButton = require("../../../components/form/mutation-button"); + +const { Error } = require("../../../components/error"); +const ExportFormatTable = require("./export-format-table"); + +module.exports = function ImportExportForm({ form, submitParse, parseResult }) { + const [submitExport, exportResult] = useFormSubmit(form, query.useExportDomainListMutation()); + + const [updateFromFile, setUpdateFromFile] = React.useState(false); + + 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]); + } + + React.useEffect(() => { + if (exportResult.isSuccess) { + form.domains.setter(exportResult.data); + } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [exportResult]); + + if (updateFromFile) { + setUpdateFromFile(false); + submitParse(); + } + return ( + <> + <h1>Import / Export suspended domains</h1> + <p> + This page can be used to import and export lists of domains to suspend. + Exports can be done in various formats, with varying functionality and support in other software. + Imports will automatically detect what format is being processed. + </p> + <ExportFormatTable /> + <div className="import-export"> + <TextArea + field={form.domains} + label="Domains" + placeholder={`google.com\nfacebook.com`} + rows={8} + /> + + <div className="button-grid"> + <MutationButton + label="Import" + type="button" + onClick={() => submitParse()} + result={parseResult} + showError={false} + /> + <label className="button"> + Import file + <input + type="file" + className="hidden" + onChange={fileChanged} + accept="application/json,text/plain,text/csv" + /> + </label> + <b /> {/* grid filler */} + <MutationButton + label="Export" + type="button" + onClick={() => submitExport("export")} + result={exportResult} showError={false} + /> + <MutationButton label="Export to file" type="button" onClick={() => submitExport("export-file")} result={exportResult} showError={false} /> + <div className="export-file"> + <span> + as + </span> + <Select + field={form.exportType} + options={<> + <option value="plain">Text</option> + <option value="json">JSON</option> + <option value="csv">CSV</option> + </>} + /> + </div> + </div> + + {parseResult.error && <Error error={parseResult.error} />} + {exportResult.error && <Error error={exportResult.error} />} + </div> + </> + ); +};
\ No newline at end of file diff --git a/web/source/settings/admin/federation/import-export/index.jsx b/web/source/settings/admin/federation/import-export/index.jsx new file mode 100644 index 000000000..3039b98f3 --- /dev/null +++ b/web/source/settings/admin/federation/import-export/index.jsx @@ -0,0 +1,78 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const React = require("react"); +const { Switch, Route, Redirect, useLocation } = require("wouter"); + +const query = require("../../../lib/query"); + +const { + useTextInput, +} = require("../../../lib/form"); + +const useFormSubmit = require("../../../lib/form/submit"); + +const ProcessImport = require("./process"); +const ImportExportForm = require("./form"); + +const baseUrl = "/settings/admin/federation/import-export"; + +module.exports = function ImportExport() { + const form = { + domains: useTextInput("domains"), + exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true }) + }; + + const [submitParse, parseResult] = useFormSubmit(form, query.useProcessDomainListMutation()); + + const [_location, setLocation] = useLocation(); + + return ( + <Switch> + <Route path={`${baseUrl}/process`}> + {parseResult.isSuccess ? ( + <> + <h1> + <span className="button" onClick={() => { + parseResult.reset(); + setLocation(baseUrl); + }}> + < back + </span> Confirm import: + </h1> + <ProcessImport + list={parseResult.data} + /> + </> + ) : <Redirect to={baseUrl} />} + </Route> + + <Route> + {!parseResult.isSuccess ? ( + <ImportExportForm + form={form} + submitParse={submitParse} + parseResult={parseResult} + /> + ) : <Redirect to={`${baseUrl}/process`} />} + </Route> + </Switch> + ); +};
\ No newline at end of file diff --git a/web/source/settings/admin/federation/import-export/process.jsx b/web/source/settings/admin/federation/import-export/process.jsx new file mode 100644 index 000000000..0b2d10099 --- /dev/null +++ b/web/source/settings/admin/federation/import-export/process.jsx @@ -0,0 +1,327 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const React = require("react"); + +const query = require("../../../lib/query"); +const { isValidDomainBlock, hasBetterScope } = require("../../../lib/domain-block"); + +const { + useTextInput, + useBoolInput, + useRadioInput, + useCheckListInput +} = require("../../../lib/form"); + +const useFormSubmit = require("../../../lib/form/submit"); + +const { + TextInput, + TextArea, + Checkbox, + Select, + RadioGroup +} = require("../../../components/form/inputs"); + +const CheckList = require("../../../components/check-list"); +const MutationButton = require("../../../components/form/mutation-button"); +const FormWithData = require("../../../lib/form/form-with-data"); + +module.exports = React.memo( + function ProcessImport({ list }) { + return ( + <div className="without-border"> + <FormWithData + dataQuery={query.useInstanceBlocksQuery} + DataForm={ImportList} + list={list} + /> + </div> + ); + } +); + +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" }); + + const form = { + domains: useCheckListInput("domains", { entries: list }), + 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 ( + <> + <form onSubmit={importDomains} className="suspend-import-list"> + <span>{list.length} domain{list.length != 1 ? "s" : ""} in this list</span> + + {hasComment.both && + <Select field={showComment} options={ + <> + <option value="public_comment">Show public comments</option> + <option value="private_comment">Show private comments</option> + </> + } /> + } + + <DomainCheckList + field={form.domains} + blockedInstances={blockedInstances} + commentType={showComment.value} + /> + + <TextArea + field={form.privateComment} + label="Private comment" + rows={3} + /> + <RadioGroup + field={form.privateCommentBehavior} + label="imported private comment" + /> + + <TextArea + field={form.publicComment} + label="Public comment" + rows={3} + /> + <RadioGroup + field={form.publicCommentBehavior} + label="imported public comment" + /> + + <Checkbox + field={form.obfuscate} + label="Obfuscate domains in public lists" + /> + + <MutationButton label="Import" result={importResult} /> + </form> + </> + ); +} + +function DomainCheckList({ field, blockedInstances, commentType }) { + const getExtraProps = React.useCallback((entry) => { + return { + comment: entry[commentType], + alreadyExists: blockedInstances[entry.domain] != undefined + }; + }, [blockedInstances, commentType]); + + const entriesWithSuggestions = React.useMemo(() => ( + Object.values(field.value).filter((entry) => entry.suggest) + ), [field.value]); + + return ( + <> + <CheckList + field={field} + header={<> + <b>Domain</b> + <b></b> + <b> + {commentType == "public_comment" && "Public comment"} + {commentType == "private_comment" && "Private comment"} + </b> + </>} + EntryComponent={DomainEntry} + getExtraProps={getExtraProps} + /> + <UpdateHint + entries={entriesWithSuggestions} + updateEntry={field.onChange} + updateMultiple={field.updateMultiple} + /> + </> + ); +} + +const UpdateHint = React.memo( + function UpdateHint({ entries, updateEntry, updateMultiple }) { + if (entries.length == 0) { + return null; + } + + function changeAll() { + updateMultiple( + entries.map((entry) => [entry.key, { domain: entry.suggest, suggest: null }]) + ); + } + + return ( + <div className="update-hints"> + <p> + {entries.length} {entries.length == 1 ? "entry uses" : "entries use"} a specific subdomain, + which you might want to change to the main domain, as that includes all it's (future) subdomains. + </p> + <div className="hints"> + {entries.map((entry) => ( + <UpdateableEntry key={entry.key} entry={entry} updateEntry={updateEntry} /> + ))} + </div> + {entries.length > 0 && <a onClick={changeAll}>change all</a>} + </div> + ); + } +); + +const UpdateableEntry = React.memo( + function UpdateableEntry({ entry, updateEntry }) { + return ( + <> + <span className="text-cutoff">{entry.domain}</span> + <i class="fa fa-long-arrow-right" aria-hidden="true"></i> + <span>{entry.suggest}</span> + <a role="button" onClick={() => + updateEntry(entry.key, { domain: entry.suggest, suggest: null }) + }>change</a> + </> + ); + } +); + +function domainValidationError(isValid) { + return isValid ? "" : "Invalid domain"; +} + +function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }) { + const domainField = useTextInput("domain", { + defaultValue: entry.domain, + showValidation: entry.checked, + initValidation: domainValidationError(entry.valid), + validator: (value) => domainValidationError(isValidDomainBlock(value)) + }); + + React.useEffect(() => { + if (entry.valid != domainField.valid) { + onChange({ valid: domainField.valid }); + } + }, [onChange, entry.valid, domainField.valid]); + + React.useEffect(() => { + if (entry.domain != domainField.value) { + domainField.setter(entry.domain); + } + // domainField.setter is enough, eslint wants domainField + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [entry.domain, domainField.setter]); + + React.useEffect(() => { + onChange({ suggest: hasBetterScope(domainField.value) }); + // only need this update if it's the entry.checked that updated, not onChange + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [domainField.value]); + + function clickIcon(e) { + if (entry.suggest) { + e.stopPropagation(); + e.preventDefault(); + domainField.setter(entry.suggest); + onChange({ domain: entry.suggest, checked: true }); + } + } + + return ( + <> + <TextInput + field={domainField} + onChange={(e) => { + domainField.onChange(e); + onChange({ domain: e.target.value, checked: true }); + }} + /> + <span id="icon" onClick={clickIcon}> + <DomainEntryIcon alreadyExists={alreadyExists} suggestion={entry.suggest} onChange={onChange} /> + </span> + <p>{comment}</p> + </> + ); +} + +function DomainEntryIcon({ alreadyExists, suggestion }) { + let icon; + let text; + + if (suggestion) { + icon = "fa-info-circle suggest-changes"; + text = `Entry targets a specific subdomain, consider changing it to '${suggestion}'.`; + } else if (alreadyExists) { + icon = "fa-history already-blocked"; + text = "Domain block already exists."; + } + + if (!icon) { + return null; + } + + return ( + <> + <i className={`fa ${icon}`} aria-hidden="true" title={text}></i> + <span className="sr-only">{text}</span> + </> + ); +}
\ No newline at end of file |