update checklist components

This commit is contained in:
f0x 2023-01-31 00:34:01 +01:00
parent 4367960fe4
commit 79c792b832
5 changed files with 104 additions and 73 deletions

View file

@ -129,14 +129,16 @@ function CopyEmojiForm({ localEmojiCodes, type, emojiList }) {
title: "No emoji selected, cannot perform any actions" title: "No emoji selected, cannot perform any actions"
}; };
const checkListExtraProps = React.useCallback(() => ({ localEmojiCodes }), [localEmojiCodes]);
return ( return (
<div className="parsed"> <div className="parsed">
<span>This {type == "statuses" ? "toot" : "account"} uses the following custom emoji, select the ones you want to copy/disable:</span> <span>This {type == "statuses" ? "toot" : "account"} uses the following custom emoji, select the ones you want to copy/disable:</span>
<form onSubmit={formSubmit}> <form onSubmit={formSubmit}>
<CheckList <CheckList
field={form.selectedEmoji} field={form.selectedEmoji}
Component={EmojiEntry} EntryComponent={EmojiEntry}
localEmojiCodes={localEmojiCodes} getExtraProps={checkListExtraProps}
/> />
<CategorySelect <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", { const shortcodeField = useTextInput("shortcode", {
defaultValue: emoji.shortcode, defaultValue: emoji.shortcode,
validator: function validateShortcode(code) { validator: function validateShortcode(code) {
@ -181,9 +183,16 @@ function EmojiEntry({ entry: emoji, localEmojiCodes, onChange }) {
}); });
React.useEffect(() => { React.useEffect(() => {
if (emoji.valid != shortcodeField.valid) {
onChange({ valid: shortcodeField.valid }); onChange({ valid: shortcodeField.valid });
/* eslint-disable-next-line react-hooks/exhaustive-deps */ }
}, [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 ( return (
<> <>

View file

@ -189,9 +189,6 @@ function ImportList({ list, data: blockedInstances }) {
}, [list]); }, [list]);
const showComment = useTextInput("showComment", { defaultValue: hasComment.type ?? "public_comment" }); 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 = { const form = {
domains: useCheckListInput("domains", { domains: useCheckListInput("domains", {
@ -235,16 +232,8 @@ function ImportList({ list, data: blockedInstances }) {
} /> } />
} }
<CheckList <DomainCheckList
field={form.domains} field={form.domains}
Component={DomainEntry}
header={
<>
<b>Domain</b>
<b></b>
<b>{commentName}</b>
</>
}
blockedInstances={blockedInstances} blockedInstances={blockedInstances}
commentType={showComment.value} commentType={showComment.value}
/> />
@ -280,31 +269,55 @@ function ImportList({ list, data: blockedInstances }) {
); );
} }
function DomainEntry({ entry, onChange, blockedInstances, commentType }) { function DomainCheckList({ field, blockedInstances, commentType }) {
const getExtraProps = React.useCallback((entry) => {
return {
comment: entry[commentType],
alreadyExists: blockedInstances[entry.domain] != undefined
};
}, [blockedInstances, commentType]);
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}
/>
);
}
function domainValidationError(isValid) {
return isValid ? "" : "Invalid domain";
}
function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }) {
const domainField = useTextInput("domain", { const domainField = useTextInput("domain", {
defaultValue: entry.domain, defaultValue: entry.domain,
validator: (value) => { initValidation: domainValidationError(entry.valid),
return (entry.checked && !isValidDomain(value, { wildcard: true, allowUnicode: true })) validator: (value) => domainValidationError(
? "Invalid domain" !entry.checked || isValidDomain(value, { wildcard: true, allowUnicode: true })
: ""; )
}
}); });
React.useEffect(() => { React.useEffect(() => {
if (entry.valid != domainField.valid) {
onChange({ valid: domainField.valid }); 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>
</>
);
} }
}, [onChange, entry.valid, domainField.valid]);
React.useEffect(() => {
domainField.validate();
// only need this update if it's the entry.checked that updated, not domainField
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [entry.checked]);
return ( return (
<> <>
@ -315,8 +328,11 @@ function DomainEntry({ entry, onChange, blockedInstances, commentType }) {
onChange({ domain: e.target.value, checked: true }); onChange({ domain: e.target.value, checked: true });
}} }}
/> />
<span id="icon">{icon}</span> <span id="icon">{alreadyExists && <>
<p>{entry[commentType]}</p> <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>
</>}</span>
<p>{comment}</p>
</> </>
); );
} }

View file

@ -20,15 +20,15 @@
const React = require("react"); const React = require("react");
module.exports = function CheckList({ field, header = "All", renderEntry }) { module.exports = function CheckList({ field, header = "All", EntryComponent, getExtraProps }) {
performance.mark("RENDER_CHECKLIST");
return ( return (
<div className="checkbox-list list"> <div className="checkbox-list list">
<CheckListHeader toggleAll={field.toggleAll}> {header}</CheckListHeader> <CheckListHeader toggleAll={field.toggleAll}> {header}</CheckListHeader>
<CheckListEntries <CheckListEntries
entries={field.value} entries={field.value}
updateValue={field.onChange} updateValue={field.onChange}
renderEntry={renderEntry} EntryComponent={EntryComponent}
getExtraProps={getExtraProps}
/> />
</div> </div>
); );
@ -47,7 +47,8 @@ function CheckListHeader({ toggleAll, children }) {
); );
} }
const CheckListEntries = React.memo(function CheckListEntries({ entries, renderEntry, updateValue }) { const CheckListEntries = React.memo(
function CheckListEntries({ entries, updateValue, EntryComponent, getExtraProps }) {
const deferredEntries = React.useDeferredValue(entries); const deferredEntries = React.useDeferredValue(entries);
return Object.values(deferredEntries).map((entry) => ( return Object.values(deferredEntries).map((entry) => (
@ -55,22 +56,27 @@ const CheckListEntries = React.memo(function CheckListEntries({ entries, renderE
key={entry.key} key={entry.key}
entry={entry} entry={entry}
updateValue={updateValue} updateValue={updateValue}
renderEntry={renderEntry} EntryComponent={EntryComponent}
getExtraProps={getExtraProps}
/> />
)); ));
}); }
);
/* /*
React.memo is a performance optimization that only re-renders a CheckListEntry React.memo is a performance optimization that only re-renders a CheckListEntry
when it's props actually change, instead of every time anything when it's props actually change, instead of every time anything
in the list (CheckListEntries) updates in the list (CheckListEntries) updates
*/ */
const CheckListEntry = React.memo(function CheckListEntry({ entry, updateValue, renderEntry }) { const CheckListEntry = React.memo(
function CheckListEntry({ entry, updateValue, getExtraProps, EntryComponent }) {
const onChange = React.useCallback( const onChange = React.useCallback(
(value) => updateValue(entry.key, value), (value) => updateValue(entry.key, value),
[updateValue, entry.key] [updateValue, entry.key]
); );
const extraProps = React.useMemo(() => getExtraProps?.(entry), [getExtraProps, entry]);
return ( return (
<label className="entry"> <label className="entry">
<input <input
@ -78,7 +84,8 @@ const CheckListEntry = React.memo(function CheckListEntry({ entry, updateValue,
onChange={(e) => onChange({ checked: e.target.checked })} onChange={(e) => onChange({ checked: e.target.checked })}
checked={entry.checked} checked={entry.checked}
/> />
{renderEntry(entry, onChange)} <EntryComponent entry={entry} onChange={onChange} extraProps={extraProps} />
</label> </label>
); );
}); }
);

View file

@ -81,7 +81,6 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke
const toggleAllRef = React.useRef(null); const toggleAllRef = React.useRef(null);
React.useEffect(() => { React.useEffect(() => {
performance.mark("GoToSocial-useCheckListInput-useEffect-start");
/* Updates (un)check all checkbox, based on shortcode checkboxes /* Updates (un)check all checkbox, based on shortcode checkboxes
Can be 0 (not checked), 1 (checked) or 2 (indeterminate) Can be 0 (not checked), 1 (checked) or 2 (indeterminate)
*/ */
@ -108,8 +107,6 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke
setToggleAllState(all ? 1 : 0); setToggleAllState(all ? 1 : 0);
toggleAllRef.current.indeterminate = false; toggleAllRef.current.indeterminate = false;
} }
performance.mark("GoToSocial-useCheckListInput-useEffect-finish");
performance.measure("GoToSocial-useCheckListInput-useEffect-processed", "GoToSocial-useCheckListInput-useEffect-start", "GoToSocial-useCheckListInput-useEffect-finish");
}, [state, toggleAllRef]); }, [state, toggleAllRef]);
const reset = React.useCallback( const reset = React.useCallback(
@ -137,7 +134,8 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke
function selectedValues() { function selectedValues() {
return syncpipe(state, [ return syncpipe(state, [
(_) => Object.values(_), (_) => Object.values(_),
(_) => _.filter((entry) => entry.checked) (_) => _.filter((entry) => entry.checked),
(_) => _.map((entry) => ({ ...entry }))
]); ]);
} }

View file

@ -87,6 +87,7 @@ module.exports = function useTextInput({ name, Name }, {
ref: textRef, ref: textRef,
setter: setText, setter: setText,
valid, valid,
validate: () => setValidation(validator(text)),
hasChanged: () => text != defaultValue hasChanged: () => text != defaultValue
}); });
}; };