Files
pocketbase/ui/src/settings/application/pageApplicationSettings.js
T
2026-04-18 22:11:58 +03:00

294 lines
12 KiB
JavaScript

import { settingsSidebar } from "../settingsSidebar";
import { batchAccordion } from "./batchAccordion";
import { rateLimitAccordion, sortRules } from "./rateLimitAccordion";
import { trustedProxyAccordion } from "./trustedProxyAccordion";
export function pageApplicationSettings() {
app.store.title = "Application settings";
const data = store({
isLoading: false,
isSaving: false,
formSettings: null,
originalFormSettings: null,
get originalFormSettingsHash() {
return JSON.stringify(data.originalFormSettings);
},
get hasChanges() {
return data.originalFormSettingsHash != JSON.stringify(data.formSettings);
},
});
loadSettings();
async function loadSettings() {
data.isLoading = true;
try {
const settings = await app.pb.settings.getAll();
init(settings);
data.isLoading = false;
} catch (err) {
if (!err.isAbort) {
app.checkApiError(err);
// data.isLoading = false; don't reset in case of a server error
}
}
}
async function save() {
if (data.isSaving || !data.hasChanges) {
return;
}
data.isSaving = true;
data.formSettings.rateLimits.rules = sortRules(data.formSettings.rateLimits.rules);
try {
const redacted = app.utils.filterRedactedProps(data.formSettings);
const settings = await app.pb.settings.update(redacted);
init(settings);
app.toasts.success("Successfully saved application settings.");
} catch (err) {
app.checkApiError(err);
}
data.isSaving = false;
}
function init(settings = {}) {
// refresh local app settings
app.store.settings = JSON.parse(JSON.stringify(settings));
// load from the css style as fallback
if (!settings.meta?.accentColor) {
const cssColor = window.getComputedStyle(document.documentElement)?.getPropertyValue("--accentColor");
if (cssColor?.startsWith("#")) {
settings.meta = settings.meta || {};
settings.meta.accentColor = cssColor.toLowerCase() || "";
}
}
data.originalFormSettings = {
meta: settings.meta || {},
batch: settings.batch || {},
trustedProxy: settings.trustedProxy || { headers: [] },
rateLimits: settings.rateLimits || { rules: [] },
};
sortRules(data.originalFormSettings.rateLimits.rules);
data.formSettings = JSON.parse(JSON.stringify(data.originalFormSettings));
}
function reset() {
data.formSettings = JSON.parse(data.originalFormSettingsHash);
}
return t.div(
{
pbEvent: "pageApplicationSettings",
className: "page page-application-settings",
},
settingsSidebar(),
t.div(
{ className: "page-content full-height" },
t.header(
{ className: "page-header" },
t.nav(
{ className: "breadcrumbs" },
t.div({ className: "breadcrumb-item" }, "Settings"),
t.div({ className: "breadcrumb-item" }, "Application"),
),
),
t.div(
{ className: "wrapper m-b-base" },
() => {
if (data.isLoading) {
return t.div({ className: "block txt-center" }, t.span({ className: "loader lg" }));
}
return t.form(
{
pbEvent: "applicationSettingsForm",
className: "grid application-settings-form",
inert: () => data.isSaving,
onsubmit: (e) => {
e.preventDefault();
save();
},
},
t.div(
{ className: "col-md-5" },
t.div(
{ className: "field" },
t.label({ htmlFor: "meta.appName" }, "Application name"),
t.input({
id: "meta.appName",
name: "meta.appName",
type: "text",
required: true,
value: () => data.formSettings.meta.appName || "",
oninput: (e) => (data.formSettings.meta.appName = e.target.value),
}),
),
),
t.div(
{ className: "col-md-5" },
t.div(
{ className: "field" },
t.label({ htmlFor: "meta.appURL" }, "Application URL"),
t.input({
id: "meta.appURL",
name: "meta.appURL",
type: "url",
required: true,
value: () => data.formSettings.meta.appURL || "",
oninput: (e) => (data.formSettings.meta.appURL = e.target.value),
}),
),
),
t.div(
{ className: "col-md-2" },
// pass isSaving to ensure that it will be rerendered after save
() => accentColorField(data, data.isSaving),
),
t.div(
{ className: "col-lg-12" },
() => trustedProxyAccordion(data),
() => rateLimitAccordion(data),
() => batchAccordion(data),
),
t.div(
{ className: "col-lg-12" },
t.div(
{ className: "field" },
t.input({
id: "meta.hideControls",
name: "meta.hideControls",
type: "checkbox",
className: "switch",
checked: () => data.formSettings.meta.hideControls,
onchange: (e) => (data.formSettings.meta.hideControls = e.target.checked),
}),
t.label(
{ htmlFor: "meta.hideControls" },
t.span({ className: "txt" }, "Hide/Lock collection and record controls"),
t.i({
className: "ri-information-line link-hint",
ariaDescription: app.attrs.tooltip(
"To prevent accidental changes when in production environment, collections create and update buttons will be hidden.\nRecords update will also require an extra unlock step before save.",
),
}),
),
),
),
t.div({ className: "col-lg-12" }, t.hr()),
t.div(
{ className: "col-lg-12" },
t.div(
{ className: "flex" },
t.div({ className: "m-r-auto" }),
t.button(
{
type: "button",
className: "btn transparent secondary",
disabled: () => data.isSaving,
hidden: () => !data.hasChanges,
onclick: reset,
},
t.span({ className: "txt" }, "Cancel"),
),
t.button(
{
className: () => `btn expanded ${data.isSaving ? "loading" : ""}`,
disabled: () => !data.hasChanges || data.isSaving,
},
t.span({ className: "txt" }, "Save changes"),
),
),
),
);
},
),
t.footer({ className: "page-footer" }, app.components.credits()),
),
);
}
function accentColorField(pageData) {
const uniqueId = "accent_" + app.utils.randomString();
const local = store({
isTooLight: false,
});
let colorChangeTimeoutId;
let tempNoAnimationTimeoutId;
function changeAccentColor(color) {
// temporary disable animations to minimize flickering
clearTimeout(tempNoAnimationTimeoutId);
document.documentElement.style.setProperty("--animationSpeed", "0");
if (color) {
document.documentElement.style.setProperty("--accentColor", color.toLowerCase());
} else {
document.documentElement.style.removeProperty("--accentColor");
}
// restore animation
tempNoAnimationTimeoutId = setTimeout(() => {
document.documentElement.style.removeProperty("--animationSpeed");
}, 100);
}
const watchers = [
watch(() => pageData.formSettings?.meta?.accentColor, (newColor) => {
clearTimeout(colorChangeTimeoutId);
colorChangeTimeoutId = setTimeout(() => {
changeAccentColor(newColor);
}, 100);
}),
];
return t.div(
{
className: "field",
ariaDescription: app.attrs.tooltip(() => local.isTooLight ? "Invalid - color is too light" : ""),
onunmount: () => {
clearTimeout(colorChangeTimeoutId);
changeAccentColor(pageData.formSettings.meta.accentColor);
watchers.forEach((w) => w?.unwatch());
},
},
t.label(
{ htmlFor: uniqueId },
t.span({ className: "txt" }, "Accent"),
t.i({
hidden: () => !local.isTooLight,
className: "txt-warning ri-alert-line",
}),
),
app.components.colorPicker({
id: uniqueId,
name: "meta.accentColor",
predefinedColors: () => app.store.predefinedAccentColors,
value: () => pageData.formSettings.meta.accentColor,
onchange: (color) => {
// @todo consider removing the constraint once contrast-color is implemented
local.isTooLight = false;
if (!app.utils.isDarkEnoughForWhiteText(color)) {
local.isTooLight = true;
return;
}
pageData.formSettings.meta.accentColor = color;
},
}),
);
}