mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[frogend] Settings refactor (#1318)
* yakshave new form field structure * fully refactor user profile settings form * use rtk query api for profile settings * refactor user post settings * refactor password change form * refactor admin settings * FormWithData structure for user forms * admin actions refactor * whitespace * fix user settings data prop * remove superfluous logging * cleanup old code * refactor federation/suspend (overview, detail) * mostly abstracted (emoji) checkbox list * refactor parse-from-toot * refactor custom-emoji, progress on federation bulk * loading icon styling to prevent big spinny * refactor federation import-export interface * cleanup old files * [chore] Update/add license headers for 2023 * redux fixes * text-field exports * appease the linter * refactor authentication with RTK Query * fix login/logout state transition weirdness * fixes/cleanup * small linter-related fixes * add eslint license header check, fix existing files * remove old code, clarify comment * clarify suspend on subdomains * collapse if/else * fa-fw width info comment
This commit is contained in:
50
web/source/settings/lib/form/bool.jsx
Normal file
50
web/source/settings/lib/form/bool.jsx
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"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
|
||||
});
|
||||
};
|
147
web/source/settings/lib/form/check-list.jsx
Normal file
147
web/source/settings/lib/form/check-list.jsx
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"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
|
||||
}
|
||||
});
|
||||
};
|
56
web/source/settings/lib/form/combo-box.jsx
Normal file
56
web/source/settings/lib/form/combo-box.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
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 { useComboboxState } = require("ariakit/combobox");
|
||||
|
||||
module.exports = function useComboBoxInput({ name, Name }, { defaultValue } = {}) {
|
||||
const [isNew, setIsNew] = React.useState(false);
|
||||
|
||||
const state = useComboboxState({
|
||||
defaultValue,
|
||||
gutter: 0,
|
||||
sameWidth: true
|
||||
});
|
||||
|
||||
function reset() {
|
||||
state.setValue("");
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
};
|
91
web/source/settings/lib/form/file.jsx
Normal file
91
web/source/settings/lib/form/file.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
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 prettierBytes = require("prettier-bytes");
|
||||
|
||||
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();
|
||||
|
||||
function onChange(e) {
|
||||
let file = e.target.files[0];
|
||||
setFile(file);
|
||||
|
||||
URL.revokeObjectURL(imageURL);
|
||||
|
||||
if (file != undefined) {
|
||||
if (withPreview) {
|
||||
setImageURL(URL.createObjectURL(file));
|
||||
}
|
||||
|
||||
let size = prettierBytes(file.size);
|
||||
if (maxSize && file.size > maxSize) {
|
||||
size = <span className="error-text">{size}</span>;
|
||||
}
|
||||
|
||||
setInfo(<>
|
||||
{file.name} ({size})
|
||||
</>);
|
||||
} else {
|
||||
setInfo();
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
URL.revokeObjectURL(imageURL);
|
||||
setImageURL();
|
||||
setFile();
|
||||
setInfo();
|
||||
}
|
||||
|
||||
const infoComponent = (
|
||||
<span className="form-info">
|
||||
{info
|
||||
? info
|
||||
: initialInfo
|
||||
}
|
||||
</span>
|
||||
);
|
||||
|
||||
// Array / Object hybrid, for easier access in different contexts
|
||||
return Object.assign([
|
||||
onChange,
|
||||
reset,
|
||||
{
|
||||
[name]: file,
|
||||
[`${name}URL`]: imageURL,
|
||||
[`${name}Info`]: infoComponent,
|
||||
}
|
||||
], {
|
||||
onChange,
|
||||
reset,
|
||||
name,
|
||||
value: file,
|
||||
previewValue: imageURL,
|
||||
hasChanged: () => file != undefined,
|
||||
infoComponent
|
||||
});
|
||||
};
|
39
web/source/settings/lib/form/form-with-data.jsx
Normal file
39
web/source/settings/lib/form/form-with-data.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
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 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, queryArg, ...formProps }) {
|
||||
const { data, isLoading } = dataQuery(queryArg);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div>
|
||||
<Loading />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <DataForm data={data} {...formProps} />;
|
||||
}
|
||||
};
|
46
web/source/settings/lib/form/index.js
Normal file
46
web/source/settings/lib/form/index.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
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")),
|
||||
useBoolInput: makeHook(require("./bool")),
|
||||
useRadioInput: makeHook(require("./radio")),
|
||||
useComboBoxInput: makeHook(require("./combo-box")),
|
||||
useCheckListInput: makeHook(require("./check-list")),
|
||||
useValue: function (name, value) {
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
hasChanged: () => true // always included
|
||||
};
|
||||
}
|
||||
};
|
51
web/source/settings/lib/form/radio.jsx
Normal file
51
web/source/settings/lib/form/radio.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
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 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
|
||||
});
|
||||
};
|
83
web/source/settings/lib/form/submit.js
Normal file
83
web/source/settings/lib/form/submit.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const React = require("react");
|
||||
const syncpipe = require("syncpipe");
|
||||
|
||||
module.exports = function useFormSubmit(form, mutationQuery, { changedOnly = true } = {}) {
|
||||
if (!Array.isArray(mutationQuery)) {
|
||||
throw new ("useFormSubmit: mutationQuery was not an Array. Is a valid useMutation RTK Query provided?");
|
||||
}
|
||||
const [runMutation, result] = mutationQuery;
|
||||
const [usedAction, setUsedAction] = React.useState();
|
||||
return [
|
||||
function submitForm(e) {
|
||||
let action;
|
||||
if (e?.preventDefault) {
|
||||
e.preventDefault();
|
||||
action = e.nativeEvent.submitter.name;
|
||||
} else {
|
||||
action = e;
|
||||
}
|
||||
|
||||
if (action == "") {
|
||||
action = undefined;
|
||||
}
|
||||
setUsedAction(action);
|
||||
// transform the field definitions into an object with just their values
|
||||
let updatedFields = [];
|
||||
const mutationData = syncpipe(form, [
|
||||
(_) => Object.values(_),
|
||||
(_) => _.map((field) => {
|
||||
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()) {
|
||||
updatedFields.push(field);
|
||||
return [field.name, field.value];
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
(_) => _.filter((value) => value != null),
|
||||
(_) => Object.fromEntries(_)
|
||||
]);
|
||||
|
||||
mutationData.action = action;
|
||||
|
||||
return Promise.try(() => {
|
||||
return runMutation(mutationData);
|
||||
}).then((res) => {
|
||||
if (res.error == undefined) {
|
||||
updatedFields.forEach((field) => {
|
||||
field.reset();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
...result,
|
||||
action: usedAction
|
||||
}
|
||||
];
|
||||
};
|
67
web/source/settings/lib/form/text.jsx
Normal file
67
web/source/settings/lib/form/text.jsx
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
|
||||
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);
|
||||
|
||||
function onChange(e) {
|
||||
let input = e.target.value;
|
||||
setText(input);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (!dontReset) {
|
||||
setText(defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (validator && textRef.current) {
|
||||
let res = validator(text);
|
||||
setValid(res == "");
|
||||
textRef.current.setCustomValidity(res);
|
||||
}
|
||||
}, [text, textRef, validator]);
|
||||
|
||||
// Array / Object hybrid, for easier access in different contexts
|
||||
return Object.assign([
|
||||
onChange,
|
||||
reset,
|
||||
{
|
||||
[name]: text,
|
||||
[`${name}Ref`]: textRef,
|
||||
[`set${Name}`]: setText,
|
||||
[`${name}Valid`]: valid,
|
||||
}
|
||||
], {
|
||||
onChange,
|
||||
reset,
|
||||
name,
|
||||
value: text,
|
||||
ref: textRef,
|
||||
setter: setText,
|
||||
valid,
|
||||
hasChanged: () => text != defaultValue
|
||||
});
|
||||
};
|
Reference in New Issue
Block a user