preload the record preview to minimize content jumps

This commit is contained in:
Gani Georgiev
2026-04-23 00:36:24 +03:00
parent 257f03e1fa
commit ae7041a889
4 changed files with 49 additions and 61 deletions

View File

@@ -2,7 +2,7 @@
- Added backups list scroll container ([#7655](https://github.com/pocketbase/pocketbase/issues/7655)). - Added backups list scroll container ([#7655](https://github.com/pocketbase/pocketbase/issues/7655)).
- Optimized record upsert panel loading to minimize layout jumps. - Optimized record upsert and preview modals loading to minimize layout jumps.
## v0.37.3 ## v0.37.3

File diff suppressed because one or more lines are too long

2
ui/dist/index.html vendored
View File

@@ -13,7 +13,7 @@
<!-- prism --> <!-- prism -->
<script src="./libs/prism/prism.js" data-manual></script> <script src="./libs/prism/prism.js" data-manual></script>
<script type="module" crossorigin src="./assets/index-CzeH6zOJ.js"></script> <script type="module" crossorigin src="./assets/index-C2jyM0t-.js"></script>
<link rel="modulepreload" crossorigin href="./assets/pocketbase.es-B_4DUNUU.js"> <link rel="modulepreload" crossorigin href="./assets/pocketbase.es-B_4DUNUU.js">
<link rel="stylesheet" crossorigin href="./assets/index-h3OAAQQg.css"> <link rel="stylesheet" crossorigin href="./assets/index-h3OAAQQg.css">
</head> </head>

View File

@@ -54,6 +54,18 @@ function copyJSON(record) {
} }
function recordPreviewModal(rawRecord, modalSettings) { function recordPreviewModal(rawRecord, modalSettings) {
if (!rawRecord?.id) {
app.toasts.error("Failed to load record.");
console.warn("[recordPreviewModal] missing required record id field:", rawRecord);
return;
}
if (!rawRecord.collectionId && !rawRecord.collectionName) {
app.toasts.error("Failed to load record.");
console.warn("[recordPreviewModal] missing required collectionId or collectionName field:", rawRecord);
return;
}
let modal; let modal;
const uniqueId = app.utils.randomString(); const uniqueId = app.utils.randomString();
@@ -69,39 +81,31 @@ function recordPreviewModal(rawRecord, modalSettings) {
}); });
async function loadRecord() { async function loadRecord() {
if (!rawRecord?.id) {
app.toasts.error("Failed to load record.");
setTimeout(() => app.modals.close(modal), 0);
console.warn("[recordPreviewModal] missing required record id field:", rawRecord);
return;
}
if (!rawRecord.collectionId && !rawRecord.collectionName) {
app.toasts.error("Failed to load record.");
setTimeout(() => app.modals.close(modal), 0);
console.warn("[recordPreviewModal] missing required collectionId or collectionName field:", rawRecord);
return;
}
data.isLoading = true; data.isLoading = true;
try { try {
// eagerly expand first level presentable relations (if any and the collections are loaded) // preload to minimize content jumps
data.record = JSON.parse(JSON.stringify(rawRecord));
// eagerly expand first level relations (if any and the collections are loaded)
let relExpands = []; let relExpands = [];
const presentableRelationFields = data.collection?.fields?.filter( const presentableRelationFields = data.collection?.fields?.filter(
(f) => !f.hidden && f.presentable && f.type == "relation", (f) => !f.hidden && f.type == "relation",
) || []; ) || [];
for (let field of presentableRelationFields) { for (let field of presentableRelationFields) {
relExpands.push(field.name); relExpands.push(field.name);
} }
data.record = await app.pb const record = await app.pb
.collection(rawRecord.collectionId || rawRecord.collectionName) .collection(rawRecord.collectionName || rawRecord.collectionId)
.getOne(rawRecord.id, { .getOne(rawRecord.id, {
requestKey: "record_preview_" + rawRecord.id, requestKey: "record_preview_" + rawRecord.id,
expand: relExpands.join(",") || undefined, expand: relExpands.join(",") || undefined,
}); });
// populate with an up-to-date fields
Object.assign(data.record, record);
data.isLoading = false; data.isLoading = false;
} catch (err) { } catch (err) {
if (!err?.isAbort) { if (!err?.isAbort) {
@@ -130,10 +134,6 @@ function recordPreviewModal(rawRecord, modalSettings) {
modalSettings.onafterclose?.(el); modalSettings.onafterclose?.(el);
el?.remove(); el?.remove();
}, },
onmount: (el) => {
},
onunmount: (el) => {
},
}, },
t.header( t.header(
{ className: "modal-header" }, { className: "modal-header" },
@@ -145,45 +145,32 @@ function recordPreviewModal(rawRecord, modalSettings) {
t.button( t.button(
{ {
title: "More options", title: "More options",
className: "btn sm circle transparent m-l-auto", className: () => `btn sm circle transparent m-l-auto ${data.isLoading ? "loading" : ""}`,
disabled: () => data.isLoading,
"html-popovertarget": uniqueId + "preview-dropdown", "html-popovertarget": uniqueId + "preview-dropdown",
}, },
t.i({ className: "ri-more-line", ariaHidden: true }), t.i({ className: "ri-more-line", ariaHidden: true }),
), ),
t.div({ id: uniqueId + "preview-dropdown", className: "dropdown", popover: "auto" }, (el) => { t.div(
return t.button( { id: uniqueId + "preview-dropdown", className: "dropdown", popover: "auto" },
{ (el) => {
className: "dropdown-item", return t.button(
onclick: () => { {
copyJSON(data.record); className: "dropdown-item",
el.hidePopover(); onclick: () => {
copyJSON(data.record);
el.hidePopover();
},
}, },
}, t.i({ className: "ri-braces-line", ariaHidden: true }),
t.i({ className: "ri-braces-line", ariaHidden: true }), t.span({ className: "txt" }, "Copy JSON"),
t.span({ className: "txt" }, "Copy JSON"), );
); },
}), ),
), ),
t.div({ className: "modal-content" }, () => { t.div(
// loader { className: "modal-content" },
if (data.isLoading || !data.record?.id || !data.collection?.id) { t.table(
return t.table(
null,
t.tbody(null, () => {
const totalRows = data.collection?.fields?.filter((f) => f.type != "password").length || 1;
const rows = [];
for (let i = 0; i < totalRows; i++) {
rows.push(t.tr(null, t.td(null, t.span({ className: "skeleton-loader" }))));
}
return rows;
}),
);
}
// attrs
return t.table(
{ {
pbEvent: "recordPreviewTable", pbEvent: "recordPreviewTable",
className: "record-preview-table responsive-table", className: "record-preview-table responsive-table",
@@ -219,8 +206,8 @@ function recordPreviewModal(rawRecord, modalSettings) {
); );
}); });
}), }),
); ),
}), ),
t.footer( t.footer(
{ className: "modal-footer" }, { className: "modal-footer" },
t.button( t.button(
@@ -234,7 +221,8 @@ function recordPreviewModal(rawRecord, modalSettings) {
t.button( t.button(
{ {
type: "button", type: "button",
className: "btn", className: () => `btn ${data.isLoading ? "loading" : ""}`,
disabled: () => data.isLoading,
onclick: () => downloadJSON(data.record), onclick: () => downloadJSON(data.record),
}, },
t.i({ className: "ri-download-line", ariaHidden: true }), t.i({ className: "ri-download-line", ariaHidden: true }),