restructure form fields
This commit is contained in:
parent
b01267de17
commit
cb137589a0
|
@ -340,4 +340,8 @@ footer {
|
||||||
margin: -0.5ex 0 0;
|
margin: -0.5ex 0 0;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monospace {
|
||||||
|
font-family: monospace;
|
||||||
}
|
}
|
|
@ -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,23 +20,24 @@
|
||||||
|
|
||||||
const Promise = require("bluebird");
|
const Promise = require("bluebird");
|
||||||
const { isPlainObject } = require("is-plain-object");
|
const { isPlainObject } = require("is-plain-object");
|
||||||
|
const d = require("dotty");
|
||||||
|
|
||||||
const { APIError } = require("../errors");
|
const { APIError } = require("../errors");
|
||||||
const { setInstanceInfo, setNamedInstanceInfo } = require("../../redux/reducers/instances").actions;
|
const { setInstanceInfo, setNamedInstanceInfo } = require("../../redux/reducers/instances").actions;
|
||||||
const oauth = require("../../redux/reducers/oauth").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) {
|
return function (dispatch, getState) {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
let base = state.oauth.instance;
|
let base = state.oauth.instance;
|
||||||
let auth = state.oauth.token;
|
let auth = state.oauth.token;
|
||||||
console.log(method, base, route, "auth:", auth != undefined);
|
console.log(method, base, route, "auth:", auth != undefined);
|
||||||
|
|
||||||
return Promise.try(() => {
|
return Promise.try(() => {
|
||||||
let url = new URL(base);
|
let url = new URL(base);
|
||||||
url.pathname = route;
|
url.pathname = route;
|
||||||
let body;
|
let body;
|
||||||
|
|
||||||
let headers = {
|
let headers = {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
};
|
};
|
||||||
|
@ -50,20 +51,24 @@ function apiCall(method, route, payload, type="json") {
|
||||||
Object.entries(payload).forEach(([key, val]) => {
|
Object.entries(payload).forEach(([key, val]) => {
|
||||||
if (isPlainObject(val)) {
|
if (isPlainObject(val)) {
|
||||||
Object.entries(val).forEach(([key2, val2]) => {
|
Object.entries(val).forEach(([key2, val2]) => {
|
||||||
formData.set(`${key}[${key2}]`, val2);
|
if (val2 != undefined) {
|
||||||
|
formData.set(`${key}[${key2}]`, val2);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
formData.set(key, val);
|
if (val != undefined) {
|
||||||
|
formData.set(key, val);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
body = formData;
|
body = formData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth != undefined) {
|
if (auth != undefined) {
|
||||||
headers["Authorization"] = auth;
|
headers["Authorization"] = auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(url.toString(), {
|
return fetch(url.toString(), {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
|
@ -74,7 +79,7 @@ function apiCall(method, route, payload, type="json") {
|
||||||
let json = res.json().catch((e) => {
|
let json = res.json().catch((e) => {
|
||||||
throw new APIError(`JSON parsing error: ${e.message}`);
|
throw new APIError(`JSON parsing error: ${e.message}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all([res, json]);
|
return Promise.all([res, json]);
|
||||||
}).then(([res, json]) => {
|
}).then(([res, json]) => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
@ -83,7 +88,7 @@ function apiCall(method, route, payload, type="json") {
|
||||||
dispatch(oauth.remove());
|
dispatch(oauth.remove());
|
||||||
throw new APIError("Stored OAUTH login was no longer valid, please log in again.");
|
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 {
|
} else {
|
||||||
return json;
|
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() {
|
function getCurrentUrl() {
|
||||||
return `${window.location.origin}${window.location.pathname}`;
|
return `${window.location.origin}${window.location.pathname}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchInstanceWithoutStore(domain) {
|
function fetchInstanceWithoutStore(domain) {
|
||||||
return function(dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
return Promise.try(() => {
|
return Promise.try(() => {
|
||||||
let lookup = getState().instances.info[domain];
|
let lookup = getState().instances.info[domain];
|
||||||
if (lookup != undefined) {
|
if (lookup != undefined) {
|
||||||
|
@ -107,7 +140,7 @@ function fetchInstanceWithoutStore(domain) {
|
||||||
// but we don't want to store it there yet
|
// but we don't want to store it there yet
|
||||||
// so we mock the API here with our function argument
|
// so we mock the API here with our function argument
|
||||||
let fakeState = {
|
let fakeState = {
|
||||||
oauth: {instance: domain}
|
oauth: { instance: domain }
|
||||||
};
|
};
|
||||||
|
|
||||||
return apiCall("GET", "/api/v1/instance")(dispatch, () => fakeState);
|
return apiCall("GET", "/api/v1/instance")(dispatch, () => fakeState);
|
||||||
|
@ -121,7 +154,7 @@ function fetchInstanceWithoutStore(domain) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchInstance() {
|
function fetchInstance() {
|
||||||
return function(dispatch, _getState) {
|
return function (dispatch, _getState) {
|
||||||
return Promise.try(() => {
|
return Promise.try(() => {
|
||||||
return dispatch(apiCall("GET", "/api/v1/instance"));
|
return dispatch(apiCall("GET", "/api/v1/instance"));
|
||||||
}).then((json) => {
|
}).then((json) => {
|
||||||
|
@ -133,12 +166,15 @@ function fetchInstance() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let submoduleArgs = { apiCall, getCurrentUrl, getChanges };
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
instance: {
|
instance: {
|
||||||
fetchWithoutStore: fetchInstanceWithoutStore,
|
fetchWithoutStore: fetchInstanceWithoutStore,
|
||||||
fetch: fetchInstance
|
fetch: fetchInstance
|
||||||
},
|
},
|
||||||
oauth: require("./oauth")({apiCall, getCurrentUrl}),
|
oauth: require("./oauth")(submoduleArgs),
|
||||||
user: require("./user")({apiCall}),
|
user: require("./user")(submoduleArgs),
|
||||||
apiCall
|
apiCall,
|
||||||
|
getChanges
|
||||||
};
|
};
|
|
@ -23,28 +23,13 @@ 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, getChanges }) {
|
||||||
function updateCredentials(selector, {formKeys=[], renamedKeys=[], fileKeys=[]}) {
|
function updateCredentials(selector, keys) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
return Promise.try(() => {
|
return Promise.try(() => {
|
||||||
const state = selector(getState());
|
const state = selector(getState());
|
||||||
|
|
||||||
const update = {};
|
const update = getChanges(state, keys);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return dispatch(apiCall("PATCH", "/api/v1/accounts/update_credentials", update, "form"));
|
return dispatch(apiCall("PATCH", "/api/v1/accounts/update_credentials", update, "form"));
|
||||||
}).then((account) => {
|
}).then((account) => {
|
||||||
|
@ -63,13 +48,17 @@ module.exports = function ({ apiCall }) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProfile: function updateProfile() {
|
updateProfile: function updateProfile() {
|
||||||
const formKeys = ["display_name", "locked", "source", "custom_css"];
|
const formKeys = ["display_name", "locked", "source", "custom_css", "source.note"];
|
||||||
const renamedKeys = [["note", "source.note"]];
|
const renamedKeys = {
|
||||||
|
"source.note": "note"
|
||||||
|
};
|
||||||
const fileKeys = ["header", "avatar"];
|
const fileKeys = ["header", "avatar"];
|
||||||
|
|
||||||
return updateCredentials((state) => state.user.profile, {formKeys, renamedKeys, fileKeys});
|
return updateCredentials((state) => state.user.profile, {formKeys, renamedKeys, fileKeys});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSettings: function updateProfile() {
|
updateSettings: function updateProfile() {
|
||||||
const formKeys = ["source"];
|
const formKeys = ["source"];
|
||||||
|
|
||||||
|
|
|
@ -277,7 +277,7 @@ section.with-sidebar > div {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
div.picker {
|
div.form-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,17 @@ const Redux = require("react-redux");
|
||||||
const Submit = require("../components/submit");
|
const Submit = require("../components/submit");
|
||||||
|
|
||||||
const api = require("../lib/api");
|
const api = require("../lib/api");
|
||||||
const formFields = require("../lib/form-fields");
|
|
||||||
const user = require("../redux/reducers/user").actions;
|
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() {
|
module.exports = function UserProfile() {
|
||||||
const dispatch = Redux.useDispatch();
|
const dispatch = Redux.useDispatch();
|
||||||
const account = Redux.useSelector(state => state.user.profile);
|
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 (
|
return (
|
||||||
<div className="user-profile">
|
<div className="user-profile">
|
||||||
<h1>Profile</h1>
|
<h1>Profile</h1>
|
||||||
|
@ -79,42 +79,42 @@ module.exports = function UserProfile() {
|
||||||
<div className="files">
|
<div className="files">
|
||||||
<div>
|
<div>
|
||||||
<h3>Header</h3>
|
<h3>Header</h3>
|
||||||
<div className="picker">
|
<File
|
||||||
<label htmlFor="header" className="file-input button">Browse</label>
|
id="header"
|
||||||
<span>{account.headerFile ? account.headerFile.name : "no file selected"}</span>
|
fileType="image/*"
|
||||||
</div>
|
/>
|
||||||
{/* <a onClick={removeFile("header")} href="#">remove</a> */}
|
|
||||||
<input className="hidden" id="header" type="file" accept="image/*" onChange={onFileChange("header")} />
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>Avatar</h3>
|
<h3>Avatar</h3>
|
||||||
<div className="picker">
|
<File
|
||||||
<label htmlFor="avatar" className="file-input button">Browse</label>
|
id="avatar"
|
||||||
<span>{account.avatarFile ? account.avatarFile.name : "no file selected"}</span>
|
fileType="image/*"
|
||||||
</div>
|
/>
|
||||||
{/* <a onClick={removeFile("avatar")} href="#">remove</a> */}
|
|
||||||
<input className="hidden" id="avatar" type="file" accept="image/*" onChange={onFileChange("avatar")} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="labelinput">
|
<TextInput
|
||||||
<label htmlFor="displayname">Name</label>
|
id="display_name"
|
||||||
<input id="displayname" type="text" value={account.display_name} onChange={onTextChange("display_name")} placeholder="A GoToSocial user" />
|
name="Name"
|
||||||
</div>
|
placeHolder="A GoToSocial user"
|
||||||
<div className="labelinput">
|
/>
|
||||||
<label htmlFor="bio">Bio</label>
|
<TextArea
|
||||||
<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." />
|
id="source.note"
|
||||||
</div>
|
name="Bio"
|
||||||
<div className="labelcheckbox">
|
placeHolder="Just trying out GoToSocial, my pronouns are they/them and I like sloths."
|
||||||
<label htmlFor="locked">Manually approve follow requests?</label>
|
/>
|
||||||
<input id="locked" type="checkbox" checked={account.locked} onChange={onCheckChange("locked")} />
|
<Checkbox
|
||||||
</div>
|
id="locked"
|
||||||
|
name="Manually approve follow requests? "
|
||||||
|
/>
|
||||||
{ !allowCustomCSS ? null :
|
{ !allowCustomCSS ? null :
|
||||||
<div className="labelinput">
|
<TextArea
|
||||||
<label htmlFor="customcss">Custom CSS</label>
|
id="custom_css"
|
||||||
<textarea className="mono" id="customcss" value={account.custom_css} onChange={onTextChange("custom_css")}/>
|
name="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>
|
className="monospace"
|
||||||
</div>
|
>
|
||||||
|
<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} />
|
<Submit onClick={submit} label="Save profile info" errorMsg={errorMsg} statusMsg={statusMsg} />
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue