1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-22 09:00:10 +00:00

Add email list accessible by edit button in settings

This commit is contained in:
Harvey Tindall 2021-02-21 15:51:42 +00:00
parent 058cac2e7b
commit d1b1b90de3
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
11 changed files with 131 additions and 28 deletions

16
api.go
View File

@ -1276,13 +1276,13 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
// @tags Configuration // @tags Configuration
func (app *appContext) GetEmails(gc *gin.Context) { func (app *appContext) GetEmails(gc *gin.Context) {
gc.JSON(200, emailListDTO{ gc.JSON(200, emailListDTO{
"UserCreated": app.storage.lang.Email["en-us"].UserCreated["name"], "UserCreated": {Name: app.storage.lang.Email["en-us"].UserCreated["name"], Enabled: app.storage.customEmails.UserCreated.Enabled},
"InviteExpiry": app.storage.lang.Email["en-us"].InviteExpiry["name"], "InviteExpiry": {Name: app.storage.lang.Email["en-us"].InviteExpiry["name"], Enabled: app.storage.customEmails.InviteExpiry.Enabled},
"PasswordReset": app.storage.lang.Email["en-us"].PasswordReset["name"], "PasswordReset": {Name: app.storage.lang.Email["en-us"].PasswordReset["name"], Enabled: app.storage.customEmails.PasswordReset.Enabled},
"UserDeleted": app.storage.lang.Email["en-us"].UserDeleted["name"], "UserDeleted": {Name: app.storage.lang.Email["en-us"].UserDeleted["name"], Enabled: app.storage.customEmails.UserDeleted.Enabled},
"InviteEmail": app.storage.lang.Email["en-us"].InviteEmail["name"], "InviteEmail": {Name: app.storage.lang.Email["en-us"].InviteEmail["name"], Enabled: app.storage.customEmails.InviteEmail.Enabled},
"WelcomeEmail": app.storage.lang.Email["en-us"].WelcomeEmail["name"], "WelcomeEmail": {Name: app.storage.lang.Email["en-us"].WelcomeEmail["name"], Enabled: app.storage.customEmails.WelcomeEmail.Enabled},
"EmailConfirmation": app.storage.lang.Email["en-us"].EmailConfirmation["name"], "EmailConfirmation": {Name: app.storage.lang.Email["en-us"].EmailConfirmation["name"], Enabled: app.storage.customEmails.EmailConfirmation.Enabled},
}) })
} }
@ -1339,7 +1339,7 @@ func (app *appContext) SetEmail(gc *gin.Context) {
// @Success 200 {object} boolResponse // @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse // @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse // @Failure 500 {object} boolResponse
// @Router /config/emails/{id}/{enable/disable} [post] // @Router /config/emails/{id}/state/{enable/disable} [post]
// @tags Configuration // @tags Configuration
func (app *appContext) SetEmailState(gc *gin.Context) { func (app *appContext) SetEmailState(gc *gin.Context) {
id := gc.Param("id") id := gc.Param("id")

View File

@ -153,6 +153,11 @@ div.card:contains(section.banner.footer) {
margin: 0.5rem; margin: 0.5rem;
} }
.flex-col {
display: flex;
flex-direction: column;
}
@media screen and (max-width: 400px) { @media screen and (max-width: 400px) {
.row { .row {
flex-direction: column; flex-direction: column;
@ -273,6 +278,10 @@ sup.\~critical, .text-critical {
width: 100%; width: 100%;
} }
.flex-auto {
flex: auto;
}
.center { .center {
justify-content: center; justify-content: center;
} }

View File

@ -109,15 +109,33 @@
</div> </div>
</form> </form>
</div> </div>
<div id="modal-customize" class="modal">
<div class="modal-content card">
<span class="heading">{{ .strings.customizeEmails }} <span class="modal-close">&times;</span></span>
<p class="content">{{ .strings.customizeEmailsDescription }}</p>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>{{ .strings.name }}</th>
<th>{{ .strings.reset }}</th>
<th>{{ .strings.edit }}</th>
</tr>
</thead>
<tbody id="customize-list"></tbody>
</table>
</div>
</div>
</div>
<div id="modal-editor" class="modal"> <div id="modal-editor" class="modal">
<form class="modal-content wide card" id="form-editor" href=""> <form class="modal-content wide card" id="form-editor" href="">
<span class="heading"><span id="header-editor"></span> <span class="modal-close">&times;</span></span> <span class="heading"><span id="header-editor"></span> <span class="modal-close">&times;</span></span>
<div class="row"> <div class="row">
<div class="col content mt-half"> <div class="col flex-col content mt-half">
<span class="label supra" for="editor-variables">{{ .strings.variables }}</span> <span class="label supra" for="editor-variables">{{ .strings.variables }}</span>
<div id="editor-variables"></div> <div id="editor-variables"></div>
<label class="label supra" for="textarea-editor">{{ .strings.message }}</label> <label class="label supra" for="textarea-editor">{{ .strings.message }}</label>
<textarea id="textarea-editor" class="textarea full-width ~neutral !normal mt-half monospace"></textarea> <textarea id="textarea-editor" class="textarea full-width flex-auto ~neutral !normal mt-half monospace"></textarea>
<p class="support mt-half mb-1">{{ .strings.markdownSupported }}</p> <p class="support mt-half mb-1">{{ .strings.markdownSupported }}</p>
<div class="flex-row"> <div class="flex-row">
<label class="full-width ml-half"> <label class="full-width ml-half">

View File

@ -35,6 +35,10 @@
"message": "Message", "message": "Message",
"variables": "Variables", "variables": "Variables",
"preview": "Preview", "preview": "Preview",
"reset": "Reset",
"edit": "Edit",
"customizeEmails": "Customize Emails",
"customizeEmailsDescription": "If you don't want to use jfa-go's email templates, you can create your own using Markdown.",
"markdownSupported": "Markdown is supported.", "markdownSupported": "Markdown is supported.",
"modifySettings": "Modify Settings", "modifySettings": "Modify Settings",
"modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.", "modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.",

View File

@ -1,7 +1,9 @@
{{ .aUserWasCreated }} {{ .aUserWasCreated }}
{{ .nameString }}: {{ .name }} {{ .nameString }}: {{ .name }}
{{ .addressString }}: {{ .address }} {{ .addressString }}: {{ .address }}
{{ .timeString }}: {{ .time }} {{ .timeString }}: {{ .time }}
{{ .notificationNotice }} {{ .notificationNotice }}

View File

@ -1,4 +1,5 @@
{{ .yourAccountWasDeleted }} {{ .yourAccountWasDeleted }}
{{ .reasonString }}: {{ .reason }} {{ .reasonString }}: {{ .reason }}
{{ .message }} {{ .message }}

View File

@ -1,8 +1,11 @@
{{ .helloUser }} {{ .helloUser }}
{{ .someoneHasRequestedReset }} {{ .someoneHasRequestedReset }}
{{ .ifItWasYou }} {{ .ifItWasYou }}
{{ .codeExpiry }} {{ .codeExpiry }}
{{ .ifItWasNotYou }} {{ .ifItWasNotYou }}
{{ .pinString }}: {{ .pin }} {{ .pinString }}: {{ .pin }}

View File

@ -175,7 +175,12 @@ type settings struct {
type langDTO map[string]string type langDTO map[string]string
type emailListDTO map[string]string type emailListDTO map[string]emailListEl
type emailListEl struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
}
type emailSetDTO struct { type emailSetDTO struct {
Content string `json:"content"` Content string `json:"content"`

View File

@ -55,6 +55,8 @@ window.availableProfiles = window.availableProfiles || [];
window.modals.announce = new Modal(document.getElementById("modal-announce")); window.modals.announce = new Modal(document.getElementById("modal-announce"));
window.modals.editor = new Modal(document.getElementById("modal-editor")); window.modals.editor = new Modal(document.getElementById("modal-editor"));
window.modals.customizeEmails = new Modal(document.getElementById("modal-customize"));
})(); })();
var inviteCreator = new createInvite(); var inviteCreator = new createInvite();

View File

@ -479,14 +479,16 @@ export class settingsList {
private _sections: { [name: string]: sectionPanel } private _sections: { [name: string]: sectionPanel }
private _buttons: { [name: string]: HTMLSpanElement } private _buttons: { [name: string]: HTMLSpanElement }
private _needsRestart: boolean = false; private _needsRestart: boolean = false;
private _emailEditor = new EmailEditor();
addSection = (name: string, s: Section) => { addSection = (name: string, s: Section, subButton?: HTMLElement) => {
const section = new sectionPanel(s, name); const section = new sectionPanel(s, name);
this._sections[name] = section; this._sections[name] = section;
this._panel.appendChild(this._sections[name].asElement()); this._panel.appendChild(this._sections[name].asElement());
const button = document.createElement("span") as HTMLSpanElement; const button = document.createElement("span") as HTMLSpanElement;
button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half"); button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half");
button.textContent = s.meta.name; button.textContent = s.meta.name;
if (subButton) { button.appendChild(subButton); }
button.onclick = () => { this._showPanel(name); }; button.onclick = () => { this._showPanel(name); };
if (s.meta.depends_true || s.meta.depends_false) { if (s.meta.depends_true || s.meta.depends_false) {
let dependant = splitDependant(name, s.meta.depends_true || s.meta.depends_false); let dependant = splitDependant(name, s.meta.depends_true || s.meta.depends_false);
@ -581,7 +583,22 @@ export class settingsList {
if (name in this._sections) { if (name in this._sections) {
this._sections[name].update(settings.sections[name]); this._sections[name].update(settings.sections[name]);
} else { } else {
this.addSection(name, settings.sections[name]); if (name == "email") {
const editButton = document.createElement("div");
editButton.classList.add("tooltip", "left");
editButton.innerHTML = `
<span class="button ~neutral !normal">
<i class="icon ri-edit-line"></i>
</span>
<span class="content sm">
${window.lang.get("strings", "customizeEmails")}
</span>
`;
(editButton.querySelector("span.button") as HTMLSpanElement).onclick = this._emailEditor.showList;
this.addSection(name, settings.sections[name], editButton);
} else {
this.addSection(name, settings.sections[name]);
}
} }
} }
this._showPanel(settings.order[0]); this._showPanel(settings.order[0]);
@ -665,15 +682,22 @@ interface templateEmail {
variables: string[]; variables: string[];
} }
interface emailListEl {
name: string;
enabled: boolean;
}
class EmailEditor { class EmailEditor {
private _currentID: string; private _currentID: string;
private _names: { [id: string]: string }; private _names: { [id: string]: emailListEl };
private _content: string; private _content: string;
private _form = document.getElementById("form-editor") as HTMLFormElement; private _form = document.getElementById("form-editor") as HTMLFormElement;
private _header = document.getElementById("header-editor") as HTMLSpanElement; private _header = document.getElementById("header-editor") as HTMLSpanElement;
private _variables = document.getElementById("editor-variables") as HTMLDivElement; private _variables = document.getElementById("editor-variables") as HTMLDivElement;
private _textArea = document.getElementById("textarea-editor") as HTMLTextAreaElement; private _textArea = document.getElementById("textarea-editor") as HTMLTextAreaElement;
private _preview = document.getElementById("editor-preview") as HTMLDivElement; private _preview = document.getElementById("editor-preview") as HTMLDivElement;
private _timeout: number;
private _finishInterval = 1000;
insert = (textarea: HTMLTextAreaElement, text: string) => { // https://kubyshkin.name/posts/insert-text-into-textarea-at-cursor-position <3 insert = (textarea: HTMLTextAreaElement, text: string) => { // https://kubyshkin.name/posts/insert-text-into-textarea-at-cursor-position <3
const isSuccess = document.execCommand("insertText", false, text); const isSuccess = document.execCommand("insertText", false, text);
@ -693,9 +717,8 @@ class EmailEditor {
} }
} }
load = (id: string) => { loadEditor = (id: string) => {
this._currentID = id; this._currentID = id;
this.loadPreview();
_get("/config/emails/" + id, null, (req: XMLHttpRequest) => { _get("/config/emails/" + id, null, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status != 200) { if (req.status != 200) {
@ -703,10 +726,11 @@ class EmailEditor {
return; return;
} }
if (this._names[id] !== undefined) { if (this._names[id] !== undefined) {
this._header.textContent = this._names[id]; this._header.textContent = this._names[id].name;
} }
const templ = req.response as templateEmail; const templ = req.response as templateEmail;
this._textArea.value = templ.content; this._textArea.value = templ.content;
this.loadPreview();
this._content = templ.content; this._content = templ.content;
const colors = ["info", "urge", "positive", "neutral"]; const colors = ["info", "urge", "positive", "neutral"];
let innerHTML = ''; let innerHTML = '';
@ -718,7 +742,10 @@ class EmailEditor {
const buttons = this._variables.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>; const buttons = this._variables.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>;
for (let i = 0; i < templ.variables.length; i++) { for (let i = 0; i < templ.variables.length; i++) {
buttons[i].innerHTML = `<span class="monospace">` + templ.variables[i] + `</span>`; buttons[i].innerHTML = `<span class="monospace">` + templ.variables[i] + `</span>`;
buttons[i].onclick = () => this.insert(this._textArea, templ.variables[i]); buttons[i].onclick = () => {
this.insert(this._textArea, templ.variables[i]);
this._timeout = setTimeout(this.loadPreview, this._finishInterval);
}
} }
window.modals.editor.show(); window.modals.editor.show();
} }
@ -731,11 +758,12 @@ class EmailEditor {
window.notifications.customError("loadTemplateError", window.lang.notif("errorFailureCheckLogs")); window.notifications.customError("loadTemplateError", window.lang.notif("errorFailureCheckLogs"));
return; return;
} }
this._preview.innerHTML = req.response.html; this._preview.innerHTML = (req.response as Email).html;
} }
}, true); }, true);
} }
constructor() {
showList = () => {
_get("/config/emails", null, (req: XMLHttpRequest) => { _get("/config/emails", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status != 200) { if (req.status != 200) {
@ -743,17 +771,49 @@ class EmailEditor {
return; return;
} }
this._names = req.response; this._names = req.response;
const list = document.getElementById("customize-list") as HTMLDivElement;
list.textContent = '';
for (let id in this._names) {
const tr = document.createElement("tr") as HTMLTableRowElement;
let resetButton = ``;
if (this._names[id].enabled) {
resetButton = `<i class="icon ri-restart-line" title="${window.lang.get("strings", "reset")}"></i>`;
}
tr.innerHTML = `
<td>${this._names[id].name}</td>
<td>${resetButton}</td>
<td><span class="button ~info !normal" title="${window.lang.get("strings", "edit")}"><i class="icon ri-edit-line"></i></span></td>
`;
(tr.querySelector("span.button") as HTMLSpanElement).onclick = () => {
window.modals.customizeEmails.close()
this.loadEditor(id);
};
if (this._names[id].enabled) {
const rb = tr.querySelector("i.ri-restart-line") as HTMLElement;
rb.onclick = () => _post("/config/emails/" + id + "/state/disable", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200 && req.status != 204) {
window.notifications.customError("setEmailStateError", window.lang.notif("errorFailureCheckLogs"));
return;
}
rb.remove();
}
});
}
list.appendChild(tr);
}
window.modals.customizeEmails.show();
} }
}); });
}
let timeout: number;
const finishInterval = 1000; constructor() {
this._textArea.onkeyup = () => { this._textArea.onkeyup = () => {
clearTimeout(timeout); clearTimeout(this._timeout);
timeout = setTimeout(this.loadPreview, finishInterval); this._timeout = setTimeout(this.loadPreview, this._finishInterval);
}; };
this._textArea.onkeydown = () => { this._textArea.onkeydown = () => {
clearTimeout(timeout); clearTimeout(this._timeout);
}; };
this._form.onsubmit = (event: Event) => { this._form.onsubmit = (event: Event) => {
@ -775,5 +835,3 @@ class EmailEditor {
}; };
} }
} }
(window as any).ee = () => { (window as any).ee = new EmailEditor(); };

View File

@ -76,6 +76,7 @@ declare interface Modals {
addProfile: Modal; addProfile: Modal;
announce: Modal; announce: Modal;
editor: Modal; editor: Modal;
customizeEmails: Modal;
} }
interface Invite { interface Invite {