Files
pocketbase/ui/src/collections/pageCollections.js
2026-04-19 12:29:45 +03:00

336 lines
13 KiB
JavaScript

import { collectionsSidebar } from "./collectionsSidebar";
const SORT_QUERY_KEY = "sort";
const FILTER_QUERY_KEY = "filter";
const COLLECTION_QUERY_KEY = "collection";
const RECORD_QUERY_KEY = "record";
const LAST_ACTIVE_STORAGE_KEY = "pbLastActiveCollection";
const TOTAL_COUNT_REQUEST_KEY = "recordsTotalCountRequest";
export function pageCollections(route) {
app.store.activeCollection = route.query[COLLECTION_QUERY_KEY]?.[0]
|| window.localStorage.getItem(LAST_ACTIVE_STORAGE_KEY);
const pageData = store({
reset: null,
activeRecordIdOrModel: route.query[RECORD_QUERY_KEY]?.[0] || "",
sort: route.query[SORT_QUERY_KEY]?.[0] || "",
filter: route.query[FILTER_QUERY_KEY]?.[0] || "",
totalCount: 0,
isTotalCountLoading: false,
});
async function loadTotalCount() {
if (!app.store.activeCollection?.id) {
return;
}
pageData.isTotalCountLoading = true;
try {
const normalizedFilter = app.utils.normalizeSearchFilter(
pageData.filter,
app.store.activeCollection.fields.filter((f) => !f.hidden).map((f) => f.name),
);
const result = await app.pb.collection(app.store.activeCollection.name).getList(1, 1, {
requestKey: TOTAL_COUNT_REQUEST_KEY,
filter: normalizedFilter,
fields: "id",
});
pageData.totalCount = result.totalItems;
} catch (err) {
if (!err.isAbort) {
pageData.totalCount = 0;
console.warn("failed to load total count:", err);
}
}
pageData.isTotalCountLoading = false;
}
function refreshRecordsList() {
pageData.reset = Date.now();
}
const watchers = [
watch(
() => (app.store.activeCollection?.name || "") + (app.store.activeCollection?.updated || ""),
(newVal, oldVal) => {
app.store.title = app.store.activeCollection?.name || "Collections";
// skip unnecessery initial params replacement
if (!oldVal) {
return;
}
// reset filter and sort params on collection change
if (oldVal != newVal) {
pageData.filter = "";
pageData.sort = "";
}
app.utils.replaceHashQueryParams({
[COLLECTION_QUERY_KEY]: app.store.activeCollection?.name,
[FILTER_QUERY_KEY]: pageData.filter || null,
[SORT_QUERY_KEY]: pageData.sort || null,
}, newVal != oldVal ? true : null);
if (app.store.activeCollection?.id) {
window.localStorage.setItem(LAST_ACTIVE_STORAGE_KEY, app.store.activeCollection.id);
} else {
window.localStorage.removeItem(LAST_ACTIVE_STORAGE_KEY);
}
},
),
watch(
() => [pageData.filter, pageData.sort],
(newVal, oldVal) => {
if (!oldVal) {
return;
}
app.utils.replaceHashQueryParams({
[FILTER_QUERY_KEY]: pageData.filter || null,
[SORT_QUERY_KEY]: pageData.sort || null,
});
},
),
watch(
() => (pageData.activeRecordIdOrModel || "") + (app.store.activeCollection?.id || ""),
(newVal, oldVal) => {
if (!pageData.activeRecordIdOrModel) {
app.utils.replaceHashQueryParams({
[RECORD_QUERY_KEY]: null,
});
return;
}
// no change or the collection model is still loading
if (newVal == oldVal || !app.store.activeCollection?.id) {
return;
}
const recordData = typeof pageData.activeRecordIdOrModel == "string"
? {
id: pageData.activeRecordIdOrModel,
collectionId: app.store.activeCollection?.id,
collectionName: app.store.activeCollection?.name,
}
: pageData.activeRecordIdOrModel;
app.utils.replaceHashQueryParams({
[RECORD_QUERY_KEY]: recordData.id || null,
});
// force close any previous modal
app.modals.close(null, true);
if (app.store.activeCollection?.type == "view") {
app.modals.openRecordPreview(recordData, {
onafterclose: () => {
pageData.activeRecordIdOrModel = "";
},
});
} else {
app.modals.openRecordUpsert(app.store.activeCollection, recordData, {
onafterclose: () => {
pageData.activeRecordIdOrModel = "";
},
});
}
},
),
watch(
() => [app.store.activeCollection?.id, pageData.filter, pageData.reset],
() => loadTotalCount(),
),
];
const documentEvents = {
"record:save": (e) => {
if (e.detail.collectionId != app.store.activeCollection?.id) {
return;
}
pageData.totalCount++;
},
"record:delete": (e) => {
if (
// check both because for delete we don't know which one was assigned to
e.detail.collectionId != app.store.activeCollection?.id
&& e.detail.collectionName != app.store.activeCollection?.name
) {
return;
}
pageData.totalCount--;
},
};
return t.div(
{
pbEvent: "pageCollections",
className: "page",
onmount: () => {
// refresh if necesser the cached collections in the background
if (!app.store.isLoadingCollections) {
app.store.silentlyReloadCollections();
}
for (let event in documentEvents) {
document.addEventListener(event, documentEvents[event]);
}
},
onunmount: () => {
app.pb.cancelRequest(TOTAL_COUNT_REQUEST_KEY);
watchers.forEach((w) => w?.unwatch());
for (let event in documentEvents) {
document.removeEventListener(event, documentEvents[event]);
}
},
},
() => collectionsSidebar(),
t.div(
{ className: "page-content full-height" },
t.header(
{ className: "page-header compact flex-nowrap" },
t.nav(
{ className: "breadcrumbs" },
t.div(null, "Collections"),
() => {
if (app.store.activeCollection?.name) {
return t.div({
title: app.store.activeCollection.name,
textContent: app.store.activeCollection.name,
});
}
},
),
t.div(
{
hidden: () => !app.store.activeCollection?.id,
pbEvent: "pageHeaderSecondaryBtns",
className: "page-header-secondary-btns",
},
t.button(
{
type: "button",
className: "btn circle transparent secondary tooltip-bottom btn-collection-settings",
ariaLabel: app.attrs.tooltip("Collection settings"),
onclick: () => {
app.modals.openCollectionUpsert(app.store.activeCollection, {
ontruncate: () => refreshRecordsList(),
onsave: (collection, isNew) => {
if (isNew) {
// e.g. in case of a duplicate or modal state reset
app.store.activeCollection = collection.id;
} else {
refreshRecordsList();
}
},
});
},
},
t.i({ className: "ri-settings-3-line", ariaHidden: true }),
),
app.components.refreshButton({
onclick: () => refreshRecordsList(),
}),
),
t.div(
{
hidden: () => !app.store.activeCollection?.id,
pbEvent: "pageHeaderPrimaryBtns",
className: "page-header-primary-btns",
},
t.button(
{
type: "button",
className: "btn outline api-preview-btn",
onclick: () => app.modals.openApiPreview(app.store.activeCollection),
},
t.i({ className: "ri-code-s-slash-line", ariaHidden: true }),
t.span({ className: "txt", textContent: "API preview" }),
),
() => {
if (app.store.activeCollection?.type == "view") {
return;
}
return t.button(
{
type: "button",
className: "btn new-record-btn",
onclick: () => app.modals.openRecordUpsert(app.store.activeCollection),
},
t.i({ className: "ri-add-line", ariaHidden: true }),
t.span({ className: "txt", textContent: "New Record" }),
);
},
),
),
// page loader
t.div(
{
hidden: () => !app.store.isLoadingCollections || app.store.activeCollection?.id,
className: "block txt-center p-base",
},
t.span({ className: "loader lg" }),
),
// no selected collection
t.div(
{
hidden: () => app.store.isLoadingCollections || app.store.activeCollection?.id,
className: "block txt-center p-base",
},
t.h6(
{ className: "txt" },
() => {
if (app.store.collections?.length) {
return "Select collection from the sidebar.";
}
return "No collections found.";
},
),
),
// records list
app.components.recordsSearchbar({
hidden: () => !app.store.activeCollection?.id,
collection: () => app.store.activeCollection,
value: () => pageData.filter,
onsubmit: (newFilter) => (pageData.filter = newFilter),
}),
app.components.recordsList({
className: "m-t-sm",
reset: () => pageData.reset,
hidden: () => !app.store.activeCollection?.id,
collection: () => app.store.activeCollection,
filter: () => pageData.filter,
sort: () => pageData.sort,
onselect: (record) => {
pageData.activeRecordIdOrModel = record;
},
onchange: (newFilter, newSort) => {
pageData.filter = newFilter;
pageData.sort = newSort;
},
}),
t.footer(
{ className: "page-footer" },
t.span(
{
className: () => `total-count ${pageData.isTotalCountLoading ? "faded" : ""}`,
},
"Total: ",
() => pageData.totalCount,
),
app.components.credits(),
),
),
);
}