form: add more customizable success card

Success card could be customized simply with the "Success message"
setting, but a new "Post sign-up help card" in the Message editor
supports full markdown.
jellyseerr
Harvey Tindall 4 months ago
parent e44d11c58c
commit dabef831d7
No known key found for this signature in database
GPG Key ID: BBC65952848FB1A2

@ -39,6 +39,7 @@ func (app *appContext) GetCustomContent(gc *gin.Context) {
"UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.MustGetCustomContentKey("UserExpired").Enabled}, "UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.MustGetCustomContentKey("UserExpired").Enabled},
"UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.MustGetCustomContentKey("UserLogin").Enabled}, "UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.MustGetCustomContentKey("UserLogin").Enabled},
"UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.MustGetCustomContentKey("UserPage").Enabled}, "UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.MustGetCustomContentKey("UserPage").Enabled},
"PostSignupCard": {Name: app.storage.lang.Admin[adminLang].Strings["postSignupCard"], Enabled: app.storage.MustGetCustomContentKey("PostSignupCard").Enabled, Description: app.storage.lang.Admin[adminLang].Strings["postSignupCardDescription"]},
} }
filter := gc.Query("filter") filter := gc.Query("filter")
@ -145,7 +146,11 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
} else if id == "UserLogin" { } else if id == "UserLogin" {
variables = []string{} variables = []string{}
customMessage.Variables = variables customMessage.Variables = variables
} else if id == "PostSignupCard" {
variables = []string{"{username}", "{myAccountURL}"}
customMessage.Variables = variables
} }
content = customMessage.Content content = customMessage.Content
noContent := content == "" noContent := content == ""
if !noContent { if !noContent {
@ -210,14 +215,14 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
msg, err = app.email.constructUserExpired(app, true) msg, err = app.email.constructUserExpired(app, true)
} }
values = app.email.userExpiredValues(app, false) values = app.email.userExpiredValues(app, false)
case "UserLogin", "UserPage": case "UserLogin", "UserPage", "PostSignupCard":
values = map[string]interface{}{} values = map[string]interface{}{}
} }
if err != nil { if err != nil {
respondBool(500, false, gc) respondBool(500, false, gc)
return return
} }
if noContent && id != "Announcement" && id != "UserPage" && id != "UserLogin" { if noContent && id != "Announcement" && id != "UserPage" && id != "UserLogin" && id != "PostSignupCard" {
content = msg.Text content = msg.Text
variables = make([]string, strings.Count(content, "{")) variables = make([]string, strings.Count(content, "{"))
i := 0 i := 0
@ -243,17 +248,32 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
} }
app.storage.SetCustomContentKey(id, customMessage) app.storage.SetCustomContentKey(id, customMessage)
var mail *Message var mail *Message
if id != "UserLogin" && id != "UserPage" { if id != "UserLogin" && id != "UserPage" && id != "PostSignupCard" {
mail, err = app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app) mail, err = app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
if err != nil { if err != nil {
respondBool(500, false, gc) respondBool(500, false, gc)
return return
} }
} else if id == "PostSignupCard" {
// Jankiness follows.
// Source content from "Success Message" setting.
if noContent {
content = "# " + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.get("successHeader") + "\n" + app.config.Section("ui").Key("success_message").String()
if app.config.Section("user_page").Key("enabled").MustBool(false) {
content += "\n\n<br>\n" + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.template("userPageSuccessMessage", tmpl{
"myAccount": "[" + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.get("myAccount") + "]({myAccountURL})",
})
}
}
mail = &Message{
HTML: "<div class=\"card ~neutral dark:~d_neutral @low\"><div class=\"preview-content\"></div><br><button class=\"button ~urge dark:~d_urge @low full-width center supra submit\">" + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.get("continue") + "</a></div>",
}
mail.Markdown = mail.HTML
} else { } else {
mail = &Message{ mail = &Message{
HTML: "<div class=\"card ~neutral dark:~d_neutral @low preview-content\"></div>", HTML: "<div class=\"card ~neutral dark:~d_neutral @low preview-content\"></div>",
Markdown: "<div class=\"card ~neutral dark:~d_neutral @low preview-content\"></div>",
} }
mail.Markdown = mail.HTML
} }
gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Conditionals: conditionals, Values: values, HTML: mail.HTML, Plaintext: mail.Text}) gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Conditionals: conditionals, Values: values, HTML: mail.HTML, Plaintext: mail.Text})
} }

@ -247,7 +247,7 @@
"requires_restart": false, "requires_restart": false,
"type": "text", "type": "text",
"value": "Your account has been created. Click below to continue to Jellyfin.", "value": "Your account has been created. Click below to continue to Jellyfin.",
"description": "Displayed when a user creates an account" "description": "Displayed when a user creates an account. Use the \"post-signup card\" in the Message editor for more control."
}, },
"url_base": { "url_base": {
"name": "Reverse Proxy subfolder", "name": "Reverse Proxy subfolder",
@ -273,7 +273,7 @@
"type": "bool", "type": "bool",
"value": false, "value": false,
"advanced": true, "advanced": true,
"description": "Navigate directly to the above URL instead of needing the user to click \"Continue\"." "description": "Navigate directly to the above URL instead of needing the user to click \"Continue\". Overrides the post-signup card."
}, },
"login_appearance": { "login_appearance": {
"name": "Login screen appearance", "name": "Login screen appearance",
@ -702,7 +702,7 @@
"description": "Allow users to start a Password Reset by inputting their Discord/Telegram/Matrix username/id." "description": "Allow users to start a Password Reset by inputting their Discord/Telegram/Matrix username/id."
}, },
"pwr_note": { "pwr_note": {
"name": "PWR Methods", "name": "PWR Methods:",
"type": "note", "type": "note",
"depends_true": "enabled", "depends_true": "enabled",
"value": "", "value": "",

@ -297,6 +297,7 @@
<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 card ~neutral @low"> <div class="col card ~neutral @low">
<aside class="aside sm ~urge dark:~d_info mb-2 @low" id="aside-editor"></aside>
<span class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</span> <span class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</span>
<div id="editor-variables" class="mt-4"></div> <div id="editor-variables" class="mt-4"></div>
<span class="label supra" for="editor-conditionals" id="label-editor-conditionals">{{ .strings.conditionals }}</span> <span class="label supra" for="editor-conditionals" id="label-editor-conditionals">{{ .strings.conditionals }}</span>

@ -31,6 +31,11 @@
window.reCAPTCHASiteKey = "{{ .reCAPTCHASiteKey }}"; window.reCAPTCHASiteKey = "{{ .reCAPTCHASiteKey }}";
window.userPageEnabled = {{ .userPageEnabled }}; window.userPageEnabled = {{ .userPageEnabled }};
window.userPageAddress = "{{ .userPageAddress }}"; window.userPageAddress = "{{ .userPageAddress }}";
{{ if index . "customSuccessCard" }}
window.customSuccessCard = {{ .customSuccessCard }};
{{ else }}
window.customSuccessCard = false;
{{ end }}
</script> </script>
{{ if .passwordReset }} {{ if .passwordReset }}
<script src="js/pwr.js" type="module"></script> <script src="js/pwr.js" type="module"></script>

@ -14,12 +14,19 @@
</head> </head>
<body class="max-w-full overflow-x-hidden section"> <body class="max-w-full overflow-x-hidden section">
<div id="modal-success" class="modal"> <div id="modal-success" class="modal">
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3"> {{ if .customSuccessCard }}
<span class="heading mb-4">{{ if .passwordReset }}{{ .strings.passwordReset }}{{ else }}{{ .strings.successHeader }}{{ end }}</span> <div class="card @low dark:~d_neutral content break-words relative mx-auto my-[10%] w-4/5 lg:w-1/3">
<p class="content mb-4">{{ if .passwordReset }}{{ .strings.youCanLoginPassword }}{{ else }}{{ .successMessage }}{{ end }}</p> {{ .customSuccessCardContent }}
{{ if .userPageEnabled }}<p class="content mb-4" id="modal-success-user-page-area" my-account-term="{{ .strings.myAccount }}">{{ .strings.userPageSuccessMessage }}</p>{{ end }} <a class="button ~urge @low full-width center supra submit my-2" href="{{ .jfLink }}" id="create-success-button">{{ .strings.continue }}</a>
<a class="button ~urge @low full-width center supra submit" href="{{ .jfLink }}" id="create-success-button">{{ .strings.continue }}</a> </div>
</div> {{ else }}
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
<span class="heading mb-4">{{ if .passwordReset }}{{ .strings.passwordReset }}{{ else }}{{ .strings.successHeader }}{{ end }}</span>
<p class="content mb-4">{{ if .passwordReset }}{{ .strings.youCanLoginPassword }}{{ else }}{{ .successMessage }}{{ end }}</p>
{{ if .userPageEnabled }}<p class="content mb-4" id="modal-success-user-page-area" my-account-term="{{ .strings.myAccount }}">{{ .strings.userPageSuccessMessage }}</p>{{ end }}
<a class="button ~urge @low full-width center supra submit" href="{{ .jfLink }}" id="create-success-button">{{ .strings.continue }}</a>
</div>
{{ end }}
</div> </div>
<div id="modal-confirmation" class="modal"> <div id="modal-confirmation" class="modal">
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3"> <div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">

@ -137,6 +137,8 @@
"jellyfinID": "Jellyfin ID", "jellyfinID": "Jellyfin ID",
"userPageLogin": "User Page: Login", "userPageLogin": "User Page: Login",
"userPagePage": "User Page: Page", "userPagePage": "User Page: Page",
"postSignupCard": "Post-signup help card",
"postSignupCardDescription": "Card shown to user after signing up. Overrides \"Success Message\". Overriden by \"Auto redirect on success\" setting.",
"buildTime": "Build Time", "buildTime": "Build Time",
"builtBy": "Built By", "builtBy": "Built By",
"loginNotAdmin": "Not an Admin?", "loginNotAdmin": "Not an Admin?",

@ -339,7 +339,7 @@ func migrateToBadger(app *appContext) {
app.info.Println("All data migrated to database. JSON files in the config folder can be deleted if you are sure all data is correct in the app. Create an issue if you have problems.") app.info.Println("All data migrated to database. JSON files in the config folder can be deleted if you are sure all data is correct in the app. Create an issue if you have problems.")
} }
// Simply creates an emply CC template if not imn the DB already. // Simply creates an emply CC template if not in the DB already.
// Add new CC types here! // Add new CC types here!
func intialiseCustomContent(app *appContext) { func intialiseCustomContent(app *appContext) {
emptyCC := CustomContent{ emptyCC := CustomContent{
@ -384,6 +384,10 @@ func intialiseCustomContent(app *appContext) {
if _, ok := app.storage.GetCustomContentKey("UserExpiryAdjusted"); !ok { if _, ok := app.storage.GetCustomContentKey("UserExpiryAdjusted"); !ok {
app.storage.SetCustomContentKey("UserExpiryAdjusted", emptyCC) app.storage.SetCustomContentKey("UserExpiryAdjusted", emptyCC)
} }
if _, ok := app.storage.GetCustomContentKey("PostSignupCard"); !ok {
app.storage.SetCustomContentKey("PostSignupCard", emptyCC)
}
} }
// Migrate between hyphenated & non-hyphenated user IDs. Doesn't seem to happen anymore, so disabled. // Migrate between hyphenated & non-hyphenated user IDs. Doesn't seem to happen anymore, so disabled.

@ -239,8 +239,9 @@ type langDTO map[string]string
type emailListDTO map[string]emailListEl type emailListDTO map[string]emailListEl
type emailListEl struct { type emailListEl struct {
Name string `json:"name"` Name string `json:"name"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Description string `json:"description"`
} }
type emailSetDTO struct { type emailSetDTO struct {

@ -38,6 +38,7 @@ interface formWindow extends Window {
reCAPTCHASiteKey: string; reCAPTCHASiteKey: string;
userPageEnabled: boolean; userPageEnabled: boolean;
userPageAddress: string; userPageAddress: string;
customSuccessCard: boolean;
} }
loadLangSelector("form"); loadLangSelector("form");
@ -296,7 +297,10 @@ const create = (event: SubmitEvent) => {
const url = ((document.getElementById("modal-success") as HTMLDivElement).querySelector("a.submit") as HTMLAnchorElement).href; const url = ((document.getElementById("modal-success") as HTMLDivElement).querySelector("a.submit") as HTMLAnchorElement).href;
window.location.href = url; window.location.href = url;
} else { } else {
if (window.userPageEnabled) { if (window.customSuccessCard) {
const content = window.successModal.asElement().querySelector(".card");
content.innerHTML = content.innerHTML.replace(new RegExp("{username}", "g"), send.username)
} else if (window.userPageEnabled) {
const userPageNoticeArea = document.getElementById("modal-success-user-page-area"); const userPageNoticeArea = document.getElementById("modal-success-user-page-area");
const link = `<a href="${window.userPageAddress}" target="_blank">${userPageNoticeArea.getAttribute("my-account-term")}</a>`; const link = `<a href="${window.userPageAddress}" target="_blank">${userPageNoticeArea.getAttribute("my-account-term")}</a>`;
userPageNoticeArea.innerHTML = userPageNoticeArea.textContent.replace("{myAccount}", link); userPageNoticeArea.innerHTML = userPageNoticeArea.textContent.replace("{myAccount}", link);

@ -1083,6 +1083,7 @@ export interface templateEmail {
interface emailListEl { interface emailListEl {
name: string; name: string;
enabled: boolean; enabled: boolean;
description: string;
} }
class MessageEditor { class MessageEditor {
@ -1092,6 +1093,7 @@ class MessageEditor {
private _templ: templateEmail; private _templ: templateEmail;
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 _aside = document.getElementById("aside-editor") as HTMLElement;
private _variables = document.getElementById("editor-variables") as HTMLDivElement; private _variables = document.getElementById("editor-variables") as HTMLDivElement;
private _variablesLabel = document.getElementById("label-editor-variables") as HTMLElement; private _variablesLabel = document.getElementById("label-editor-variables") as HTMLElement;
private _conditionals = document.getElementById("editor-conditionals") as HTMLDivElement; private _conditionals = document.getElementById("editor-conditionals") as HTMLDivElement;
@ -1113,6 +1115,12 @@ class MessageEditor {
if (this._names[id] !== undefined) { if (this._names[id] !== undefined) {
this._header.textContent = this._names[id].name; this._header.textContent = this._names[id].name;
} }
this._aside.classList.add("unfocused");
if (this._names[id].description != "") {
this._aside.textContent = this._names[id].description;
this._aside.classList.remove("unfocused");
}
this._templ = req.response as templateEmail; this._templ = req.response as templateEmail;
this._textArea.value = this._templ.content; this._textArea.value = this._templ.content;
if (this._templ.html == "") { if (this._templ.html == "") {
@ -1212,11 +1220,22 @@ class MessageEditor {
if (this._names[id].enabled) { if (this._names[id].enabled) {
resetButton = `<i class="icon ri-restart-line" title="${window.lang.get("strings", "reset")}"></i>`; resetButton = `<i class="icon ri-restart-line" title="${window.lang.get("strings", "reset")}"></i>`;
} }
tr.innerHTML = ` let innerHTML = `
<td>${this._names[id].name}</td> <td>
${this._names[id].name}
`;
if (this._names[id].description != "") innerHTML += `
<div class="tooltip right">
<i class="icon ri-information-line"></i>
<span class="content sm">${this._names[id].description}</span>
</div>
`;
innerHTML += `
</td>
<td class="table-inline justify-center"><span class="customize-reset">${resetButton}</span></td> <td class="table-inline justify-center"><span class="customize-reset">${resetButton}</span></td>
<td><span class="button ~info @low" title="${window.lang.get("strings", "edit")}"><i class="icon ri-edit-line"></i></span></td> <td><span class="button ~info @low" title="${window.lang.get("strings", "edit")}"><i class="icon ri-edit-line"></i></span></td>
`; `;
tr.innerHTML = innerHTML;
(tr.querySelector("span.button") as HTMLSpanElement).onclick = () => { (tr.querySelector("span.button") as HTMLSpanElement).onclick = () => {
window.modals.customizeEmails.close() window.modals.customizeEmails.close()
this.loadEditor(id); this.loadEditor(id);

@ -271,13 +271,14 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
app.pushResources(gc, PWRPage) app.pushResources(gc, PWRPage)
lang := app.getLang(gc, PWRPage, app.storage.lang.chosenPWRLang) lang := app.getLang(gc, PWRPage, app.storage.lang.chosenPWRLang)
data := gin.H{ data := gin.H{
"urlBase": app.getURLBase(gc), "urlBase": app.getURLBase(gc),
"cssClass": app.cssClass, "cssClass": app.cssClass,
"cssVersion": cssVersion, "cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(), "contactMessage": app.config.Section("ui").Key("contact_message").String(),
"strings": app.storage.lang.PasswordReset[lang].Strings, "strings": app.storage.lang.PasswordReset[lang].Strings,
"success": false, "success": false,
"ombiEnabled": app.config.Section("ombi").Key("enabled").MustBool(false), "ombiEnabled": app.config.Section("ombi").Key("enabled").MustBool(false),
"customSuccessCard": false,
} }
pwr, isInternal := app.internalPWRs[pin] pwr, isInternal := app.internalPWRs[pin]
// if isInternal && setPassword { // if isInternal && setPassword {
@ -734,6 +735,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"), "userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"),
"langName": lang, "langName": lang,
"passwordReset": false, "passwordReset": false,
"customSuccessCard": false,
"telegramEnabled": telegram, "telegramEnabled": telegram,
"discordEnabled": discord, "discordEnabled": discord,
"matrixEnabled": matrix, "matrixEnabled": matrix,
@ -766,6 +768,22 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
data["discordServerName"] = app.discord.serverName data["discordServerName"] = app.discord.serverName
data["discordInviteLink"] = app.discord.inviteChannelName != "" data["discordInviteLink"] = app.discord.inviteChannelName != ""
} }
if msg, ok := app.storage.GetCustomContentKey("PostSignupCard"); ok && msg.Enabled {
data["customSuccessCard"] = true
// We don't template here, since the username is only known after login.
data["customSuccessCardContent"] = template.HTML(markdown.ToHTML(
[]byte(templateEmail(
msg.Content,
msg.Variables,
msg.Conditionals,
map[string]interface{}{
"username": "{username}",
"myAccountURL": userPageAddress,
},
),
), nil, markdownRenderer,
))
}
// if discordEnabled { // if discordEnabled {
// pin := "" // pin := ""

Loading…
Cancel
Save