Files
apps/settings/SettingsRolesSection.js
metacryst 0d6c7683ff init
2026-04-28 20:05:00 -05:00

251 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class SettingsRolesSection extends Shadow {
newRoleName = ""
confirmDeleteId = null
constructor(roles, allApps, activeSection, activeRoleId, roleApps, basePath, callbacks, loading = false) {
super()
this.roles = roles
this.allApps = allApps
this.activeSection = activeSection // "roles" | "role-detail"
this.activeRoleId = activeRoleId
this.roleApps = roleApps
this.basePath = basePath
this.loading = loading
this.onDeleteRole = callbacks.onDeleteRole
this.onCreateRole = callbacks.onCreateRole
this.onToggleApp = callbacks.onToggleApp
}
get activeRole() {
return this.roles.find(r => String(r.id) === String(this.activeRoleId)) ?? null
}
get activeRoleAppIds() {
return this.roleApps[this.activeRoleId] ?? new Set()
}
render() {
if (this.activeSection === "role-detail") {
this.renderRoleDetail()
} else {
this.renderRolesList()
}
}
renderRolesList() {
VStack(() => {
this.backHeader("Roles & Apps", () => window.navigateTo(this.basePath))
VStack(() => {}).height(1, px).background("var(--divider)").flexShrink(0)
if (this.loading) { LoadingCircle(); return }
VStack(() => {
this.roles.forEach((role, i) => {
const appCount = (this.roleApps[role.id] ?? new Set()).size
const isConfirming = this.confirmDeleteId === role.id
HStack(() => {
VStack(() => {
HStack(() => {
p(role.name)
.margin(0).fontSize(0.92, em).fontWeight("600").color("var(--headertext)")
if (role.is_default) {
p("default")
.margin(0).fontSize(0.62, em).fontWeight("600")
.color("var(--quillred)")
.background("rgba(159,28,41,0.1)")
.paddingHorizontal(0.45, em).paddingVertical(0.1, em)
.borderRadius(100, px)
}
})
.gap(0.45, em).alignItems("center")
p(`${appCount} app${appCount !== 1 ? "s" : ""} assigned`)
.margin(0).marginTop(0.15, em).fontSize(0.72, em)
.color("var(--headertext)").opacity(0.4)
})
.flex(1).gap(0)
if (isConfirming) {
HStack(() => {
button("Delete")
.padding("0.25em 0.6em")
.border("none").borderRadius(0.35, em)
.background("var(--quillred)").color("white")
.fontSize(0.72, em).fontWeight("600").cursor("pointer")
.onTap(async (e) => {
e.stopPropagation()
await this.onDeleteRole(role.id)
})
button("✕")
.padding("0.25em 0.45em")
.border("none").borderRadius(0.35, em)
.background("transparent").color("var(--headertext)")
.opacity(0.4).fontSize(0.78, em).cursor("pointer")
.onTap((e) => {
e.stopPropagation()
this.confirmDeleteId = null
this.rerender()
})
})
.gap(0.35, em).alignItems("center")
} else {
HStack(() => {
button("⋯")
.border("none").background("transparent")
.color("var(--headertext)").opacity(0.35)
.fontSize(1.1, em).cursor("pointer").padding("0.1em 0.4em")
.onTap((e) => {
e.stopPropagation()
this.confirmDeleteId = role.id
this.rerender()
})
p("").margin(0).fontSize(1.1, em).color("var(--headertext)").opacity(0.3)
})
.gap(0.1, em).alignItems("center")
}
})
.gap(0.75, em).alignItems("center")
.paddingVertical(0.85, em)
.cursor("pointer")
.onTap(() => window.navigateTo(`${this.basePath}/roles/${role.id}`))
if (i < this.roles.length - 1) {
VStack(() => {}).height(1, px).background("var(--divider)").marginLeft(0)
}
})
// New role input
HStack(() => {
p("+").margin(0).fontSize(1.1, em).color("var(--headertext)").opacity(0.35).flexShrink(0)
input("New role name…", "100%")
.border("none").background("transparent")
.color("var(--headertext)").fontSize(0.88, em).outline("none").flex(1)
.attr({ value: this.newRoleName })
.onInput(e => { this.newRoleName = e.target.value })
.onKeyDown(async (e) => {
if (e.key === "Enter" && this.newRoleName.trim()) {
await this.onCreateRole(this.newRoleName.trim())
this.newRoleName = ""
}
})
})
.gap(0.65, em).alignItems("center")
.paddingVertical(0.85, em)
.borderTop("1px dashed var(--divider)")
.marginTop(0.1, em)
})
.paddingHorizontal(1, em)
.flex(1).overflowY("auto")
})
.height(100, pct).overflow("hidden")
}
renderRoleDetail() {
VStack(() => {
this.backHeader(this.activeRole?.name ?? "Role", () => window.navigateTo(`${this.basePath}/roles`))
VStack(() => {}).height(1, px).background("var(--divider)").flexShrink(0)
if (this.loading) { LoadingCircle(); return }
VStack(() => {
VStack(() => {
p("APP ACCESS")
.margin(0).marginBottom(0.55, em)
.fontSize(0.62, em).fontWeight("700").letterSpacing("0.07em")
.color("var(--headertext)").opacity(0.35)
this.allApps.forEach((app, i) => {
const hasApp = this.activeRoleAppIds.has(app.id)
const comingSoonApps = ["jobs", "politics", "files"]
const isComingSoon = comingSoonApps.includes(app.name)
HStack(() => {
p(app.name.charAt(0).toUpperCase() + app.name.slice(1))
.margin(0).fontSize(0.9, em).color("var(--headertext)").flex(1)
HStack(() => {
if (isComingSoon) {
p("Coming soon")
.margin(0).fontSize(0.9, em).color("var(--headertext)").opacity(0.45)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.paddingHorizontal(0.5, em).paddingVertical(0.15, em).borderRadius(100, px)
}
VStack(() => {})
.width(1.15, em).height(1.15, em)
.borderRadius(0.25, em)
.background(hasApp ? "var(--quillred)" : "transparent")
.border(`2px solid ${hasApp ? "var(--quillred)" : "var(--divider)"}`)
.boxSizing("border-box")
.justifyContent("center").alignItems("center")
})
.justifyContent("center").alignItems("center")
.gap(1, em)
})
.paddingVertical(0.8, em)
.gap(1, em).alignItems("center")
.cursor(isComingSoon ? "default" : "pointer")
.opacity(isComingSoon ? 0.5 : 1)
.onTap(() => { if (!isComingSoon) this.onToggleApp(this.activeRoleId, app.id, !hasApp) })
if (i < this.allApps.length - 1) {
VStack(() => {}).height(1, px).background("var(--divider)")
}
})
})
.paddingHorizontal(1, em)
.paddingVertical(0.75, em)
.marginBottom(1, em)
VStack(() => {}).height(1, px).background("var(--divider)").marginHorizontal(1, em)
VStack(() => {
this.comingSoonRow("🔐", "Permissions")
VStack(() => {}).height(1, px).background("var(--divider)")
this.comingSoonRow("🔔", "Notifications")
VStack(() => {}).height(1, px).background("var(--divider)")
this.comingSoonRow("🎨", "Color & Badge")
})
.paddingHorizontal(1, em)
.marginTop(1, em)
.opacity(0.4)
})
.flex(1).overflowY("auto").paddingBottom(2, em)
})
.height(100, pct).overflow("hidden")
}
backHeader(title, onBack) {
HStack(() => {
button("")
.border("none").background("transparent")
.color("var(--headertext)").fontSize(1.4, em)
.lineHeight("1").paddingVertical(0.2, em).paddingRight(0.3, em)
.cursor("pointer").flexShrink(0)
.onTap(onBack)
p(title)
.margin(0).fontSize(1.05, em).fontWeight("700").color("var(--headertext)")
.flex(1).overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
})
.gap(0.25, em).alignItems("center")
.paddingHorizontal(0.85, em).paddingTop(1.25, em).paddingBottom(0.85, em)
.flexShrink(0)
}
comingSoonRow(icon, label) {
HStack(() => {
p(icon).margin(0).fontSize(1, em).flexShrink(0)
p(label).margin(0).fontSize(0.9, em).color("var(--headertext)").flex(1)
p("Coming soon")
.margin(0).fontSize(0.9, em).color("var(--headertext)").opacity(0.45)
.background("var(--darkaccent)").border("1px solid var(--divider)")
.paddingHorizontal(0.5, em).paddingVertical(0.15, em).borderRadius(100, px)
})
.gap(0.75, em).alignItems("center").paddingVertical(0.85, em)
}
}
register(SettingsRolesSection)