mirror of
https://github.com/hrfee/jfa-go.git
synced 2026-01-18 16:47:42 +01:00
settings/config: add root order and use on web, fix nesting and
animation added an optional root "Order" field to the config. scripts/ini will warn if you've used this and forgot to include any sections. added more/most sections to a group now. groups have their maxHeight set to 9999px once animation finishes, and have it quickly set back to ~scrollHeight before they're animated closed.
This commit is contained in:
@@ -64,6 +64,9 @@ type Group struct {
|
||||
type Config struct {
|
||||
Sections []Section `json:"sections" yaml:"sections"`
|
||||
Groups []Group `json:"groups" yaml:"groups"`
|
||||
// Optional order, which can interleave sections and groups.
|
||||
// If unset, falls back to sections in order, then groups in order.
|
||||
Order []Member `json:"order,omitempty" yaml:"order,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Config) removeSection(section string) {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
order:
|
||||
- section: ui
|
||||
- section: advanced
|
||||
- section: jellyfin
|
||||
- group: sign_up
|
||||
- group: accounts
|
||||
- section: messages
|
||||
- group: external_services
|
||||
- section: activity_log
|
||||
- section: backups
|
||||
- section: updates
|
||||
- section: url_paths
|
||||
- section: template_email
|
||||
- section: files
|
||||
groups:
|
||||
- group: external_services
|
||||
name: "Integrations"
|
||||
@@ -7,6 +21,7 @@ groups:
|
||||
- group: chatbots
|
||||
- section: ombi
|
||||
- section: jellyseerr
|
||||
- section: webhooks
|
||||
- group: email
|
||||
name: "Email"
|
||||
description: "Options for sending emails through jfa-go."
|
||||
@@ -14,6 +29,7 @@ groups:
|
||||
- section: email
|
||||
- section: smtp
|
||||
- section: mailgun
|
||||
- section: email_confirmation
|
||||
- group: chatbots
|
||||
name: "Chat bots"
|
||||
description: "Options for messaging through chat services."
|
||||
@@ -21,6 +37,24 @@ groups:
|
||||
- section: discord
|
||||
- section: telegram
|
||||
- section: matrix
|
||||
- group: sign_up
|
||||
name: "Invites & Referrals"
|
||||
description: "Settings relating to invites, the sign up page and referrals."
|
||||
members:
|
||||
- section: captcha
|
||||
- section: password_validation
|
||||
- section: invite_emails
|
||||
- section: notifications
|
||||
- section: welcome_email
|
||||
- group: accounts
|
||||
name: "Accounts"
|
||||
description: "Settings relating to account management."
|
||||
members:
|
||||
- section: user_page
|
||||
- section: password_resets
|
||||
- section: user_expiry
|
||||
- section: disable_enable
|
||||
- section: deletion
|
||||
sections:
|
||||
- section: updates
|
||||
meta:
|
||||
@@ -1259,7 +1293,7 @@ sections:
|
||||
description: Path to custom email text template for announcements/custom messages.
|
||||
- section: notifications
|
||||
meta:
|
||||
name: Admin invite notifications
|
||||
name: Admin notifications
|
||||
description: Allows toggling "user created" and "invite expired" notifications
|
||||
to be sent to the admin per-invite.
|
||||
depends_true: messages|enabled
|
||||
@@ -1471,7 +1505,7 @@ sections:
|
||||
description: Path to custom email in plain text
|
||||
- section: user_expiry
|
||||
meta:
|
||||
name: User Expiry
|
||||
name: Account Expiry
|
||||
description: When set on an invite, users will be deleted or disabled a specified
|
||||
amount of time after they create their account. Expiries can also be set and
|
||||
extended for invididual users, optionally with a message why.
|
||||
|
||||
@@ -916,7 +916,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-3">
|
||||
<div class="md:card @low dark:~d_neutral flex md:flex flex-col gap-2 flex-1" id="settings-sidebar">
|
||||
<div class="@low dark:~d_neutral flex md:flex flex-col gap-2" id="settings-sidebar">
|
||||
<div class="flex flex-row justify-between">
|
||||
<input type="search" class="field ~neutral @low input settings-section-button justify-between" id="settings-search" placeholder="{{ .strings.search }}">
|
||||
<button class="button ~neutral @low center -ml-10 rounded-s-none settings-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></button>
|
||||
@@ -927,8 +927,9 @@
|
||||
<a class="button ~urge dark:~d_info @low justify-center grow" target="_blank" href="https://wiki.jfa-go.com"><span class="flex">{{ .strings.wiki }} <i class="ri-book-shelf-line ml-2"></i></a>
|
||||
<span class="button ~neutral @low justify-center grow" id="setting-profiles"><span class="flex">{{ .strings.userProfiles }} <i class="ri-user-line ml-2"></i></span></span>
|
||||
</div>
|
||||
<div class="flex md:flex flex-col gap-2 overflow-y-scroll" id="settings-sidebar-items"></div>
|
||||
</div>
|
||||
<div class="card ~neutral @low overflow flex-1" id="settings-panel">
|
||||
<div class="card ~neutral @low overflow flex-1 grow" id="settings-panel">
|
||||
<div class="settings-section unfocused h-[100%]" id="settings-not-found">
|
||||
<div class="flex flex-col h-[100%] justify-center items-center">
|
||||
<span class="text-2xl font-medium italic mb-2">{{ .strings.noResultsFound }}</span>
|
||||
|
||||
@@ -2,11 +2,15 @@ module github.com/hrfee/jfa-go/scripts/ini
|
||||
|
||||
replace github.com/hrfee/jfa-go/common => ../../common
|
||||
|
||||
go 1.18
|
||||
go 1.22.4
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/hrfee/jfa-go/common v0.0.0-20240824141650-fcdd4e451882 // indirect
|
||||
github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a h1:qbXZgCqb9eaPSJfLEXczQD2lxTv6jb6silMPIWW9j6o=
|
||||
github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a/go.mod h1:c5HKkLayo0GrEUDlJwT12b67BL9cdPjP271Xlv/KDRQ=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
"gopkg.in/ini.v1"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -26,6 +27,49 @@ func generateIni(yamlPath string, iniPath string) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Validate that all groups/sections are listed in the root order, if it exists
|
||||
if len(configBase.Order) > 0 {
|
||||
// Expand order
|
||||
var traverseGroup func(groupName string) []string
|
||||
traverseGroup = func(groupName string) []string {
|
||||
out := []string{}
|
||||
for _, group := range configBase.Groups {
|
||||
if group.Group == groupName {
|
||||
for _, groupMember := range group.Members {
|
||||
if groupMember.Group != "" {
|
||||
out = append(out, traverseGroup(groupMember.Group)...)
|
||||
} else if groupMember.Section != "" {
|
||||
out = append(out, groupMember.Section)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
listedSects := map[string]bool{}
|
||||
for _, member := range configBase.Order {
|
||||
if member.Group != "" {
|
||||
for _, sect := range traverseGroup(member.Group) {
|
||||
listedSects[sect] = true
|
||||
}
|
||||
} else if member.Section != "" {
|
||||
listedSects[member.Section] = true
|
||||
}
|
||||
}
|
||||
|
||||
missingSections := false
|
||||
for _, section := range configBase.Sections {
|
||||
if _, ok := listedSects[section.Section]; !ok {
|
||||
if !missingSections {
|
||||
color.Red("WARNING: Root order specified but the following sections were not listed, directly or indirectly:")
|
||||
missingSections = true
|
||||
}
|
||||
color.Red("\t%s", section.Section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conf := ini.Empty()
|
||||
|
||||
for _, section := range configBase.Sections {
|
||||
|
||||
@@ -476,7 +476,7 @@ interface Group {
|
||||
group: string;
|
||||
name: string;
|
||||
description: string;
|
||||
members: ({ group: string } | { section: string })[];
|
||||
members: Member[];
|
||||
}
|
||||
|
||||
interface Section {
|
||||
@@ -573,9 +573,12 @@ class sectionPanel {
|
||||
asElement = (): HTMLDivElement => { return this._section; }
|
||||
}
|
||||
|
||||
type Member = { group: string } | { section: string };
|
||||
|
||||
interface Settings {
|
||||
groups: Group[];
|
||||
sections: Section[];
|
||||
order?: Member[];
|
||||
}
|
||||
|
||||
export class settingsList {
|
||||
@@ -586,7 +589,7 @@ export class settingsList {
|
||||
private _loader = document.getElementById("settings-loader") as HTMLDivElement;
|
||||
|
||||
private _panel = document.getElementById("settings-panel") as HTMLDivElement;
|
||||
private _sidebar = document.getElementById("settings-sidebar") as HTMLDivElement;
|
||||
private _sidebar = document.getElementById("settings-sidebar-items") as HTMLDivElement;
|
||||
private _visibleSection: string;
|
||||
private _sections: { [name: string]: sectionPanel };
|
||||
private _buttons: { [name: string]: HTMLSpanElement };
|
||||
@@ -611,7 +614,7 @@ export class settingsList {
|
||||
// Takes all groups at once since members might contain each other.
|
||||
addGroups = (groups: Group[]) => {
|
||||
groups.forEach((g) => { this._groups[g.group] = g });
|
||||
const addGroup = (g: Group): HTMLElement => {
|
||||
const addGroup = (g: Group, indent: number = 0): HTMLElement => {
|
||||
if (g.group in this._groupButtons) return null;
|
||||
|
||||
const container = document.createElement("div") as HTMLDivElement;
|
||||
@@ -627,38 +630,70 @@ export class settingsList {
|
||||
<input class="unfocused" type="checkbox">
|
||||
</label>
|
||||
`;
|
||||
|
||||
const dropdown = document.createElement("div") as HTMLDivElement;
|
||||
container.appendChild(dropdown);
|
||||
dropdown.classList.add("ml-" + ((indent+1)*2));
|
||||
dropdown.style.maxHeight = "0";
|
||||
dropdown.style.opacity = "0";
|
||||
dropdown.classList.add("settings-dropdown", "unfocused", "flex", "flex-col", "gap-2", "transition-all");
|
||||
|
||||
const icon = button.querySelector("i.icon");
|
||||
const check = button.querySelector("input[type=checkbox]") as HTMLInputElement;
|
||||
|
||||
|
||||
button.onclick = () => {
|
||||
check.checked = !check.checked;
|
||||
onCheck();
|
||||
};
|
||||
// When groups are nested, the outer group's scrollHeight will obviously change when an
|
||||
// inner group is opened/closed. Instead of traversing the tree and adjusting the maxHeight property
|
||||
// each open/close, just set the maxHeight to 9999px once the animation is completed.
|
||||
// On close, quickly set maxHeight back to ~scrollHeight, then animate to 0.
|
||||
const onCheck = () => {
|
||||
if (check.checked) {
|
||||
icon.classList.add("rotated");
|
||||
// Hide the scrollbar while we animate
|
||||
this._sidebar.style.overflowY = "hidden";
|
||||
dropdown.classList.remove("unfocused");
|
||||
dropdown.style.maxHeight = dropdown.scrollHeight+"px";
|
||||
const fullHeight = () => {
|
||||
dropdown.removeEventListener("transitionend", fullHeight);
|
||||
dropdown.style.maxHeight = "9999px";
|
||||
// Return the scrollbar (or whatever, just don't hide it)
|
||||
this._sidebar.style.overflowY = "";
|
||||
};
|
||||
dropdown.addEventListener("transitionend", fullHeight);
|
||||
dropdown.style.maxHeight = (1.2*dropdown.scrollHeight)+"px";
|
||||
dropdown.style.opacity = "100%";
|
||||
} else {
|
||||
icon.classList.remove("rotated");
|
||||
const hide = () => {
|
||||
const mainTransitionEnd = () => {
|
||||
dropdown.removeEventListener("transitionend", mainTransitionEnd);
|
||||
dropdown.classList.add("unfocused");
|
||||
dropdown.removeEventListener("transitionend", hide);
|
||||
// Return the scrollbar (or whatever, just don't hide it)
|
||||
this._sidebar.style.overflowY = "";
|
||||
};
|
||||
dropdown.addEventListener("transitionend", hide);
|
||||
dropdown.style.maxHeight = "0";
|
||||
dropdown.style.opacity = "0";
|
||||
const mainTransitionStart = () => {
|
||||
dropdown.removeEventListener("transitionend", mainTransitionStart)
|
||||
dropdown.style.transitionDuration = "";
|
||||
dropdown.addEventListener("transitionend", mainTransitionEnd);
|
||||
dropdown.style.maxHeight = "0";
|
||||
dropdown.style.opacity = "0";
|
||||
}
|
||||
// Hide the scrollbar while we animate
|
||||
this._sidebar.style.overflowY = "hidden";
|
||||
// Disabling transitions then going from 9999 - scrollHeight doesn't work in firefox to me,
|
||||
// so instead just make the transition duration really short.
|
||||
dropdown.style.transitionDuration = "1ms";
|
||||
dropdown.addEventListener("transitionend", mainTransitionStart);
|
||||
dropdown.style.maxHeight = (1.2*dropdown.scrollHeight)+"px";
|
||||
}
|
||||
}
|
||||
|
||||
const dropdown = document.createElement("div") as HTMLDivElement;
|
||||
container.appendChild(dropdown);
|
||||
dropdown.style.maxHeight = "0";
|
||||
dropdown.style.opacity = "0";
|
||||
dropdown.classList.add("unfocused", "flex", "flex-col", "gap-2", "flex-1", "max-h-0", "transition-all");
|
||||
check.onchange = onCheck;
|
||||
|
||||
for (const member of g.members) {
|
||||
if ("group" in member) {
|
||||
let subgroup = addGroup(this._groups[member.group]);
|
||||
let subgroup = addGroup(this._groups[member.group], indent+1);
|
||||
if (!subgroup) {
|
||||
subgroup = this._groupButtons[member.group];
|
||||
// Remove from page
|
||||
@@ -727,6 +762,21 @@ export class settingsList {
|
||||
this._sidebar.appendChild(this._buttons[name]);
|
||||
}
|
||||
|
||||
setOrder(order: Member[]) {
|
||||
this._sidebar.textContent = ``;
|
||||
for (const member of order) {
|
||||
if ("group" in member) {
|
||||
this._sidebar.appendChild(this._groupButtons[member.group]);
|
||||
} else if ("section" in member) {
|
||||
if (member.section in this._buttons) {
|
||||
this._sidebar.appendChild(this._buttons[member.section]);
|
||||
} else {
|
||||
console.warn("Settings section specified in order but missing:", member.section);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _showPanel = (name: string) => {
|
||||
// console.log("showing", name);
|
||||
for (let n in this._sections) {
|
||||
@@ -1014,6 +1064,8 @@ export class settingsList {
|
||||
|
||||
this.addGroups(this._settings.groups);
|
||||
|
||||
if ("order" in this._settings && this._settings.order) this.setOrder(this._settings.order);
|
||||
|
||||
removeLoader(this._loader);
|
||||
for (let i = 0; i < this._loader.children.length; i++) {
|
||||
this._loader.children[i].classList.remove("invisible");
|
||||
|
||||
Reference in New Issue
Block a user