mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-10-31 22:40:01 +00:00
restructure form fields
This commit is contained in:
parent
b01267de17
commit
cb137589a0
6 changed files with 241 additions and 74 deletions
|
@ -341,3 +341,7 @@ footer {
|
|||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
}
|
138
web/source/settings-panel/components/form-fields.jsx
Normal file
138
web/source/settings-panel/components/form-fields.jsx
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
const Redux = require("react-redux");
|
||||
const d = require("dotty");
|
||||
|
||||
function eventListeners(dispatch, setter, obj) {
|
||||
return {
|
||||
onTextChange: function (key) {
|
||||
return function (e) {
|
||||
dispatch(setter([key, e.target.value]));
|
||||
};
|
||||
},
|
||||
|
||||
onCheckChange: function (key) {
|
||||
return function (e) {
|
||||
dispatch(setter([key, e.target.checked]));
|
||||
};
|
||||
},
|
||||
|
||||
onFileChange: function (key) {
|
||||
return function (e) {
|
||||
let old = d.get(obj, key);
|
||||
if (old != undefined) {
|
||||
URL.revokeObjectURL(old); // no error revoking a non-Object URL as provided by instance
|
||||
}
|
||||
let file = e.target.files[0];
|
||||
let objectURL = URL.createObjectURL(file);
|
||||
dispatch(setter([key, objectURL]));
|
||||
dispatch(setter([`${key}File`, file]));
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function get(state, id) {
|
||||
let value;
|
||||
if (id.includes(".")) {
|
||||
value = d.get(state, id);
|
||||
} else {
|
||||
value = state[id];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// function removeFile(name) {
|
||||
// return function(e) {
|
||||
// e.preventDefault();
|
||||
// dispatch(user.setProfileVal([name, ""]));
|
||||
// dispatch(user.setProfileVal([`${name}File`, ""]));
|
||||
// };
|
||||
// }
|
||||
|
||||
module.exports = {
|
||||
formFields: function formFields(setter, selector) {
|
||||
function FormField({type, id, name, className="", placeHolder="", fileType="", children=null}) {
|
||||
const dispatch = Redux.useDispatch();
|
||||
let state = Redux.useSelector(selector);
|
||||
let {
|
||||
onTextChange,
|
||||
onCheckChange,
|
||||
onFileChange
|
||||
} = eventListeners(dispatch, setter, state);
|
||||
|
||||
let field;
|
||||
let defaultLabel = true;
|
||||
if (type == "text") {
|
||||
field = <input type="text" id={id} value={get(state, id)} placeholder={placeHolder} className={className} onChange={onTextChange(id)}/>;
|
||||
} else if (type == "textarea") {
|
||||
field = <textarea type="text" id={id} value={get(state, id)} placeholder={placeHolder} className={className} onChange={onTextChange(id)}/>;
|
||||
} else if (type == "checkbox") {
|
||||
field = <input type="checkbox" id={id} checked={get(state, id)} className={className} onChange={onCheckChange(id)}/>;
|
||||
} else if (type == "file") {
|
||||
defaultLabel = false;
|
||||
let file = get(state, `${id}File`);
|
||||
field = (
|
||||
<>
|
||||
<label htmlFor={id} className="file-input button">Browse</label>
|
||||
<span>{file ? file.name : "no file selected"}</span>
|
||||
{/* <a onClick={removeFile("header")} href="#">remove</a> */}
|
||||
<input className="hidden" id={id} type="file" accept={fileType} onChange={onFileChange(id)} />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
defaultLabel = false;
|
||||
field = `unsupported FormField ${type}, this is a developer error`;
|
||||
}
|
||||
|
||||
let label = <label htmlFor={id}>{name}</label>;
|
||||
|
||||
return (
|
||||
<div className={`form-field ${type}`}>
|
||||
{defaultLabel ? label : null}
|
||||
{field}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
TextInput: function(props) {
|
||||
return <FormField type="text" {...props} />;
|
||||
},
|
||||
|
||||
TextArea: function(props) {
|
||||
return <FormField type="textarea" {...props} />;
|
||||
},
|
||||
|
||||
Checkbox: function(props) {
|
||||
return <FormField type="checkbox" {...props} />;
|
||||
},
|
||||
|
||||
File: function(props) {
|
||||
return <FormField type="file" {...props} />;
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
eventListeners
|
||||
};
|
|
@ -20,12 +20,13 @@
|
|||
|
||||
const Promise = require("bluebird");
|
||||
const { isPlainObject } = require("is-plain-object");
|
||||
const d = require("dotty");
|
||||
|
||||
const { APIError } = require("../errors");
|
||||
const { setInstanceInfo, setNamedInstanceInfo } = require("../../redux/reducers/instances").actions;
|
||||
const oauth = require("../../redux/reducers/oauth").actions;
|
||||
|
||||
function apiCall(method, route, payload, type="json") {
|
||||
function apiCall(method, route, payload, type = "json") {
|
||||
return function (dispatch, getState) {
|
||||
const state = getState();
|
||||
let base = state.oauth.instance;
|
||||
|
@ -50,10 +51,14 @@ function apiCall(method, route, payload, type="json") {
|
|||
Object.entries(payload).forEach(([key, val]) => {
|
||||
if (isPlainObject(val)) {
|
||||
Object.entries(val).forEach(([key2, val2]) => {
|
||||
formData.set(`${key}[${key2}]`, val2);
|
||||
if (val2 != undefined) {
|
||||
formData.set(`${key}[${key2}]`, val2);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
formData.set(key, val);
|
||||
if (val != undefined) {
|
||||
formData.set(key, val);
|
||||
}
|
||||
}
|
||||
});
|
||||
body = formData;
|
||||
|
@ -83,7 +88,7 @@ function apiCall(method, route, payload, type="json") {
|
|||
dispatch(oauth.remove());
|
||||
throw new APIError("Stored OAUTH login was no longer valid, please log in again.");
|
||||
}
|
||||
throw new APIError(json.error, {json});
|
||||
throw new APIError(json.error, { json });
|
||||
} else {
|
||||
return json;
|
||||
}
|
||||
|
@ -91,12 +96,40 @@ function apiCall(method, route, payload, type="json") {
|
|||
};
|
||||
}
|
||||
|
||||
function getChanges(state, keys) {
|
||||
const { formKeys = [], fileKeys = [], renamedKeys = {} } = keys;
|
||||
const update = {};
|
||||
|
||||
formKeys.forEach((key) => {
|
||||
let value = d.get(state, key);
|
||||
if (value == undefined) {
|
||||
return;
|
||||
}
|
||||
if (renamedKeys[key]) {
|
||||
key = renamedKeys[key];
|
||||
}
|
||||
d.put(update, key, value);
|
||||
});
|
||||
|
||||
fileKeys.forEach((key) => {
|
||||
let file = d.get(state, `${key}File`);
|
||||
if (file != undefined) {
|
||||
if (renamedKeys[key]) {
|
||||
key = renamedKeys[key];
|
||||
}
|
||||
d.put(update, key, file);
|
||||
}
|
||||
});
|
||||
|
||||
return update;
|
||||
}
|
||||
|
||||
function getCurrentUrl() {
|
||||
return `${window.location.origin}${window.location.pathname}`;
|
||||
}
|
||||
|
||||
function fetchInstanceWithoutStore(domain) {
|
||||
return function(dispatch, getState) {
|
||||
return function (dispatch, getState) {
|
||||
return Promise.try(() => {
|
||||
let lookup = getState().instances.info[domain];
|
||||
if (lookup != undefined) {
|
||||
|
@ -107,7 +140,7 @@ function fetchInstanceWithoutStore(domain) {
|
|||
// but we don't want to store it there yet
|
||||
// so we mock the API here with our function argument
|
||||
let fakeState = {
|
||||
oauth: {instance: domain}
|
||||
oauth: { instance: domain }
|
||||
};
|
||||
|
||||
return apiCall("GET", "/api/v1/instance")(dispatch, () => fakeState);
|
||||
|
@ -121,7 +154,7 @@ function fetchInstanceWithoutStore(domain) {
|
|||
}
|
||||
|
||||
function fetchInstance() {
|
||||
return function(dispatch, _getState) {
|
||||
return function (dispatch, _getState) {
|
||||
return Promise.try(() => {
|
||||
return dispatch(apiCall("GET", "/api/v1/instance"));
|
||||
}).then((json) => {
|
||||
|
@ -133,12 +166,15 @@ function fetchInstance() {
|
|||
};
|
||||
}
|
||||
|
||||
let submoduleArgs = { apiCall, getCurrentUrl, getChanges };
|
||||
|
||||
module.exports = {
|
||||
instance: {
|
||||
fetchWithoutStore: fetchInstanceWithoutStore,
|
||||
fetch: fetchInstance
|
||||
},
|
||||
oauth: require("./oauth")({apiCall, getCurrentUrl}),
|
||||
user: require("./user")({apiCall}),
|
||||
apiCall
|
||||
oauth: require("./oauth")(submoduleArgs),
|
||||
user: require("./user")(submoduleArgs),
|
||||
apiCall,
|
||||
getChanges
|
||||
};
|
|
@ -23,28 +23,13 @@ const d = require("dotty");
|
|||
|
||||
const user = require("../../redux/reducers/user").actions;
|
||||
|
||||
module.exports = function ({ apiCall }) {
|
||||
function updateCredentials(selector, {formKeys=[], renamedKeys=[], fileKeys=[]}) {
|
||||
module.exports = function ({ apiCall, getChanges }) {
|
||||
function updateCredentials(selector, keys) {
|
||||
return function (dispatch, getState) {
|
||||
return Promise.try(() => {
|
||||
const state = selector(getState());
|
||||
|
||||
const update = {};
|
||||
|
||||
formKeys.forEach((key) => {
|
||||
d.put(update, key, d.get(state, key));
|
||||
});
|
||||
|
||||
renamedKeys.forEach(([sendKey, intKey]) => {
|
||||
d.put(update, sendKey, d.get(state, intKey));
|
||||
});
|
||||
|
||||
fileKeys.forEach((key) => {
|
||||
let file = d.get(state, `${key}File`);
|
||||
if (file != undefined) {
|
||||
d.put(update, key, file);
|
||||
}
|
||||
});
|
||||
const update = getChanges(state, keys);
|
||||
|
||||
return dispatch(apiCall("PATCH", "/api/v1/accounts/update_credentials", update, "form"));
|
||||
}).then((account) => {
|
||||
|
@ -63,13 +48,17 @@ module.exports = function ({ apiCall }) {
|
|||
});
|
||||
};
|
||||
},
|
||||
|
||||
updateProfile: function updateProfile() {
|
||||
const formKeys = ["display_name", "locked", "source", "custom_css"];
|
||||
const renamedKeys = [["note", "source.note"]];
|
||||
const formKeys = ["display_name", "locked", "source", "custom_css", "source.note"];
|
||||
const renamedKeys = {
|
||||
"source.note": "note"
|
||||
};
|
||||
const fileKeys = ["header", "avatar"];
|
||||
|
||||
return updateCredentials((state) => state.user.profile, {formKeys, renamedKeys, fileKeys});
|
||||
},
|
||||
|
||||
updateSettings: function updateProfile() {
|
||||
const formKeys = ["source"];
|
||||
|
||||
|
|
|
@ -277,7 +277,7 @@ section.with-sidebar > div {
|
|||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
div.picker {
|
||||
div.form-field {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
|
|
|
@ -25,9 +25,17 @@ const Redux = require("react-redux");
|
|||
const Submit = require("../components/submit");
|
||||
|
||||
const api = require("../lib/api");
|
||||
const formFields = require("../lib/form-fields");
|
||||
const user = require("../redux/reducers/user").actions;
|
||||
|
||||
const { formFields } = require("../components/form-fields");
|
||||
|
||||
const {
|
||||
TextInput,
|
||||
TextArea,
|
||||
Checkbox,
|
||||
File
|
||||
} = formFields(user.setProfileVal, (state) => state.user.profile);
|
||||
|
||||
module.exports = function UserProfile() {
|
||||
const dispatch = Redux.useDispatch();
|
||||
const account = Redux.useSelector(state => state.user.profile);
|
||||
|
@ -53,14 +61,6 @@ module.exports = function UserProfile() {
|
|||
});
|
||||
}
|
||||
|
||||
// function removeFile(name) {
|
||||
// return function(e) {
|
||||
// e.preventDefault();
|
||||
// dispatch(user.setProfileVal([name, ""]));
|
||||
// dispatch(user.setProfileVal([`${name}File`, ""]));
|
||||
// };
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="user-profile">
|
||||
<h1>Profile</h1>
|
||||
|
@ -79,42 +79,42 @@ module.exports = function UserProfile() {
|
|||
<div className="files">
|
||||
<div>
|
||||
<h3>Header</h3>
|
||||
<div className="picker">
|
||||
<label htmlFor="header" className="file-input button">Browse</label>
|
||||
<span>{account.headerFile ? account.headerFile.name : "no file selected"}</span>
|
||||
</div>
|
||||
{/* <a onClick={removeFile("header")} href="#">remove</a> */}
|
||||
<input className="hidden" id="header" type="file" accept="image/*" onChange={onFileChange("header")} />
|
||||
<File
|
||||
id="header"
|
||||
fileType="image/*"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Avatar</h3>
|
||||
<div className="picker">
|
||||
<label htmlFor="avatar" className="file-input button">Browse</label>
|
||||
<span>{account.avatarFile ? account.avatarFile.name : "no file selected"}</span>
|
||||
</div>
|
||||
{/* <a onClick={removeFile("avatar")} href="#">remove</a> */}
|
||||
<input className="hidden" id="avatar" type="file" accept="image/*" onChange={onFileChange("avatar")} />
|
||||
<File
|
||||
id="avatar"
|
||||
fileType="image/*"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="labelinput">
|
||||
<label htmlFor="displayname">Name</label>
|
||||
<input id="displayname" type="text" value={account.display_name} onChange={onTextChange("display_name")} placeholder="A GoToSocial user" />
|
||||
</div>
|
||||
<div className="labelinput">
|
||||
<label htmlFor="bio">Bio</label>
|
||||
<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 className="labelcheckbox">
|
||||
<label htmlFor="locked">Manually approve follow requests?</label>
|
||||
<input id="locked" type="checkbox" checked={account.locked} onChange={onCheckChange("locked")} />
|
||||
</div>
|
||||
<TextInput
|
||||
id="display_name"
|
||||
name="Name"
|
||||
placeHolder="A GoToSocial user"
|
||||
/>
|
||||
<TextArea
|
||||
id="source.note"
|
||||
name="Bio"
|
||||
placeHolder="Just trying out GoToSocial, my pronouns are they/them and I like sloths."
|
||||
/>
|
||||
<Checkbox
|
||||
id="locked"
|
||||
name="Manually approve follow requests? "
|
||||
/>
|
||||
{ !allowCustomCSS ? null :
|
||||
<div className="labelinput">
|
||||
<label htmlFor="customcss">Custom CSS</label>
|
||||
<textarea className="mono" id="customcss" value={account.custom_css} onChange={onTextChange("custom_css")}/>
|
||||
<a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about custom CSS (opens in a new tab)</a>
|
||||
</div>
|
||||
<TextArea
|
||||
id="custom_css"
|
||||
name="Custom CSS"
|
||||
className="monospace"
|
||||
>
|
||||
<a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about custom profile CSS (opens in a new tab)</a>
|
||||
</TextArea>
|
||||
}
|
||||
<Submit onClick={submit} label="Save profile info" errorMsg={errorMsg} statusMsg={statusMsg} />
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue