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

262 lines
10 KiB
JavaScript

const PINNED_STORAGE_KEY = "pbPinnedCollections";
const compactThreshold = 12;
export function collectionsSidebar() {
const data = store({
search: "",
pinned: app.utils.getLocalHistory(PINNED_STORAGE_KEY, []),
get filteredCollections() {
if (!data.search.length) {
return app.store.collections;
}
const normalizedSearch = data.search.replaceAll(" ", "").toLowerCase();
return app.store.collections.filter((c) => {
return (c.name + c.id + c.type).toLowerCase().includes(normalizedSearch);
});
},
get systemCollections() {
return data.filteredCollections.filter((c) => c.system && !data.pinned.includes(c.id));
},
get regularCollections() {
return data.filteredCollections.filter((c) => !c.system && !data.pinned.includes(c.id));
},
get pinnedCollections() {
if (!data.pinned.length) {
return [];
}
return data.filteredCollections.filter((c) => data.pinned.includes(c.id));
},
});
function clearSearch() {
data.search = "";
}
const watchers = [];
return app.components.pageSidebar(
{
className: () => `collections-sidebar ${data.responsiveShow ? "active" : ""}`,
onmount: (el) => {
// init and persist pinned changes
watchers.push(watch(() => {
app.utils.saveLocalHistory(PINNED_STORAGE_KEY, JSON.stringify(data.pinned));
}));
// scroll to the active item
watchers.push(watch(
() => app.store.activeCollection?.id,
async () => {
await new Promise((r) => setTimeout(r, 0));
const activeNavItem = el?.querySelector(".nav-item.active");
const details = activeNavItem?.closest("details");
if (details) {
details.open = true;
activeNavItem?.scrollIntoView({ block: "nearest" });
}
},
));
},
onunmount: () => {
watchers.forEach((w) => w?.unwatch());
},
},
t.div(
{ className: "sidebar-search" },
t.div(
{ className: "fields" },
t.div(
{ className: "field" },
t.input({
className: "p-r-5",
type: "text",
placeholder: "Search collections...",
value: () => data.search,
oninput: (e) => data.search = e.target.value,
}),
),
t.div(
{ className: "field addon p-l-0 p-r-5 gap-0" },
t.button(
{
hidden: () => !data.search.length,
type: "button",
className: "btn sm circle transparent secondary",
ariaDescription: app.attrs.tooltip("Clear", "left"),
onclick: clearSearch,
},
t.i({ className: "ri-close-line", ariaHidden: true }),
),
t.button(
{
hidden: () => app.store.isLoadingCollections,
type: "button",
className: "btn sm circle transparent secondary link-faded",
ariaDescription: app.attrs.tooltip("Collections overview", "left"),
onclick: () => app.modals.openCollectionsOverview(),
},
t.i({ className: "ri-organization-chart", ariaHidden: true }),
),
),
),
),
() => {
if (
!data.search.length
|| !!data.filteredCollections.length
|| app.store.isLoadingCollections
) {
return;
}
return t.div(
{ className: "block p-t-base txt-center txt-hint" },
t.p(null, "No collections found."),
t.button({
type: "button",
className: "btn sm secondary",
textContent: "Clear search",
onclick: () => clearSearch(),
}),
);
},
() => {
if (app.store.isLoadingCollections) {
return t.div({ className: "sidebar-content txt-center" }, t.span({ className: "loader sm" }));
}
return [
t.nav(
{
className: () =>
`sidebar-content collections-list scrollable ${
data.regularCollections.length + data.pinnedCollections >= compactThreshold
? "compact"
: ""
}`,
},
t.details(
{
hidden: () => !data.pinnedCollections.length,
className: () => `nav-group nav-group-pinned-collections`,
open: true,
},
t.summary(
{ tabIndex: -1, onfocusout: () => false, onclick: () => false, onkeyup: () => false },
"Pinned",
),
() => data.pinnedCollections.map((c) => collectionItem(c, data)),
),
t.details(
{
hidden: () => !data.regularCollections.length,
className: "nav-group nav-group-regular-collections",
open: true,
},
t.summary(
{ tabIndex: -1, onfocusout: () => false, onclick: () => false, onkeyup: () => false },
() => data.pinnedCollections.length ? "Others" : "Collections",
),
() => data.regularCollections.map((c) => collectionItem(c, data)),
),
t.details(
{
hidden: () => !data.systemCollections.length,
className: "nav-group nav-group-system-collections",
open: () => data.search.length,
},
t.summary(null, "System"),
() => data.systemCollections.map((c) => collectionItem(c, data)),
),
),
t.div(
{
hidden: () => data.search.length && !data.filteredCollections.length,
className: "sidebar-content new-collection",
},
t.button(
{
type: "button",
className: "btn outline block",
onclick: () => {
app.modals.openCollectionUpsert({}, {
onsave: (newCollection) => {
app.store.activeCollection = newCollection.id;
},
});
},
},
t.i({ className: "ri-add-line", ariaHidden: true }),
t.span({ textContent: "New collection" }),
),
),
];
},
);
}
function collectionItem(collection, data) {
return t.button(
{
"html-data-collection-id": () => collection.id,
type: "button",
className: () =>
`nav-item responsive-close ${collection.id == app.store.activeCollection?.id ? "active" : ""}`,
title: () => collection.name,
onclick: () => app.store.activeCollection = collection.name,
},
t.i({
className: () => app.collectionTypes[collection.type]?.icon || app.utils.fallbackCollectionIcon,
ariaHidden: true,
}),
t.span({ className: "txt" }, () => collection.name),
() => {
if (
collection.type != "auth"
|| !collection.oauth2?.enabled
|| collection.oauth2?.providers?.length > 0
) {
return;
}
return t.i({
ariaHidden: true,
className: "ri-alert-line txt-hint txt-sm",
ariaDescription: app.attrs.tooltip(
"OAuth2 auth is enabled but the collection doesn't have any registered providers",
),
});
},
() => {
const pinnedIndex = data.pinned.indexOf(collection.id);
return t.span(
{
tabIndex: -1,
role: "button",
className: "pin",
title: () => pinnedIndex >= 0 ? "Unpin" : "Pin",
onclick: (e) => {
e.preventDefault();
e.stopPropagation();
if (pinnedIndex >= 0) {
data.pinned.splice(pinnedIndex, 1);
} else {
data.pinned.push(collection.id);
}
},
},
t.i({
ariaHidden: false,
className: () => pinnedIndex >= 0 ? "ri-unpin-line" : "ri-pushpin-line",
}),
);
},
);
}