453 lines
14 KiB
JavaScript
453 lines
14 KiB
JavaScript
const notifyChannel = new BroadcastChannel("tabsSync");
|
|
|
|
const SETTINGS_STORAGE_KEY = "pbSettings";
|
|
const COLOR_SCHEME_STORAGE_KEY = "pbColorScheme";
|
|
|
|
window.app = window.app || {};
|
|
window.app.store = store({
|
|
// flag used to track when the internal bootstrap process is done
|
|
_ready: false,
|
|
|
|
// the current authenticated superuser
|
|
superuser: null,
|
|
|
|
// used to force hiding the header even when authenticated
|
|
showHeader: true,
|
|
|
|
page: t.div({ className: "page" }, () => {
|
|
if (!app.store._ready) {
|
|
return t.span({ className: "loader lg m-auto", title: "Loading plugins..." });
|
|
}
|
|
}),
|
|
|
|
mainLogo: import.meta.env.BASE_URL + "images/logo.svg",
|
|
headerLogo: import.meta.env.BASE_URL + "images/logo_white.svg",
|
|
favicon: "", // leave empty to fallback to the default one
|
|
|
|
title: "",
|
|
|
|
_mediaColorScheme: "",
|
|
userColorScheme: window.localStorage.getItem(COLOR_SCHEME_STORAGE_KEY) || "",
|
|
get activeColorScheme() {
|
|
// explicitly set
|
|
if (app.store.userColorScheme) {
|
|
return app.store.userColorScheme;
|
|
}
|
|
|
|
// fallback to the loaded browser preference
|
|
return app.store._mediaColorScheme || "light";
|
|
},
|
|
|
|
// api response errors
|
|
errors: null,
|
|
|
|
creditLinks: [
|
|
{
|
|
// optional: isActive
|
|
href: import.meta.env.PB_DOCS_URL,
|
|
icon: "ri-book-open-line",
|
|
label: "Docs",
|
|
},
|
|
{
|
|
href: import.meta.env.PB_RELEASES,
|
|
icon: "ri-github-line",
|
|
label: `PocketBase ${import.meta.env.PB_VERSION}`,
|
|
},
|
|
],
|
|
|
|
headerLinks: [
|
|
{
|
|
// optional: isActive
|
|
href: "#/collections",
|
|
icon: "ri-database-2-line",
|
|
label: "Collections",
|
|
},
|
|
{
|
|
href: "#/logs",
|
|
icon: "ri-bar-chart-box-line",
|
|
label: "Logs",
|
|
},
|
|
{
|
|
href: "#/settings",
|
|
icon: "ri-settings-3-line",
|
|
label: "Settings",
|
|
},
|
|
],
|
|
|
|
settingsNavGroups: {
|
|
System: [
|
|
{
|
|
// optional: isActive
|
|
href: "#/settings",
|
|
icon: "ri-home-gear-line",
|
|
label: "Application",
|
|
},
|
|
{
|
|
href: "#/settings/mail",
|
|
icon: "ri-send-plane-2-line",
|
|
label: "Mail settings",
|
|
},
|
|
{
|
|
href: "#/settings/storage",
|
|
icon: "ri-archive-drawer-line",
|
|
label: "Files storage",
|
|
},
|
|
{
|
|
href: "#/settings/backups",
|
|
icon: "ri-archive-line",
|
|
label: "Backups",
|
|
},
|
|
{
|
|
href: "#/settings/crons",
|
|
icon: "ri-time-line",
|
|
label: "Crons",
|
|
},
|
|
],
|
|
Sync: [
|
|
{
|
|
href: "#/settings/export-collections",
|
|
icon: "ri-uninstall-line",
|
|
label: "Export collections",
|
|
},
|
|
{
|
|
href: "#/settings/import-collections",
|
|
icon: "ri-install-line",
|
|
label: "Import collections",
|
|
},
|
|
],
|
|
},
|
|
|
|
predefinedAccentColors: [
|
|
"#1055c9",
|
|
"#a3142a",
|
|
"#096d5c",
|
|
"#e6620a",
|
|
"#007d9c",
|
|
"#3f3da9",
|
|
],
|
|
|
|
settings: app.utils.getLocalHistory(SETTINGS_STORAGE_KEY, {}),
|
|
isLoadingSettings: false,
|
|
async loadSettings() {
|
|
app.store.isLoadingSettings = true;
|
|
|
|
try {
|
|
const settings = await app.pb.settings.getAll({ requestKey: "appStore.loadSettings" });
|
|
|
|
app.store.settings = settings;
|
|
app.store.isLoadingSettings = false;
|
|
} catch (err) {
|
|
if (!err.isAbort) {
|
|
app.store.isLoadingSettings = false;
|
|
app.checkApiError(err);
|
|
}
|
|
}
|
|
},
|
|
|
|
collections: [],
|
|
collectionScaffolds: {},
|
|
isLoadingCollections: false,
|
|
_activeCollectionIdOrName: "",
|
|
get activeCollection() {
|
|
const idOrName = app.store._activeCollectionIdOrName;
|
|
return app.store.collections.find((c) => c.id == idOrName || c.name == idOrName) || app.store.collections[0];
|
|
},
|
|
set activeCollection(collection) {
|
|
if (typeof collection == "string") {
|
|
app.store._activeCollectionIdOrName = collection;
|
|
} else {
|
|
app.store._activeCollectionIdOrName = collection?.id;
|
|
}
|
|
},
|
|
async silentlyReloadCollections() {
|
|
try {
|
|
let newCollections = await app.pb.collections.getFullList({
|
|
requestKey: "appStore.silentlyReloadCollections",
|
|
});
|
|
newCollections = app.utils.sortedCollectionsByType(newCollections);
|
|
|
|
if (JSON.stringify(newCollections) != JSON.stringify(app.store.collections)) {
|
|
app.store.collections = newCollections;
|
|
}
|
|
} catch (err) {
|
|
if (!err.isAbort) {
|
|
console.warn("failed to reload app store collections:", err);
|
|
}
|
|
}
|
|
},
|
|
async loadCollections(activeIdOrName = null) {
|
|
app.store.isLoadingCollections = true;
|
|
|
|
try {
|
|
const [resultScaffolds, resultCollections] = await Promise.all([
|
|
app.pb.collections.getScaffolds({ requestKey: "appStore.loadCollections.getScaffolds" }),
|
|
app.pb.collections.getFullList({ requestKey: "appStore.loadCollections.getFullList" }),
|
|
]);
|
|
|
|
app.store.collections = app.utils.sortedCollectionsByType(resultCollections);
|
|
app.store.collectionScaffolds = resultScaffolds;
|
|
app.store._activeCollectionIdOrName = activeIdOrName || app.store._activeCollectionIdOrName
|
|
|| app.store.collections[0]?.id || "";
|
|
app.store.isLoadingCollections = false;
|
|
} catch (err) {
|
|
if (!err.isAbort) {
|
|
app.store.isLoadingCollections = false;
|
|
app.checkApiError(err);
|
|
}
|
|
}
|
|
},
|
|
addOrUpdateCollection(collection) {
|
|
const index = app.store.collections.findIndex((c) => c.id == collection.id);
|
|
if (index >= 0) {
|
|
if (app.store.activeCollection.id == collection.id) {
|
|
app.store._activeCollectionIdOrName = collection.id;
|
|
}
|
|
|
|
app.store.collections[index] = collection;
|
|
} else {
|
|
app.store.collections.push(collection);
|
|
}
|
|
|
|
app.store.collections = app.utils.sortedCollectionsByType(app.store.collections);
|
|
},
|
|
|
|
oauth2Providers: [],
|
|
isLoadingOAuth2Providers: false,
|
|
async loadOAuth2Providers() {
|
|
app.store.isLoadingOAuth2Providers = true;
|
|
|
|
try {
|
|
// @todo replace with SDK call
|
|
app.store.oauth2Providers = await app.pb.send("/api/collections/meta/oauth2-providers");
|
|
app.store.isLoadingOAuth2Providers = false;
|
|
} catch (err) {
|
|
if (!err.isAbort) {
|
|
app.checkApiError(err);
|
|
app.store.isLoadingOAuth2Providers = false;
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
// reset title and errors on route change
|
|
window.addEventListener("hashchange", () => {
|
|
app.store.title = "";
|
|
app.store.errors = null;
|
|
});
|
|
|
|
// append the app settings name to document.title.
|
|
watch(() => {
|
|
let titleParts = app.utils.toArray(app.store.title);
|
|
|
|
const appName = app.store.settings?.meta?.appName || "";
|
|
if (appName) {
|
|
titleParts.push(appName);
|
|
}
|
|
|
|
document.title = titleParts.join(" - ");
|
|
});
|
|
|
|
// sync <meta="theme-color"> with the accent color
|
|
let metaThemeColor;
|
|
watch(() => app.store.settings?.meta?.accentColor, (newColor) => {
|
|
if (!metaThemeColor) {
|
|
metaThemeColor = t.meta({ name: "theme-color" });
|
|
document.head.appendChild(metaThemeColor);
|
|
}
|
|
|
|
if (newColor) {
|
|
metaThemeColor?.setAttribute("content", newColor);
|
|
document.documentElement.style.setProperty("--accentColor", newColor);
|
|
} else {
|
|
metaThemeColor?.removeAttribute("content");
|
|
document.documentElement.style.removeProperty("--accentColor");
|
|
}
|
|
});
|
|
|
|
// sync favicon
|
|
let linkFavicon;
|
|
watch(() => app.store.favicon, (favicon) => {
|
|
if (!linkFavicon) {
|
|
linkFavicon = t.link({ rel: "icon" });
|
|
document.head.appendChild(linkFavicon);
|
|
}
|
|
|
|
if (favicon) {
|
|
linkFavicon.href = favicon;
|
|
} else {
|
|
linkFavicon.href = window.location.href.startsWith("https://")
|
|
? "./images/favicon_prod.png"
|
|
: "./images/favicon.png";
|
|
}
|
|
});
|
|
|
|
// sync color scheme
|
|
const colorSchemeMedia = window.matchMedia("(prefers-color-scheme: dark)");
|
|
app.store._mediaColorScheme = colorSchemeMedia.matches ? "dark" : "light";
|
|
colorSchemeMedia.addEventListener("change", ({ matches }) => {
|
|
app.store._mediaColorScheme = matches ? "dark" : "light";
|
|
});
|
|
watch(() => app.store.userColorScheme, (colorScheme) => {
|
|
if (!colorScheme) {
|
|
window.localStorage.removeItem(COLOR_SCHEME_STORAGE_KEY);
|
|
} else {
|
|
window.localStorage.setItem(COLOR_SCHEME_STORAGE_KEY, colorScheme);
|
|
}
|
|
|
|
notifyChannel?.postMessage({ colorScheme });
|
|
});
|
|
|
|
// temporary disable animations on color scheme change to minimize flickering
|
|
let tempNoAnimationTimeoutId;
|
|
watch(() => app.store.activeColorScheme, (colorScheme) => {
|
|
clearTimeout(tempNoAnimationTimeoutId);
|
|
document.documentElement.style.setProperty("--animationSpeed", "0");
|
|
|
|
document.documentElement.setAttribute("data-color-scheme", colorScheme);
|
|
|
|
// restore animation
|
|
tempNoAnimationTimeoutId = setTimeout(() => {
|
|
document.documentElement.style.removeProperty("--animationSpeed");
|
|
}, 100);
|
|
});
|
|
|
|
// Errors handler
|
|
// -------------------------------------------------------------------
|
|
|
|
function removeErrorState(input, container) {
|
|
if (input.__errListener) {
|
|
input.removeEventListener("input", input.__errListener);
|
|
input.removeEventListener("change", input.__errListener);
|
|
input.__errListener = null;
|
|
}
|
|
|
|
if (input.setCustomValidity) {
|
|
input.setCustomValidity("");
|
|
if (input._oldTitle) {
|
|
input.setAttribute("title", input._oldTitle);
|
|
} else {
|
|
input.removeAttribute("title");
|
|
}
|
|
}
|
|
|
|
input.removeAttribute("data-error");
|
|
|
|
const helpElem = container.nextSibling;
|
|
if (
|
|
helpElem
|
|
&& helpElem.classList?.contains("generated-error")
|
|
// remove only the error help text related to the input
|
|
&& helpElem.getAttribute("data-input-name") == input.getAttribute("name")
|
|
) {
|
|
helpElem.remove();
|
|
}
|
|
|
|
// no other error inputs
|
|
if (!container.querySelector("[data-error]")) {
|
|
container.classList.remove("error");
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => JSON.stringify(app.store.errors) && app.store.errors,
|
|
(errs) => {
|
|
// search for input or other elements wiht "name" attribute
|
|
const inputs = document.querySelectorAll(`[name]`);
|
|
|
|
for (let input of inputs) {
|
|
if (input.classList.contains("no-error")) {
|
|
continue;
|
|
}
|
|
|
|
const container = input.closest(".fields") || input.closest(".field");
|
|
if (!container) {
|
|
continue;
|
|
}
|
|
|
|
const name = input.getAttribute("name");
|
|
|
|
removeErrorState(input, container);
|
|
|
|
const errMsg = app.utils.getByPath(errs, name)?.message;
|
|
if (!errMsg) {
|
|
continue;
|
|
}
|
|
|
|
container.classList.add("error");
|
|
|
|
input.__errListener = function() {
|
|
removeErrorState(input, container);
|
|
app.utils.deleteByPath(app.store.errors, name);
|
|
};
|
|
input.addEventListener("input", input.__errListener);
|
|
input.addEventListener("change", input.__errListener);
|
|
input.setAttribute("data-error", true);
|
|
|
|
if (input.setCustomValidity && input.reportValidity && input.classList.contains("inline-error")) {
|
|
input.setCustomValidity(errMsg);
|
|
input.reportValidity();
|
|
|
|
input._oldTitle = input.title;
|
|
input.title = errMsg;
|
|
} else {
|
|
container.after(
|
|
t.div({
|
|
"html-data-input-name": name,
|
|
"className": "field-help error generated-error",
|
|
"textContent": errMsg,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
// Tabs sync
|
|
// -------------------------------------------------------------------
|
|
|
|
notifyChannel.onmessage = (e) => {
|
|
if (
|
|
e.data?.collections
|
|
// replace only if there are changes to minimize flickering
|
|
&& JSON.stringify(app.store.collections) != JSON.stringify(e.data.collections)
|
|
) {
|
|
app.store.collections = e.data.collections;
|
|
}
|
|
|
|
if (
|
|
e.data?.settings
|
|
// replace only if there are changes to minimize flickering
|
|
&& JSON.stringify(app.store.settings) != JSON.stringify(e.data.settings)
|
|
) {
|
|
app.store.settings = e.data.settings;
|
|
}
|
|
|
|
if (e.data?.colorScheme) {
|
|
app.store.userColorScheme = e.data.colorScheme;
|
|
}
|
|
};
|
|
|
|
watch(
|
|
() => JSON.stringify(app.store.collections),
|
|
(newHash, oldHash) => {
|
|
if (newHash && newHash != "[]" && oldHash && oldHash != "[]" && newHash != oldHash) {
|
|
notifyChannel?.postMessage({
|
|
collections: JSON.parse(newHash),
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
watch(
|
|
() => JSON.stringify(app.store.settings),
|
|
(newHash, oldHash) => {
|
|
if (newHash && newHash != "{}" && oldHash && oldHash != "{}" && newHash != oldHash) {
|
|
notifyChannel?.postMessage({
|
|
settings: JSON.parse(newHash),
|
|
});
|
|
}
|
|
|
|
window.localStorage.setItem(SETTINGS_STORAGE_KEY, newHash);
|
|
},
|
|
);
|