use redux state for profile form

This commit is contained in:
f0x 2022-09-11 23:02:23 +02:00
parent 3ce9dfe7d6
commit 2bd18790cf
5 changed files with 71 additions and 45 deletions

View file

@ -18,6 +18,7 @@
"browserlist": "^1.0.1", "browserlist": "^1.0.1",
"create-error": "^0.3.1", "create-error": "^0.3.1",
"css-extract": "^2.0.0", "css-extract": "^2.0.0",
"dotty": "^0.1.2",
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react": "^7.24.0",
"express": "^4.18.1", "express": "^4.18.1",
"factor-bundle": "^2.5.0", "factor-bundle": "^2.5.0",

View file

@ -19,10 +19,11 @@
"use strict"; "use strict";
const Promise = require("bluebird"); const Promise = require("bluebird");
const d = require("dotty");
const user = require("../../redux/reducers/user").actions; const user = require("../../redux/reducers/user").actions;
module.exports = function({apiCall}) { module.exports = function ({ apiCall }) {
return { return {
fetchAccount: function fetchAccount() { fetchAccount: function fetchAccount() {
return function (dispatch, _getState) { return function (dispatch, _getState) {
@ -33,10 +34,34 @@ module.exports = function({apiCall}) {
}); });
}; };
}, },
updateAccount: function updateAccount(newAccount) { updateAccount: function updateAccount() {
return function (dispatch, _getSate) { const formKeys = ["display_name", "locked"];
const renamedKeys = [["note", "source.note"]];
const fileKeys = ["header", "avatar"];
return function (dispatch, getState) {
return Promise.try(() => { return Promise.try(() => {
return dispatch(apiCall("PATCH", "/api/v1/accounts/update_credentials", newAccount, "form")); const { account } = getState().user;
const update = {};
formKeys.forEach((key) => {
d.put(update, key, d.get(account, key));
update[key] = account[key];
});
renamedKeys.forEach(([sendKey, intKey]) => {
d.put(update, sendKey, d.get(account, intKey));
});
fileKeys.forEach((key) => {
let file = d.get(account, `${key}File`);
if (file != undefined) {
d.put(update, key, file);
}
});
return dispatch(apiCall("PATCH", "/api/v1/accounts/update_credentials", update, "form"));
}).then((account) => { }).then((account) => {
console.log(account); console.log(account);
return dispatch(user.setAccount(account)); return dispatch(user.setAccount(account));

View file

@ -19,6 +19,7 @@
"use strict"; "use strict";
const {createSlice} = require("@reduxjs/toolkit"); const {createSlice} = require("@reduxjs/toolkit");
const d = require("dotty");
module.exports = createSlice({ module.exports = createSlice({
name: "user", name: "user",
@ -27,6 +28,9 @@ module.exports = createSlice({
reducers: { reducers: {
setAccount: (state, {payload}) => { setAccount: (state, {payload}) => {
state.account = payload; state.account = payload;
},
setAccountVal: (state, {payload: [key, val]}) => {
d.put(state.account, key, val);
} }
} }
}); });

View file

@ -21,10 +21,12 @@
const Promise = require("bluebird"); const Promise = require("bluebird");
const React = require("react"); const React = require("react");
const Redux = require("react-redux"); const Redux = require("react-redux");
const d = require("dotty");
const Submit = require("../components/submit"); const Submit = require("../components/submit");
const api = require("../lib/api"); const api = require("../lib/api");
const user = require("../redux/reducers/user").actions;
module.exports = function UserProfile() { module.exports = function UserProfile() {
const dispatch = Redux.useDispatch(); const dispatch = Redux.useDispatch();
@ -33,29 +35,30 @@ module.exports = function UserProfile() {
const [errorMsg, setError] = React.useState(""); const [errorMsg, setError] = React.useState("");
const [statusMsg, setStatus] = React.useState(""); const [statusMsg, setStatus] = React.useState("");
const [headerFile, setHeaderFile] = React.useState(undefined); function onTextChange(key) {
const [avatarFile, setAvatarFile] = React.useState(undefined); return function (e) {
dispatch(user.setAccountVal([key, e.target.value]));
};
}
const [displayName, setDisplayName] = React.useState(""); function onCheckChange(key) {
const [bio, setBio] = React.useState(""); return function (e) {
const [locked, setLocked] = React.useState(false); dispatch(user.setAccountVal([key, e.target.checked]));
};
}
React.useEffect(() => { function onFileChange(key) {
return function (e) {
setDisplayName(account.display_name); let old = d.get(account, key);
setBio(account.source ? account.source.note : ""); if (old != undefined) {
setLocked(account.locked); URL.revokeObjectURL(old); // no error revoking a non-Object URL as provided by instance
}, []); }
let file = e.target.files[0];
const headerOnChange = (e) => { let objectURL = URL.createObjectURL(file);
setHeaderFile(e.target.files[0]); dispatch(user.setAccountVal([key, objectURL]));
// setHeaderSrc(URL.createObjectURL(e.target.files[0])); dispatch(user.setAccountVal([`${key}File`, file]));
}; };
}
const avatarOnChange = (e) => {
setAvatarFile(e.target.files[0]);
// setAvatarSrc(URL.createObjectURL(e.target.files[0]));
};
const submit = (e) => { const submit = (e) => {
e.preventDefault(); e.preventDefault();
@ -63,21 +66,7 @@ module.exports = function UserProfile() {
setStatus("PATCHing"); setStatus("PATCHing");
setError(""); setError("");
return Promise.try(() => { return Promise.try(() => {
let payload = { return dispatch(api.user.updateAccount());
display_name: displayName,
note: bio,
locked: locked
};
if (headerFile) {
payload.header = headerFile;
}
if (avatarFile) {
payload.avatar = avatarFile;
}
return dispatch(api.user.updateAccount(payload));
}).then(() => { }).then(() => {
setStatus("Saved!"); setStatus("Saved!");
}).catch((e) => { }).catch((e) => {
@ -105,26 +94,28 @@ module.exports = function UserProfile() {
<div> <div>
<h3>Header</h3> <h3>Header</h3>
<label htmlFor="header" className="file-input button">Browse</label> <label htmlFor="header" className="file-input button">Browse</label>
<span>{headerFile ? headerFile.name : "no file selected"}</span> <span>{account.headerFile ? account.headerFile.name : "no file selected"}</span>
<input className="hidden" id="header" type="file" accept="image/*" onChange={onFileChange("header")}/>
</div> </div>
<div> <div>
<h3>Avatar</h3> <h3>Avatar</h3>
<label htmlFor="avatar" className="file-input button">Browse</label> <label htmlFor="avatar" className="file-input button">Browse</label>
<span>{avatarFile ? avatarFile.name : "no file selected"}</span> <span>{account.avatarFile ? account.avatarFile.name : "no file selected"}</span>
<input className="hidden" id="avatar" type="file" accept="image/*" onChange={onFileChange("avatar")}/>
</div> </div>
</div> </div>
</div> </div>
<div className="labelinput"> <div className="labelinput">
<label htmlFor="displayname">Name</label> <label htmlFor="displayname">Name</label>
<input id="displayname" type="text" value={displayName} onChange={(e) => setDisplayName(e.target.value)} placeholder="A GoToSocial user"/> <input id="displayname" type="text" value={account.display_name} onChange={onTextChange("display_name")} placeholder="A GoToSocial user"/>
</div> </div>
<div className="labelinput"> <div className="labelinput">
<label htmlFor="bio">Bio</label> <label htmlFor="bio">Bio</label>
<textarea id="bio" value={bio} onChange={(e) => setBio(e.target.value)} placeholder="Just trying out GoToSocial, my pronouns are they/them and I like sloths."/> <textarea id="bio" value={account.source.note} onChange={onTextChange("source.note")} placeholder="Just trying out GoToSocial, my pronouns are they/them and I like sloths."/>
</div> </div>
<div className="labelcheckbox"> <div className="labelcheckbox">
<label htmlFor="locked">Manually approve follow requests?</label> <label htmlFor="locked">Manually approve follow requests?</label>
<input id="locked" type="checkbox" checked={locked} onChange={(e) => setLocked(e.target.checked)}/> <input id="locked" type="checkbox" checked={account.locked} onChange={onCheckChange("locked")}/>
</div> </div>
<Submit onClick={submit} label="Save profile info" errorMsg={errorMsg} statusMsg={statusMsg}/> <Submit onClick={submit} label="Save profile info" errorMsg={errorMsg} statusMsg={statusMsg}/>
</div> </div>

View file

@ -2528,6 +2528,11 @@ domain-browser@^1.2.0:
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
dotty@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/dotty/-/dotty-0.1.2.tgz#512d44cc4111a724931226259297f235e8484f6f"
integrity sha512-V0EWmKeH3DEhMwAZ+8ZB2Ao4OK6p++Z0hsDtZq3N0+0ZMVqkzrcEGROvOnZpLnvBg5PTNG23JEDLAm64gPaotQ==
duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2, duplexer2@~0.1.4: duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2, duplexer2@~0.1.4:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"