Files
pocketbase/ui/src/collections/collectionsOverviewModal.js
T
2026-04-18 22:11:58 +03:00

285 lines
11 KiB
JavaScript

window.app = window.app || {};
window.app.modals = window.app.modals || {};
window.app.modals.openCollectionsOverview = function(settings = {
onbeforeopen: null,
onafteropen: null,
onbeforeclose: null,
onafterclose: null,
}) {
const modal = collectionsOverviewModal(settings);
if (!modal) {
return;
}
document.body.appendChild(modal);
app.modals.open(modal);
};
function collectionsOverviewModal(settings = {}) {
const uniqueId = "overview_modal_" + app.utils.randomString();
const tabs = {
"Fields and relations": erd,
"Rules": rules,
};
const data = store({
showSystemCollections: false,
activeTab: Object.keys(tabs)[0],
get collections() {
if (data.showSystemCollections) {
return app.store.collections;
}
return app.store.collections.filter((c) => !c.system);
},
});
const modal = t.div(
{
pbEvent: "collectionsOverviewModal",
className: "modal popup collections-overview-modal",
onbeforeopen: (el) => {
return settings.onbeforeopen?.(el);
},
onafteropen: (el) => {
settings.onafteropen?.(el);
},
onbeforeclose: (el) => {
return settings.onbeforeclose?.(el);
},
onafterclose: (el) => {
settings.onafterclose?.(el);
el?.remove();
},
},
t.header(
{ className: "modal-header isolated" },
t.div(
{ className: "grid sm" },
t.div(
{ className: "col-12" },
t.div(
{ className: "flex" },
t.h6({ className: "modal-title" }, "Collections overview"),
t.div({ className: "flex-fill" }),
t.div(
{ className: "field" },
t.input({
id: uniqueId + ".showSystemCollections",
type: "checkbox",
className: "sm switch",
checked: () => data.showSystemCollections,
onchange: (e) => data.showSystemCollections = e.target.checked,
}),
t.label({ htmlFor: uniqueId + ".showSystemCollections" }, "System collections"),
),
t.button(
{
type: "button",
className: "btn sm secondary transparent circle modal-close-btn",
title: "Close",
onclick: () => app.modals.close(modal),
},
t.i({ className: "ri-close-line", ariaHidden: true }),
),
),
),
t.div(
{ className: "col-12" },
t.div(
{ className: "tabs-header equal-width" },
() => {
const items = [];
for (let title in tabs) {
items.push(t.button({
type: "button",
className: () => `tab-item ${data.activeTab == title ? "active" : ""}`,
onclick: () => data.activeTab = title,
textContent: title,
}));
}
return items;
},
),
),
),
),
() => {
return tabs[data.activeTab]?.(data);
},
);
return modal;
}
function erd(data) {
return t.div(
{ className: "modal-content erd-tab" },
app.components.erd({
collections: () => {
let underscoreA, underscoreB;
function sortSystemUnderscoredLast(a, b) {
underscoreA = a.name.startsWith("_");
underscoreB = b.name.startsWith("_");
if (
(a.system && !b.system)
|| (underscoreA && !underscoreB)
) {
return 1;
}
if (
(!a.system && b.system)
|| (!underscoreA && underscoreB)
) {
return -1;
}
return 0;
}
return data.collections.slice().sort(sortSystemUnderscoredLast);
},
}),
);
}
function rules(data) {
const ruleOptions = [
{ value: "listRule", label: "List/Search rule" },
{ value: "viewRule", label: "View rule" },
{ value: "createRule", label: "Create rule", filter: (c) => c.type != "view" },
{ value: "updateRule", label: "Update rule", filter: (c) => c.type != "view" },
{ value: "deleteRule", label: "Delete rule", filter: (c) => c.type != "view" },
{ value: "authRule", label: "Auth rule", filter: (c) => c.type == "auth" },
{ value: "manageRule", label: "Manage rule", filter: (c) => c.type == "auth" },
{
value: "mfaRule",
label: "MFA rule",
emptyLabel: t.span({ className: "label info" }, "Enabled for everyone"),
rule: (c) => c.mfa?.rule,
filter: (c) => c.mfa?.enabled && c.type == "auth",
},
];
const local = store({
activeRuleOption: ruleOptions[0],
get activeCollections() {
if (!local.activeRuleOption.filter) {
return data.collections;
}
return data.collections.filter((c) => local.activeRuleOption.filter(c));
},
});
return t.div(
{ className: "modal-content rules-tab" },
t.table(
{ className: "rules-table" },
t.thead(
{ className: "sticky" },
t.tr(
null,
t.td(
{ colSpan: 99, className: "col-rule-btns" },
t.div(
{ className: "rule-btns" },
() => {
return ruleOptions.map((opt) => {
return t.button({
type: "button",
className: () =>
`btn sm ${
local.activeRuleOption?.value == opt.value
? "outline"
: "transparent secondary"
}`,
textContent: () => opt.label,
onclick: () => local.activeRuleOption = opt,
});
});
},
),
),
),
),
t.tbody(
null,
() => {
if (!local.activeCollections.length) {
return t.tr(
null,
t.td(
{ colSpan: 99, className: "txt-hint" },
t.p(null, "No collections with the selected rule."),
),
);
}
return local.activeCollections.map((collection) => {
return t.tr(
null,
t.th(
{ className: "min-width" },
t.div(
{ className: "inline-flex gap-10" },
t.i({
ariaHidden: true,
className: () =>
app.collectionTypes[collection.type]?.icon
|| app.utils.fallbackCollectionIcon,
}),
t.span({
className: "txt collection-name",
title: () => collection.name,
textContent: () => collection.name,
}),
),
),
() => {
let rule;
if (local.activeRuleOption.rule) {
rule = local.activeRuleOption.rule(collection);
} else {
rule = collection[local.activeRuleOption.value];
}
return t.td(
{ style: "vertical-align: top" },
() => {
if (rule === null) {
if (local.activeRuleOption.nullLabel) {
return local.activeRuleOption.nullLabel;
}
return t.span({ className: "label success" }, "Superusers only");
}
if (rule === "") {
if (local.activeRuleOption.emptyLabel) {
return local.activeRuleOption.emptyLabel;
}
return t.span({ className: "label info" }, "Public");
}
return app.components.codeBlock({
language: "pbrule",
value: rule,
});
},
);
},
);
});
},
),
),
);
}