alot of settings stuff

This commit is contained in:
Roman Hergenreder 2020-06-26 17:24:57 +02:00
parent 09475be545
commit f5a8f2c777
5 changed files with 2578 additions and 250 deletions

2106
js/admin.min.js vendored

File diff suppressed because one or more lines are too long

90
src/package-lock.json generated

@ -4678,6 +4678,56 @@
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz",
"integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA=="
}, },
"draft-js": {
"version": "0.11.6",
"resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.11.6.tgz",
"integrity": "sha512-H8OaophZecxm0L/C0LvOo6IZrbMb+s0txPXF7DW3E2oUjSo5ufvWvWb3B/0K2wRqBnwJrRxApqljD899i0fZMA==",
"requires": {
"fbjs": "^1.0.0",
"immutable": "~3.7.4",
"object-assign": "^4.1.1"
},
"dependencies": {
"core-js": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
},
"fbjs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-1.0.0.tgz",
"integrity": "sha512-MUgcMEJaFhCaF1QtWGnmq9ZDRAzECTCRAF7O6UZIlAlkTs1SasiX9aP0Iw7wfD2mJ7wDTNfg2w7u5fSCwJk1OA==",
"requires": {
"core-js": "^2.4.1",
"fbjs-css-vars": "^1.0.0",
"isomorphic-fetch": "^2.1.1",
"loose-envify": "^1.0.0",
"object-assign": "^4.1.0",
"promise": "^7.1.1",
"setimmediate": "^1.0.5",
"ua-parser-js": "^0.7.18"
}
},
"promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"requires": {
"asap": "~2.0.3"
}
}
}
},
"draftjs-to-html": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/draftjs-to-html/-/draftjs-to-html-0.9.1.tgz",
"integrity": "sha512-fFstE6+IayaVFBEvaFt/wN8vdj8FsTRzij7dy7LI9QIwf5LgfHFi9zSpvCg+feJ2tbYVqHxUkjcibwpsTpgFVQ=="
},
"draftjs-utils": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/draftjs-utils/-/draftjs-utils-0.10.2.tgz",
"integrity": "sha512-EstHqr3R3JVcilJrBaO/A+01GvwwKmC7e4TCjC7S94ZeMh4IVmf60OuQXtHHpwItK8C2JCi3iljgN5KHkJboUg=="
},
"duplexer": { "duplexer": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
@ -5723,6 +5773,11 @@
} }
} }
}, },
"fbjs-css-vars": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
"integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="
},
"figgy-pudding": { "figgy-pudding": {
"version": "3.5.2", "version": "3.5.2",
"resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
@ -6476,6 +6531,11 @@
} }
} }
}, },
"html-to-draftjs": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/html-to-draftjs/-/html-to-draftjs-1.5.0.tgz",
"integrity": "sha512-kggLXBNciKDwKf+KYsuE+V5gw4dZ7nHyGMX9m0wy7urzWjKGWyNFetmArRLvRV0VrxKN70WylFsJvMTJx02OBQ=="
},
"html-webpack-plugin": { "html-webpack-plugin": {
"version": "4.0.0-beta.11", "version": "4.0.0-beta.11",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.0.0-beta.11.tgz", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.0.0-beta.11.tgz",
@ -6629,6 +6689,11 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==" "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg=="
}, },
"immutable": {
"version": "3.7.6",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz",
"integrity": "sha1-E7TTyxK++hVIKib+Gy665kAHHks="
},
"import-cwd": { "import-cwd": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@ -7895,6 +7960,14 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
}, },
"linkify-it": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
"requires": {
"uc.micro": "^1.0.1"
}
},
"load-json-file": { "load-json-file": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
@ -10855,6 +10928,18 @@
"scheduler": "^0.19.1" "scheduler": "^0.19.1"
} }
}, },
"react-draft-wysiwyg": {
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/react-draft-wysiwyg/-/react-draft-wysiwyg-1.14.5.tgz",
"integrity": "sha512-utbJEs91757QXYoBwKRb/4kB3JdswLlj0heUiAeXs/OxZAUISJXxLMFLBIixRlIcUnNkwxOsMikRshDMtWIS3g==",
"requires": {
"classnames": "^2.2.6",
"draftjs-utils": "^0.10.2",
"html-to-draftjs": "^1.5.0",
"linkify-it": "^2.2.0",
"prop-types": "^15.7.2"
}
},
"react-error-overlay": { "react-error-overlay": {
"version": "6.0.7", "version": "6.0.7",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
@ -13137,6 +13222,11 @@
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz",
"integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==" "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ=="
}, },
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"unicode-canonical-property-names-ecmascript": { "unicode-canonical-property-names-ecmascript": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",

@ -4,12 +4,16 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"chart.js": "^2.9.3", "chart.js": "^2.9.3",
"draft-js": "^0.11.6",
"draftjs-to-html": "^0.9.1",
"html-to-draftjs": "latest",
"moment": "^2.26.0", "moment": "^2.26.0",
"rc-color-picker": "^1.2.6", "rc-color-picker": "^1.2.6",
"react": "^16.13.1", "react": "^16.13.1",
"react-chartjs-2": "^2.9.0", "react-chartjs-2": "^2.9.0",
"react-collapse": "^5.0.1", "react-collapse": "^5.0.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-draft-wysiwyg": "^1.14.5",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "^3.4.1", "react-scripts": "^3.4.1",
"react-select": "^3.1.0", "react-select": "^3.1.0",

@ -3,6 +3,11 @@ import {Link} from "react-router-dom";
import Alert from "../elements/alert"; import Alert from "../elements/alert";
import {Collapse} from "react-collapse/lib/Collapse"; import {Collapse} from "react-collapse/lib/Collapse";
import Icon from "../elements/icon"; import Icon from "../elements/icon";
import { EditorState, ContentState, convertFromHTML, convertToRaw } from 'draft-js'
import { Editor } from 'react-draft-wysiwyg'
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
export default class Settings extends React.Component { export default class Settings extends React.Component {
@ -11,114 +16,114 @@ export default class Settings extends React.Component {
this.state = { this.state = {
errors: [], errors: [],
mailErrors: [],
generalErrors: [],
etcErrors: [],
settings: {}, settings: {},
generalOpened: true, general: {
mailOpened: true, alerts: [],
etcOpened: true, isOpen: true,
isResetting: false, isSaving: false,
isSaving: false, isResetting: false,
isSending: false, keys: ["site_name", "base_url", "user_registration_enabled"]
test_email: "", },
unsavedMailSettings: false mail: {
alerts: [],
isOpen: true,
isSaving: false,
isResetting: false,
isSending: false,
test_email: "",
unsavedMailSettings: false,
keys: ["mail_enabled", "mail_host", "mail_port", "mail_username", "mail_password", "mail_from"]
},
messages: {
alerts: [],
isOpen: true,
isSaving: false,
isResetting: false,
editor: EditorState.createEmpty(),
isEditing: null,
keys: ["message_confirm_email", "message_accept_invite", "message_reset_password"]
},
uncategorised: {
alerts: [],
isOpen: true,
isSaving: false,
isResetting: false,
},
}; };
this.parent = { this.parent = {
api: props.api api: props.api
}; };
this.mailKeys = ["mail_enabled", "mail_host", "mail_port", "mail_username", "mail_password", "mail_from"];
this.generalKeys = ["site_name", "base_url", "user_registration_enabled"];
} }
componentDidMount() { componentDidMount() {
this.parent.api.getSettings().then((res) => { this.parent.api.getSettings().then((res) => {
if (res.success) { if (res.success) {
this.setState({...this.state, settings: res.settings}); this.setState({ ...this.state, settings: res.settings });
} else { } else {
let errors = this.state.errors.slice(); let errors = this.state.errors.slice();
errors.push({title: "Error fetching settings", message: res.msg}); errors.push({title: "Error fetching settings", message: res.msg});
this.setState({...this.state, errors: errors}); this.setState({...this.state, errors: errors });
} }
}); });
} }
removeError(key, i) { removeError(i, category = null) {
if (i >= 0 && i < this.state[key].length) { if (category) {
let errors = this.state[key].slice(); if (i >= 0 && i < this.state[category].alerts.length) {
errors.splice(i, 1); let alerts = this.state[category].alerts.slice();
this.setState({...this.state, [key]: errors}); alerts.splice(i, 1);
this.setState({...this.state, [category]: {...this.state[category], alerts: alerts}});
}
} else {
if (i >= 0 && i < this.state.errors.length) {
let errors = this.state.errors.slice();
errors.splice(i, 1);
this.setState({...this.state, errors: errors});
}
} }
} }
toggleCollapse(key) { toggleCollapse(category) {
this.setState({...this.state, [key]: !this.state[key]}); this.setState({...this.state, [category]: {...this.state[category], isOpen: !this.state[category].isOpen}});
} }
getGeneralCard() { createCard(category, color, icon, title, content) {
let errors = []; let alerts = [];
for (let i = 0; i < this.state.generalErrors.length; i++) { for (let i = 0; i < this.state[category].alerts.length; i++) {
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError("generalErrors", i)} {...this.state.generalErrors[i]}/>) alerts.push(<Alert key={"alert-" + i}
onClose={() => this.removeError(i, category)} {...this.state[category].alerts[i]}/>)
} }
return <> return <div className={"card card-" + color} key={"card-" + category}>
<div className={"card-header"} style={{cursor: "pointer"}} <div className={"card-header"} style={{cursor: "pointer"}}
onClick={() => this.toggleCollapse("generalOpened")}> onClick={() => this.toggleCollapse(category)}>
<h4 className={"card-title"}> <h4 className={"card-title"}>
<Icon className={"mr-2"} icon={"cogs"}/> <Icon className={"mr-2"} icon={icon}/>
General Settings {title}
</h4> </h4>
<div className={"card-tools"}> <div className={"card-tools"}>
<span className={"btn btn-tool btn-sm"}> <span className={"btn btn-tool btn-sm"}>
<Icon icon={this.state.generalOpened ? "angle-up" : "angle-down"}/> <Icon icon={this.state[category].isOpen ? "angle-up" : "angle-down"}/>
</span> </span>
</div> </div>
</div> </div>
<Collapse isOpened={this.state.generalOpened}> <Collapse isOpened={this.state[category].isOpen}>
<div className={"card-body"}> <div className={"card-body"}>
<div className={"row"}> <div className={"row"}>
<div className={"col-12 col-lg-6"}> <div className={"col-12 col-lg-6"}>
{errors} {alerts}
<div className={"form-group"}> {content}
<label htmlFor={"site_name"}>Site Name</label>
<input type={"text"} className={"form-control"}
value={this.state.settings["site_name"] ?? ""}
placeholder={"Enter a title"} name={"site_name"} id={"site_name"}
onChange={this.onChangeValue.bind(this)}/>
</div>
<div className={"form-group"}>
<label htmlFor={"base_url"}>Base URL</label>
<input type={"text"} className={"form-control"}
value={this.state.settings["base_url"] ?? ""}
placeholder={"Enter a url"} name={"base_url"} id={"base_url"}
onChange={this.onChangeValue.bind(this)}/>
</div>
<div className={"form-group"}>
<label htmlFor={"user_registration_enabled"}>User Registration</label>
<div className={"form-check"}>
<input type={"checkbox"} className={"form-check-input"}
name={"user_registration_enabled"}
id={"user_registration_enabled"}
defaultChecked={(this.state.settings["user_registration_enabled"] ?? "0") === "1"}
onChange={this.onChangeValue.bind(this)}/>
<label className={"form-check-label"}
htmlFor={"user_registration_enabled"}>
Allow anyone to register an account
</label>
</div>
</div>
<div> <div>
<button className={"btn btn-secondary ml-2"} onClick={() => this.onReset("generalErrors", this.generalKeys)} <button className={"btn btn-secondary"} onClick={() => this.onReset(category)}
disabled={this.state.isResetting || this.state.isSaving}> disabled={this.state[category].isResetting || this.state[category].isSaving}>
{this.state.isResetting ? {this.state[category].isResetting ?
<span>Resetting&nbsp;<Icon icon={"circle-notch"}/></span> : "Reset"} <span>Resetting&nbsp;<Icon icon={"circle-notch"}/></span> : "Reset"}
</button> </button>
<button className={"btn btn-success ml-2"} onClick={() => this.onSave("generalErrors", this.generalKeys)} <button className={"btn btn-success ml-2"} onClick={() => this.onSave(category)}
disabled={this.state.isResetting || this.state.isSaving}> disabled={this.state[category].isResetting || this.state[category].isSaving}>
{this.state.isSaving ? {this.state[category].isSaving ?
<span>Saving&nbsp;<Icon icon={"circle-notch"}/></span> : "Save"} <span>Saving&nbsp;<Icon icon={"circle-notch"}/></span> : "Save"}
</button> </button>
</div> </div>
@ -126,163 +131,209 @@ export default class Settings extends React.Component {
</div> </div>
</div> </div>
</Collapse> </Collapse>
</div>
}
createGeneralForm() {
return <>
<div className={"form-group"}>
<label htmlFor={"site_name"}>Site Name</label>
<input type={"text"} className={"form-control"}
value={this.state.settings["site_name"] ?? ""}
placeholder={"Enter a title"} name={"site_name"} id={"site_name"}
onChange={this.onChangeValue.bind(this)}/>
</div>
<div className={"form-group"}>
<label htmlFor={"base_url"}>Base URL</label>
<input type={"text"} className={"form-control"}
value={this.state.settings["base_url"] ?? ""}
placeholder={"Enter a url"} name={"base_url"} id={"base_url"}
onChange={this.onChangeValue.bind(this)}/>
</div>
<div className={"form-group"}>
<label htmlFor={"user_registration_enabled"}>User Registration</label>
<div className={"form-check"}>
<input type={"checkbox"} className={"form-check-input"}
name={"user_registration_enabled"}
id={"user_registration_enabled"}
checked={(this.state.settings["user_registration_enabled"] ?? "0") === "1"}
onChange={this.onChangeValue.bind(this)}/>
<label className={"form-check-label"}
htmlFor={"user_registration_enabled"}>
Allow anyone to register an account
</label>
</div>
</div>
</> </>
} }
getEmailCard() { createMailForm() {
let errors = [];
for (let i = 0; i < this.state.mailErrors.length; i++) {
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError("mailErrors", i)} {...this.state.mailErrors[i]}/>)
}
return <> return <>
<div className={"card-header"} style={{cursor: "pointer"}} <div className={"form-group mt-2"}>
onClick={() => this.toggleCollapse("mailOpened")}> <div className={"form-check"}>
<h4 className={"card-title"}> <input type={"checkbox"} className={"form-check-input"}
<Icon className={"mr-2"} icon={"envelope"}/> name={"mail_enabled"} id={"mail_enabled"}
Mail Settings checked={(this.state.settings["mail_enabled"] ?? "0") === "1"}
</h4> onChange={this.onChangeValue.bind(this)}/>
<div className={"card-tools"}> <label className={"form-check-label"} htmlFor={"mail_enabled"}>
<span className={"btn btn-tool btn-sm"}> Enable E-Mail service
<Icon icon={this.state.mailOpened ? "angle-up" : "angle-down"}/> </label>
</span>
</div> </div>
</div> </div>
<Collapse isOpened={this.state.mailOpened}> <hr className={"m-3"}/>
<div className={"card-body"}> <label htmlFor={"mail_username"}>Username</label>
<div className={"row"}> <div className={"input-group"}>
<div className={"col-12 col-lg-6"}> <div className={"input-group-prepend"}>
{errors}
<div className={"form-group mt-2"}>
<div className={"form-check"}>
<input type={"checkbox"} className={"form-check-input"}
name={"mail_enabled"} id={"mail_enabled"}
checked={(this.state.settings["mail_enabled"] ?? "0") === "1"}
onChange={this.onChangeValue.bind(this)}/>
<label className={"form-check-label"} htmlFor={"mail_enabled"}>
Enable E-Mail service
</label>
</div>
<hr className={"m-3"}/>
<label htmlFor={"mail_username"}>Username</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}> <span className={"input-group-text"}>
<Icon icon={"hashtag"}/> <Icon icon={"hashtag"}/>
</span> </span>
</div> </div>
<input type={"text"} className={"form-control"} <input type={"text"} className={"form-control"}
value={this.state.settings["mail_username"] ?? ""} value={this.state.settings["mail_username"] ?? ""}
placeholder={"Enter a username"} name={"mail_username"} placeholder={"Enter a username"} name={"mail_username"}
id={"mail_username"} onChange={this.onChangeValue.bind(this)} id={"mail_username"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/> disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div> </div>
<label htmlFor={"mail_password"} className={"mt-2"}>Password</label> <label htmlFor={"mail_password"} className={"mt-2"}>Password</label>
<div className={"input-group"}> <div className={"input-group"}>
<div className={"input-group-prepend"}> <div className={"input-group-prepend"}>
<span className={"input-group-text"}> <span className={"input-group-text"}>
<Icon icon={"key"}/> <Icon icon={"key"}/>
</span> </span>
</div> </div>
<input type={"password"} className={"form-control"} <input type={"password"} className={"form-control"}
value={this.state.settings["mail_password"] ?? ""} value={this.state.settings["mail_password"] ?? ""}
placeholder={"(unchanged)"} name={"mail_password"} placeholder={"(unchanged)"} name={"mail_password"}
id={"mail_password"} onChange={this.onChangeValue.bind(this)} id={"mail_password"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/> disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div> </div>
<label htmlFor={"mail_from"} className={"mt-2"}>Sender Email Address</label> <label htmlFor={"mail_from"} className={"mt-2"}>Sender Email Address</label>
<div className={"input-group"}> <div className={"input-group"}>
<div className={"input-group-prepend"}> <div className={"input-group-prepend"}>
<span className={"input-group-text"}>@</span> <span className={"input-group-text"}>@</span>
</div> </div>
<input type={"email"} className={"form-control"} <input type={"email"} className={"form-control"}
value={this.state.settings["mail_from"] ?? ""} value={this.state.settings["mail_from"] ?? ""}
placeholder={"Enter a email address"} name={"mail_from"} placeholder={"Enter a email address"} name={"mail_from"}
id={"mail_from"} onChange={this.onChangeValue.bind(this)} id={"mail_from"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/> disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div> </div>
<div className={"row"}> <div className={"row"}>
<div className={"col-6"}> <div className={"col-6"}>
<label htmlFor={"mail_host"} className={"mt-2"}>SMTP Host</label> <label htmlFor={"mail_host"} className={"mt-2"}>SMTP Host</label>
<div className={"input-group"}> <div className={"input-group"}>
<div className={"input-group-prepend"}> <div className={"input-group-prepend"}>
<span className={"input-group-text"}> <span className={"input-group-text"}>
<Icon icon={"project-diagram"}/> <Icon icon={"project-diagram"}/>
</span> </span>
</div>
<input type={"text"} className={"form-control"}
value={this.state.settings["mail_host"] ?? ""}
placeholder={"e.g. smtp.example.com"} name={"mail_host"}
id={"mail_host"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
</div>
<div className={"col-6"}>
<label htmlFor={"mail_port"} className={"mt-2"}>SMTP Port</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"project-diagram"}/>
</span>
</div>
<input type={"number"} className={"form-control"}
value={parseInt(this.state.settings["mail_port"] ?? "25")}
placeholder={"smtp port"} name={"mail_port"}
id={"mail_port"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
</div>
</div>
</div>
<div>
<button className={"btn btn-secondary ml-2"}
onClick={() => this.onReset("mailErrors", this.mailKeys)}
disabled={this.state.isResetting || this.state.isSaving}>
{this.state.isResetting ?
<span>Resetting&nbsp;<Icon icon={"circle-notch"}/></span> : "Reset"}
</button>
<button className={"btn btn-success ml-2"}
onClick={() => this.onSave("mailErrors", this.mailKeys)}
disabled={this.state.isResetting || this.state.isSaving}>
{this.state.isSaving ?
<span>Saving&nbsp;<Icon icon={"circle-notch"}/></span> : "Save"}
</button>
</div>
<div className={"mt-3"}>
<label htmlFor={"mail_from"} className={"mt-2"}>Send Test E-Mail</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>@</span>
</div>
<input type={"email"} className={"form-control"}
value={this.state.test_email}
placeholder={"Enter a email address"}
onChange={(e) => this.setState({
...this.state,
test_email: e.target.value
})}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
<div className={"form-group form-inline mt-3"}>
<button className={"btn btn-info col-2"}
onClick={() => this.onSendTestMail()}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1" || this.state.isSending}>
{this.state.isSending ?
<span>Sending&nbsp;<Icon icon={"circle-notch"}/></span> : "Send Mail"}
</button>
<div className={"col-10"}>
{ this.state.unsavedMailSettings ? <span className={"text-red"}>You need to save your mail settings first.</span> : null }
</div>
</div>
</div>
</div> </div>
<input type={"text"} className={"form-control"}
value={this.state.settings["mail_host"] ?? ""}
placeholder={"e.g. smtp.example.com"} name={"mail_host"}
id={"mail_host"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div> </div>
</div> </div>
</Collapse> <div className={"col-6"}>
<label htmlFor={"mail_port"} className={"mt-2"}>SMTP Port</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"project-diagram"}/>
</span>
</div>
<input type={"number"} className={"form-control"}
value={parseInt(this.state.settings["mail_port"] ?? "25")}
placeholder={"smtp port"} name={"mail_port"}
id={"mail_port"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
</div>
</div>
<div className={"mt-3"}>
<label htmlFor={"mail_from"} className={"mt-2"}>Send Test E-Mail</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>@</span>
</div>
<input type={"email"} className={"form-control"}
value={this.state.mail.test_email}
placeholder={"Enter a email address"}
onChange={(e) => this.setState({
...this.state,
mail: {...this.state.mail, test_email: e.target.value},
})}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1"}/>
</div>
<div className={"form-group form-inline mt-3"}>
<button className={"btn btn-info col-2"}
onClick={() => this.onSendTestMail()}
disabled={(this.state.settings["mail_enabled"] ?? "0") !== "1" || this.state.mail.isSending}>
{this.state.mail.isSending ?
<span>Sending&nbsp;<Icon icon={"circle-notch"}/></span> : "Send Mail"}
</button>
<div className={"col-10"}>
{this.state.mail.unsavedMailSettings ?
<span className={"text-red"}>You need to save your mail settings first.</span> : null}
</div>
</div>
</div>
</> </>
} }
getMessagesForm() {
const editor = <Editor
editorState={this.state.messages.editor}
onEditorStateChange={this.onEditorStateChange.bind(this)}
/>;
let messageTemplates = {
"message_confirm_email": "Confirm E-Mail Message",
"message_accept_invite": "Accept Invitation Message",
"message_reset_password": "Reset Password Message",
};
let formGroups = [];
for (let key in messageTemplates) {
let title = messageTemplates[key];
if (this.state.messages.isEditing === key) {
formGroups.push(
<div className={"form-group"} key={"group-" + key}>
<label htmlFor={key}>
{ title }
<Icon icon={"times"} className={"ml-2 text-danger"} style={{cursor: "pointer"}}
onClick={() => this.closeEditor(false)}
/>
<Icon icon={"check"} className={"ml-1 text-success"} style={{cursor: "pointer"}}
onClick={() => this.closeEditor(true)}
/>
</label>
{ editor }
</div>
);
} else {
formGroups.push(
<div className={"form-group"} key={"group-" + key}>
<label htmlFor={key}>
{ title }
<Icon icon={"pencil-alt"} className={"ml-2"} style={{cursor: "pointer"}}
onClick={() => this.openEditor(key)}
/>
</label>
<textarea rows={6} className={"form-control"} disabled={true}
value={this.state.settings[key] ?? ""} id={key} name={key}
/>
</div>
);
}
}
return formGroups;
}
/*
getUncategorisedCard() { getUncategorisedCard() {
let keys = []; let keys = [];
@ -351,12 +402,26 @@ export default class Settings extends React.Component {
</Collapse> </Collapse>
</> </>
} }
*/
render() { render() {
let errors = []; let errors = [];
for (let i = 0; i < this.state.errors.length; i++) { for (let i = 0; i < this.state.errors.length; i++) {
errors.push(<Alert key={"error-" + i} onClose={() => this.removeError("errors", i)} {...this.state.errors[i]}/>) errors.push(<Alert key={"error-" + i}
onClose={() => this.removeError("errors", i)} {...this.state.errors[i]}/>)
}
const categories = {
"general": {color: "primary", icon: "cogs", title: "General Settings", content: this.createGeneralForm()},
"mail": {color: "warning", icon: "envelope", title: "Mail Settings", content: this.createMailForm()},
"messages": {color: "info", icon: "copy", title: "Message Templates", content: this.getMessagesForm()},
"uncategorised": {color: "secondary", icon: "stream", title: "Uncategorised", content: <></>},
};
let cards = [];
for (let name in categories) {
let category = categories[name];
cards.push(this.createCard(name, category.color, category.icon, category.title, category.content));
} }
return <> return <>
@ -378,20 +443,22 @@ export default class Settings extends React.Component {
<div className={"content"}> <div className={"content"}>
{errors} {errors}
<div> <div>
<div className={"card card-primary"}> {cards}
{this.getGeneralCard()}
</div>
<div className={"card card-warning"}>
{this.getEmailCard()}
</div>
<div className={"card card-secondary"}>
{this.getUncategorisedCard()}
</div>
</div> </div>
</div> </div>
</> </>
} }
onEditorStateChange(editorState, key) {
this.setState({
...this.state,
messages: {
...this.state.messages,
editor: editorState
}
});
};
onChangeValue(event) { onChangeValue(event) {
const target = event.target; const target = event.target;
const name = target.name; const name = target.name;
@ -403,45 +470,65 @@ export default class Settings extends React.Component {
} }
let changedMailSettings = false; let changedMailSettings = false;
if (name.startsWith("mail_")) { if (this.state.mail.keys.includes(name)) {
changedMailSettings = true; changedMailSettings = true;
} }
this.setState({...this.state, settings: {...this.state.settings, [name]: value}, let newState = {...this.state, settings: {...this.state.settings, [name]: value}};
unsavedMailSettings: changedMailSettings ? true : this.state.unsavedMailSettings if (changedMailSettings) {
}); newState.mail = {...this.state.mail, unsavedMailSettings: true};
}
this.setState(newState);
} }
onReset(errorKey, keys) { onReset(category) {
this.setState({...this.state, isResetting: true}); this.setState({...this.state, [category]: {...this.state[category], isResetting: true}});
let values = {}; let values = {};
for (let key of keys) { for (let key of this.state[category].keys) {
values[key] = this.state.settings[key]; values[key] = this.state.settings[key];
} }
let mailSettingsSaved = errorKey === "mailErrors";
this.parent.api.getSettings().then((res) => { this.parent.api.getSettings().then((res) => {
if (!res.success) { if (!res.success) {
let errors = this.state[errorKey].slice(); let alerts = this.state[category].alerts.slice();
errors.push({title: "Error fetching settings", message: res.msg}); alerts.push({title: "Error fetching settings", message: res.msg});
this.setState({...this.state, [errorKey]: errors, isResetting: false}); this.setState({
...this.state,
[category]: {...this.state[category], alerts: alerts, isResetting: false}
});
} else { } else {
let newSettings = {...this.state.settings}; let newSettings = {...this.state.settings};
for (let key of keys) { let categoryUpdated = {...this.state[category], isResetting: false};
for (let key of this.state[category].keys) {
newSettings[key] = res.settings[key] ?? ""; newSettings[key] = res.settings[key] ?? "";
} }
this.setState({...this.state, settings: newSettings, isResetting: false,
unsavedMailSettings: mailSettingsSaved ? false : this.state.unsavedMailSettings}); if (category === "mail") {
categoryUpdated.unsavedMailSettings = false;
} else if (category === "messages") {
categoryUpdated.isEditing = null;
}
this.setState({
...this.state, settings: newSettings,
[category]: categoryUpdated
});
} }
}); });
} }
onSave(errorKey, keys) { onSave(category) {
this.setState({...this.state, isSaving: true}); this.setState({...this.state, [category]: {...this.state[category], isSaving: true}});
if (category === "messages" && this.state.messages.isEditing) {
this.closeEditor(true, () => this.onSave(category));
}
let values = {}; let values = {};
for (let key of keys) { for (let key of this.state[category].keys) {
if (key === "mail_password" && !this.state.settings[key]) { if (key === "mail_password" && !this.state.settings[key]) {
continue; continue;
} }
@ -449,34 +536,77 @@ export default class Settings extends React.Component {
values[key] = this.state.settings[key]; values[key] = this.state.settings[key];
} }
let mailSettingsSaved = errorKey === "mailErrors";
this.parent.api.saveSettings(values).then((res) => { this.parent.api.saveSettings(values).then((res) => {
let alerts = this.state[category].alerts.slice();
let categoryUpdated = {...this.state[category], isSaving: false};
if (!res.success) { if (!res.success) {
let errors = this.state[errorKey].slice(); alerts.push({title: "Error fetching settings", message: res.msg});
errors.push({title: "Error fetching settings", message: res.msg});
this.setState({...this.state, [errorKey]: errors, isSaving: false});
} else { } else {
this.setState({...this.state, isSaving: false, unsavedMailSettings: mailSettingsSaved ? false : this.state.unsavedMailSettings }); alerts.push({title: "Success", message: "Settings were successfully saved.", type: "success"});
if (category === "mail") categoryUpdated.unsavedMailSettings = false;
this.setState({...this.state, [category]: categoryUpdated});
} }
categoryUpdated.alerts = alerts;
this.setState({...this.state, [category]: categoryUpdated});
}); });
} }
onSendTestMail() { onSendTestMail() {
this.setState({...this.state, isSending: true}); this.setState({...this.state, mail: {...this.state.mail, isSending: true}});
this.parent.api.sendTestMail(this.state.test_email).then((res) => { console.log(this.state.mail);
let errors = this.state.mailErrors.slice(); this.parent.api.sendTestMail(this.state.mail.test_email).then((res) => {
let alerts = this.state.mail.alerts.slice();
let newState = {...this.state.mail, isSending: false};
if (!res.success) { if (!res.success) {
errors.push({title: "Error sending email", message: res.msg}); alerts.push({title: "Error sending email", message: res.msg});
this.setState({...this.state, mailErrors: errors, isSending: false});
} else { } else {
errors.push({ alerts.push({
title: "Success!", title: "Success!",
message: "E-Mail was successfully sent, check your inbox.", message: "E-Mail was successfully sent, check your inbox.",
type: "success" type: "success"
}); });
this.setState({...this.state, mailErrors: errors, isSending: false, test_email: ""}); newState.test_email = "";
} }
newState.alerts = alerts;
this.setState({...this.state, mail: newState});
}); });
} }
closeEditor(save, callback = null) {
if (this.state.messages.isEditing) {
const key = this.state.messages.isEditing;
let newState = { ...this.state, messages: {...this.state.messages, isEditing: null }};
if (save) {
newState.settings = {
...this.state.settings,
[key]: draftToHtml(convertToRaw(this.state.messages.editor.getCurrentContent())),
};
}
callback = callback || function () { };
this.setState(newState, callback);
}
}
openEditor(message) {
this.closeEditor(true);
const contentBlock = htmlToDraft(this.state.settings[message] ?? "");
if (contentBlock) {
const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
const editorState = EditorState.createWithContent(contentState);
this.setState({
...this.state,
messages: {
...this.state.messages,
isEditing: message,
editor: editorState
}
});
}
}
} }