[#3344, #2505] optimized records listing

This commit is contained in:
Gani Georgiev
2023-09-24 11:05:12 +03:00
parent 0f5dad7ede
commit 2550a9de54
48 changed files with 442 additions and 236 deletions
+6 -6
View File
@@ -9,7 +9,7 @@
import RefreshButton from "@/components/base/RefreshButton.svelte";
import SortHeader from "@/components/base/SortHeader.svelte";
import FormattedDate from "@/components/base/FormattedDate.svelte";
import HorizontalScroller from "@/components/base/HorizontalScroller.svelte";
import Scroller from "@/components/base/Scroller.svelte";
import CopyIcon from "@/components/base/CopyIcon.svelte";
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
import AdminUpsertPanel from "@/components/admins/AdminUpsertPanel.svelte";
@@ -97,7 +97,7 @@
/>
<div class="clearfix m-b-base" />
<HorizontalScroller class="table-wrapper">
<Scroller class="table-wrapper">
<table class="table" class:table-loading={isLoading}>
<thead>
<tr>
@@ -211,11 +211,11 @@
{/each}
</tbody>
</table>
</HorizontalScroller>
</Scroller>
{#if admins.length}
<small class="block txt-hint txt-right m-t-sm">Showing {admins.length} of {admins.length}</small>
{/if}
<svelte:fragment slot="footer">
<div class="m-r-auto txt-sm txt-hint">Total found: {admins.length}</div>
</svelte:fragment>
</PageWrapper>
<AdminUpsertPanel bind:this={adminUpsertPanel} on:save={() => loadAdmins()} on:delete={() => loadAdmins()} />
@@ -1,81 +0,0 @@
<script>
import { onMount } from "svelte";
let classes = "";
export { classes as class }; // export reserved keyword
let wrapper = null;
let scrollClasses = "";
let scrollTimeoutId = null;
let observer;
export function refresh() {
if (!wrapper) {
return;
}
clearTimeout(scrollTimeoutId);
scrollTimeoutId = setTimeout(() => {
const offsetWidth = wrapper.offsetWidth;
const scrollWidth = wrapper.scrollWidth;
if (scrollWidth - offsetWidth) {
scrollClasses = "scrollable";
if (wrapper.scrollLeft === 0) {
scrollClasses += " scroll-start";
} else if (wrapper.scrollLeft + offsetWidth == scrollWidth) {
scrollClasses += " scroll-end";
}
} else {
scrollClasses = "";
}
}, 100);
}
onMount(() => {
refresh();
observer = new MutationObserver(() => {
refresh();
});
observer.observe(wrapper, {
attributeFilter: ["width"],
childList: true,
subtree: true,
});
return () => {
observer?.disconnect();
clearTimeout(scrollTimeoutId);
};
});
</script>
<svelte:window on:resize={refresh} />
<div class="horizontal-scroller-wrapper">
<slot name="before" />
<div bind:this={wrapper} class="horizontal-scroller {classes} {scrollClasses}" on:scroll={refresh}>
<slot />
</div>
<slot name="after" />
</div>
<style>
.horizontal-scroller {
width: auto;
overflow-x: auto;
}
.horizontal-scroller-wrapper {
position: relative;
}
:global(.horizontal-scroller-wrapper .columns-dropdown) {
top: 40px;
z-index: 100;
max-height: 340px;
}
</style>
@@ -11,6 +11,8 @@
</main>
<footer class="page-footer">
<slot name="footer" />
<a href={import.meta.env.PB_DOCS_URL} target="_blank" rel="noopener noreferrer">
<i class="ri-book-open-line txt-sm" />
<span class="txt">Docs</span>
+151
View File
@@ -0,0 +1,151 @@
<script>
import { onMount, createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
let classes = "";
export { classes as class }; // export reserved keyword
export let vThreshold = 0;
export let hThreshold = 0;
export let dispatchOnNoScroll = true;
let wrapper = null;
let scrollClasses = "";
let throttleTimeoutId = null;
let hDiff;
let vDiff;
let wrapperWidth;
let wrapperHeight;
let observer;
export function resetVerticalScroll() {
if (!wrapper) {
return;
}
wrapper.scrollTop = 0;
}
export function resetHorizontalScroll() {
if (!wrapper) {
return;
}
wrapper.scrollLeft = 0;
}
export function refresh() {
if (!wrapper) {
return;
}
scrollClasses = "";
// +2 extra threshold as a workaround for the lack of subpixel precision
wrapperWidth = wrapper.clientWidth + 2;
wrapperHeight = wrapper.clientHeight + 2;
hDiff = wrapper.scrollWidth - wrapperWidth;
vDiff = wrapper.scrollHeight - wrapperHeight;
// vertical scroller
if (vDiff > 0) {
scrollClasses += " v-scroll";
if (vThreshold >= wrapperHeight) {
vThreshold = 0;
}
if (wrapper.scrollTop - vThreshold <= 0) {
scrollClasses += " v-scroll-start";
dispatch("vScrollStart");
}
if (wrapper.scrollTop + vThreshold >= vDiff) {
scrollClasses += " v-scroll-end";
dispatch("vScrollEnd");
}
} else if (dispatchOnNoScroll) {
dispatch("vScrollEnd");
}
// horizontal scroller
if (hDiff > 0) {
scrollClasses += " h-scroll";
if (hThreshold >= wrapperWidth) {
hThreshold = 0;
}
if (wrapper.scrollLeft - hThreshold <= 0) {
scrollClasses += " h-scroll-start";
dispatch("hScrollStart");
}
if (wrapper.scrollLeft + hThreshold >= hDiff) {
scrollClasses += " h-scroll-end";
dispatch("hScrollEnd");
}
} else if (dispatchOnNoScroll) {
dispatch("hScrollEnd");
}
}
export function throttleRefresh() {
if (throttleTimeoutId) {
return;
}
throttleTimeoutId = setTimeout(() => {
refresh();
throttleTimeoutId = null;
}, 150);
}
onMount(() => {
throttleRefresh();
observer = new MutationObserver(throttleRefresh);
observer.observe(wrapper, {
attributeFilter: ["width", "height"],
childList: true,
subtree: true,
});
return () => {
observer?.disconnect();
clearTimeout(throttleTimeoutId);
};
});
</script>
<svelte:window on:resize={throttleRefresh} />
<div class="scroller-wrapper">
<slot name="before" />
<div bind:this={wrapper} class="scroller {classes} {scrollClasses}" on:scroll={throttleRefresh}>
<slot />
</div>
<slot name="after" />
</div>
<style>
.scroller {
width: auto;
min-height: 0;
overflow: auto;
}
.scroller-wrapper {
position: relative;
min-height: 0;
}
:global(.scroller-wrapper .columns-dropdown) {
top: 40px;
z-index: 100;
max-height: 340px;
}
</style>
+3 -3
View File
@@ -4,7 +4,7 @@
import CommonHelper from "@/utils/CommonHelper";
import SortHeader from "@/components/base/SortHeader.svelte";
import FormattedDate from "@/components/base/FormattedDate.svelte";
import HorizontalScroller from "@/components/base/HorizontalScroller.svelte";
import Scroller from "@/components/base/Scroller.svelte";
const dispatch = createEventDispatcher();
const labelMethodClass = {
@@ -82,7 +82,7 @@
}
</script>
<HorizontalScroller class="table-wrapper">
<Scroller class="table-wrapper">
<table class="table" class:table-loading={isLoading}>
<thead>
<tr>
@@ -211,7 +211,7 @@
{/each}
</tbody>
</table>
</HorizontalScroller>
</Scroller>
{#if items.length}
<small class="block txt-hint txt-right m-t-sm">Showing {items.length} of {totalItems}</small>
+29 -4
View File
@@ -19,6 +19,7 @@
import RecordUpsertPanel from "@/components/records/RecordUpsertPanel.svelte";
import RecordPreviewPanel from "@/components/records/RecordPreviewPanel.svelte";
import RecordsList from "@/components/records/RecordsList.svelte";
import RecordsCount from "@/components/records/RecordsCount.svelte";
const queryParams = new URLSearchParams($querystring);
@@ -27,9 +28,11 @@
let recordUpsertPanel;
let recordPreviewPanel;
let recordsList;
let recordsCount;
let filter = queryParams.get("filter") || "";
let sort = queryParams.get("sort") || "-created";
let selectedCollectionId = queryParams.get("collectionId") || $activeCollection?.id;
let totalCount = 0; // used to manully change the count without the need of reloading the recordsCount component
$: reactiveParams = new URLSearchParams($querystring);
@@ -129,7 +132,7 @@
{:else}
<CollectionsSidebar />
<PageWrapper>
<PageWrapper class="flex-content">
<header class="page-header">
<nav class="breadcrumbs">
<div class="breadcrumb-item">Collections</div>
@@ -176,7 +179,8 @@
autocompleteCollection={$activeCollection}
on:submit={(e) => (filter = e.detail)}
/>
<div class="clearfix m-b-base" />
<div class="clearfix m-b-sm" />
<RecordsList
bind:this={recordsList}
@@ -188,8 +192,21 @@
? recordPreviewPanel.show(e?.detail)
: recordUpsertPanel?.show(e?.detail);
}}
on:delete={() => {
recordsCount?.reload();
}}
on:new={() => recordUpsertPanel?.show()}
/>
<svelte:fragment slot="footer">
<RecordsCount
bind:this={recordsCount}
class="m-r-auto txt-sm txt-hint"
collection={$activeCollection}
{filter}
bind:totalCount
/>
</svelte:fragment>
</PageWrapper>
{/if}
@@ -200,8 +217,16 @@
<RecordUpsertPanel
bind:this={recordUpsertPanel}
collection={$activeCollection}
on:save={() => recordsList?.reloadLoadedPages()}
on:delete={() => recordsList?.reloadLoadedPages()}
on:save={(e) => {
recordsList?.reloadLoadedPages();
if (e.detail?.isNew) {
totalCount++;
}
}}
on:delete={() => {
recordsList?.reloadLoadedPages();
totalCount--;
}}
/>
<RecordPreviewPanel bind:this={recordPreviewPanel} collection={$activeCollection} />
@@ -209,7 +209,10 @@
replaceOriginal(result);
}
dispatch("save", result);
dispatch("save", {
isNew: isNew,
record: result,
});
} catch (err) {
ApiClient.error(err);
}
@@ -0,0 +1,53 @@
<script>
import { createEventDispatcher } from "svelte";
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
const dispatch = createEventDispatcher();
export let collection;
export let filter = "";
export let totalCount = 0;
let classes = undefined;
export { classes as class }; // export reserved keyword
let isLoading = false;
$: if (collection?.id && filter !== -1) {
reload();
}
export async function reload() {
if (!collection?.id) {
return;
}
isLoading = true;
totalCount = 0;
try {
const fallbackSearchFields = CommonHelper.getAllCollectionIdentifiers(collection);
const result = await ApiClient.collection(collection.id).getList(1, 1, {
filter: CommonHelper.normalizeSearchFilter(filter, fallbackSearchFields),
fields: "id",
requestKey: "records_count",
});
totalCount = result.totalItems;
dispatch("count", totalCount);
isLoading = false;
} catch (err) {
if (!err?.isAbort) {
isLoading = false;
console.warn(err);
}
}
}
</script>
<div class="inline-flex flex-gap-5 records-counter {classes}">
<span class="txt">Total found:</span>
<span class="txt">{!isLoading ? totalCount : "..."}</span>
</div>
+45 -30
View File
@@ -12,19 +12,21 @@
import Field from "@/components/base/Field.svelte";
import CopyIcon from "@/components/base/CopyIcon.svelte";
import FormattedDate from "@/components/base/FormattedDate.svelte";
import HorizontalScroller from "@/components/base/HorizontalScroller.svelte";
import Scroller from "@/components/base/Scroller.svelte";
import RecordFieldValue from "@/components/records/RecordFieldValue.svelte";
const dispatch = createEventDispatcher();
const sortRegex = /^([\+\-])?(\w+)$/;
const perPage = 40;
export let collection;
export let sort = "";
export let filter = "";
let scrollWrapper;
let records = [];
let currentPage = 1;
let totalRecords = 0;
let lastTotal = 0;
let bulkSelected = {};
let isLoading = true;
let isDeleting = false;
@@ -44,6 +46,8 @@
$: fields = collection?.schema || [];
$: editorFields = fields.filter((field) => field.type === "editor");
$: relFields = fields.filter((field) => field.type === "relation");
$: visibleFields = fields.filter((field) => !hiddenColumns.includes(field.id));
@@ -52,7 +56,7 @@
load(1);
}
$: canLoadMore = totalRecords > records.length;
$: canLoadMore = lastTotal >= perPage;
$: totalBulkSelected = Object.keys(bulkSelected).length;
@@ -121,11 +125,11 @@
// allow sorting by the relation display fields
let listSort = sort;
const sortMatch = listSort.match(sortRegex);
const relField = sortMatch ? relFields.find((f) => f.name === sortMatch[2]) : null;
if (sortMatch && relField) {
const sortRelField = sortMatch ? relFields.find((f) => f.name === sortMatch[2]) : null;
if (sortMatch && sortRelField) {
const relPresentableFields =
$collections
?.find((c) => c.id == relField.options?.collectionId)
?.find((c) => c.id == sortRelField.options?.collectionId)
?.schema?.filter((f) => f.presentable)
?.map((f) => f.name) || [];
@@ -140,12 +144,22 @@
const fallbackSearchFields = CommonHelper.getAllCollectionIdentifiers(collection);
const listFields = editorFields
.map((f) => f.name + ":excerpt(200)")
.concat(relFields.map((field) => "expand." + field.name + ".*:excerpt(200)"));
if (listFields.length) {
listFields.unshift("*");
}
return ApiClient.collection(collection.id)
.getList(page, 30, {
.getList(page, perPage, {
sort: listSort,
skipTotal: 1,
filter: CommonHelper.normalizeSearchFilter(filter, fallbackSearchFields),
expand: relFields.map((field) => field.name).join(","),
$cancelKey: "records_list",
// @todo temp disable the :excerpt fields until individual RecordUpsert loader is implemented
// fields: listFields.join(","),
requestKey: "records_list",
})
.then(async (result) => {
if (page <= 1) {
@@ -154,7 +168,7 @@
isLoading = false;
currentPage = result.page;
totalRecords = result.totalItems;
lastTotal = result.items.length;
dispatch("load", records.concat(result.items));
// optimize the records listing by rendering the rows in task batches
@@ -184,9 +198,10 @@
}
function clearList() {
scrollWrapper?.resetVerticalScroll();
records = [];
currentPage = 1;
totalRecords = 0;
lastTotal = 0;
bulkSelected = {};
}
@@ -244,6 +259,9 @@
addSuccessToast(
`Successfully deleted the selected ${totalBulkSelected === 1 ? "record" : "records"}.`
);
dispatch("delete", bulkSelected);
deselectAllRecords();
})
.catch((err) => {
@@ -258,7 +276,7 @@
}
</script>
<HorizontalScroller class="table-wrapper">
<Scroller bind:this={scrollWrapper} class="table-wrapper">
<svelte:fragment slot="before">
{#if columnsTrigger}
<Toggler
@@ -517,27 +535,24 @@
</tr>
{/if}
{/each}
{#if records.length && canLoadMore}
<tr>
<td colspan="99" class="txt-center">
<button
class="btn btn-expanded-lg btn-secondary"
disabled={isLoading}
class:btn-loading={isLoading}
on:click|preventDefault={() => load(currentPage + 1)}
>
<span class="txt">Load more</span>
</button>
</td>
</tr>
{/if}
</tbody>
</table>
</HorizontalScroller>
{#if records.length}
<small class="block txt-hint txt-right m-t-sm">Showing {records.length} of {totalRecords}</small>
{/if}
{#if records.length && canLoadMore}
<div class="block txt-center m-t-xs">
<button
type="button"
class="btn btn-lg btn-secondary btn-expanded"
class:btn-loading={isLoading}
class:btn-disabled={isLoading}
on:click={() => load(currentPage + 1)}
>
<span class="txt">Load more ({totalRecords - records.length})</span>
</button>
</div>
{/if}
</Scroller>
{#if totalBulkSelected}
<div class="bulkbar" transition:fly={{ duration: 150, y: 5 }}>
@@ -330,11 +330,11 @@
bind:this={upsertPanel}
{collection}
on:save={(e) => {
CommonHelper.removeByKey(list, "id", e.detail.id);
list.unshift(e.detail);
CommonHelper.removeByKey(list, "id", e.detail.record.id);
list.unshift(e.detail.record);
list = list;
select(e.detail);
select(e.detail.record);
}}
on:delete={(e) => {
CommonHelper.removeByKey(list, "id", e.detail.id);