optimized record upsert panel loading to minimize layout jumps
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
- 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.
|
||||||
|
|
||||||
|
|
||||||
## v0.37.3
|
## v0.37.3
|
||||||
|
|
||||||
|
|||||||
2
ui/.env
2
ui/.env
@@ -11,4 +11,4 @@ PB_DOCS_URL = "https://pocketbase.io/docs"
|
|||||||
PB_JS_SDK_URL = "https://github.com/pocketbase/js-sdk"
|
PB_JS_SDK_URL = "https://github.com/pocketbase/js-sdk"
|
||||||
PB_DART_SDK_URL = "https://github.com/pocketbase/dart-sdk"
|
PB_DART_SDK_URL = "https://github.com/pocketbase/dart-sdk"
|
||||||
PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases"
|
PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases"
|
||||||
PB_VERSION = "v0.37.3"
|
PB_VERSION = "v0.37.4-dev"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
ui/dist/index.html
vendored
4
ui/dist/index.html
vendored
@@ -13,9 +13,9 @@
|
|||||||
|
|
||||||
<!-- 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-gesvZeL3.js"></script>
|
<script type="module" crossorigin src="./assets/index-DzC-D-6z.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-DNEbN4Hj.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-h3OAAQQg.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -196,12 +196,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-field-input {
|
|
||||||
.record-summary {
|
|
||||||
animation: fadeIn var(--animationSpeed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-type-bool {
|
.field-type-bool {
|
||||||
&.record-field-view .label {
|
&.record-field-view .label {
|
||||||
min-width: 48px;
|
min-width: 48px;
|
||||||
|
|||||||
@@ -36,34 +36,51 @@ export function input(props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const resultRecords = [];
|
||||||
const fieldCollection = app.store.collections.find((c) => c.id == props.field.collectionId);
|
const idsToLoad = [];
|
||||||
|
|
||||||
// eagerly expand first level presentable relations (if any and the collections are loaded)
|
// check for preloaded expand
|
||||||
const relExpands = [];
|
const expanded = app.utils.toArray(props.record.expand?.[props.field.name]);
|
||||||
const presentableRelationFields = fieldCollection?.fields?.filter(
|
for (const id of ids) {
|
||||||
(f) => !f.hidden && f.presentable && f.type == "relation",
|
const found = expanded.find((r) => r.id == id);
|
||||||
) || [];
|
if (found) {
|
||||||
for (const field of presentableRelationFields) {
|
resultRecords.push(found);
|
||||||
relExpands.push(field.name);
|
} else {
|
||||||
|
idsToLoad.push(id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const records = await app.pb.collection(props.field.collectionId).getFullList({
|
try {
|
||||||
requestKey: null,
|
if (idsToLoad.length) {
|
||||||
filter: ids.map((id) => app.pb.filter("id={:id}", { id })).join("||"),
|
const fieldCollection = app.store.collections.find((c) => c.id == props.field.collectionId);
|
||||||
expand: relExpands.join(",") || undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
// preserve the original order
|
// eagerly expand first level presentable relations (if any and the collections are loaded)
|
||||||
const orderedRecords = [];
|
const relExpands = [];
|
||||||
for (let id of ids) {
|
const presentableRelationFields = fieldCollection?.fields?.filter(
|
||||||
const record = records.find((r) => r.id == id);
|
(f) => !f.hidden && f.presentable && f.type == "relation",
|
||||||
if (record) {
|
) || [];
|
||||||
orderedRecords.push(record);
|
for (const field of presentableRelationFields) {
|
||||||
|
relExpands.push(field.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const records = await app.pb.collection(props.field.collectionId).getFullList({
|
||||||
|
requestKey: null,
|
||||||
|
filter: idsToLoad.map((id) => app.pb.filter("id={:id}", { id })).join("||"),
|
||||||
|
expand: relExpands.join(",") || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// preserve the original order
|
||||||
|
for (const id of idsToLoad) {
|
||||||
|
const found = records.find((r) => r.id == id);
|
||||||
|
if (found) {
|
||||||
|
resultRecords.push(found);
|
||||||
|
} else {
|
||||||
|
console.warn("missing relation id:", id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
local.selected = orderedRecords;
|
local.selected = resultRecords;
|
||||||
local.isLoading = false;
|
local.isLoading = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!err.isAbort) {
|
if (!err.isAbort) {
|
||||||
|
|||||||
@@ -193,36 +193,67 @@ function recordUpsertModal(collection, rawRecord, modalSettings) {
|
|||||||
data.record = draftClone;
|
data.record = draftClone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let draftWatcher;
|
||||||
|
|
||||||
|
function initDraftWatcher() {
|
||||||
|
data.initialDraft = getDraft();
|
||||||
|
|
||||||
|
draftWatcher?.unwatch();
|
||||||
|
draftWatcher = watch(() => data.recordHash, (newVal, oldVal) => {
|
||||||
|
if (typeof oldVal == "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.hasChanges) {
|
||||||
|
saveDraft(data.recordHash);
|
||||||
|
} else {
|
||||||
|
deleteDraft();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function initRecord(rawRecord) {
|
async function initRecord(rawRecord) {
|
||||||
data.isLoading = true;
|
data.isLoading = true;
|
||||||
|
|
||||||
const recordId = typeof rawRecord == "string" ? rawRecord : rawRecord?.id;
|
draftWatcher?.unwatch();
|
||||||
|
|
||||||
|
// normalize rawRecord (could be plain id string)
|
||||||
|
rawRecord = app.utils.isObject(rawRecord) ? rawRecord : { id: rawRecord || "" };
|
||||||
|
|
||||||
// new record
|
// new record
|
||||||
if (!recordId) {
|
if (!rawRecord.id) {
|
||||||
const record = app.utils.isObject(rawRecord) ? JSON.parse(JSON.stringify(rawRecord)) : {};
|
data.originalRecord = JSON.parse(JSON.stringify(rawRecord));
|
||||||
data.originalRecord = app.utils.emptyClone(record, ["collectionId", "collectionName"]);
|
data.record = JSON.parse(JSON.stringify(rawRecord));
|
||||||
data.initialDraft = getDraft();
|
|
||||||
data.record = record;
|
|
||||||
data.isLoading = false;
|
data.isLoading = false;
|
||||||
data.isLocked = false;
|
data.isLocked = false;
|
||||||
|
initDraftWatcher();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.isLocked = !!app.store.settings?.meta?.hideControls;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// eagerly load to allow elements to show their "update" UI and minimize flickering
|
data.isLocked = !!app.store.settings?.meta?.hideControls;
|
||||||
data.originalRecord = { id: recordId };
|
|
||||||
|
|
||||||
const record = await app.pb.collection(collection.name).getOne(recordId, {
|
// preload to minimize content jumps
|
||||||
requestKey: "upsert_load_" + recordId,
|
data.originalRecord = JSON.parse(JSON.stringify(rawRecord));
|
||||||
|
data.record = JSON.parse(JSON.stringify(rawRecord));
|
||||||
|
|
||||||
|
// fetch to ensure that the main record fields are up-to-date
|
||||||
|
let record = await app.pb.collection(collection.name).getOne(rawRecord.id, {
|
||||||
|
requestKey: "upsert_load_" + rawRecord.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
data.originalRecord = record;
|
// preload existing expands (if any)
|
||||||
data.initialDraft = getDraft();
|
if (rawRecord.expand) {
|
||||||
data.record = JSON.parse(JSON.stringify(record));
|
record.expand = JSON.parse(JSON.stringify(rawRecord.expand));
|
||||||
|
}
|
||||||
|
|
||||||
|
// extend, not overwrite, to prevent reseting the reference passed down to the inputs
|
||||||
|
Object.assign(data.originalRecord, JSON.parse(JSON.stringify(record)));
|
||||||
|
Object.assign(data.record, JSON.parse(JSON.stringify(record)));
|
||||||
|
|
||||||
data.isLoading = false;
|
data.isLoading = false;
|
||||||
|
initDraftWatcher();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!err?.isAbort) {
|
if (!err?.isAbort) {
|
||||||
app.checkApiError(err);
|
app.checkApiError(err);
|
||||||
@@ -297,12 +328,12 @@ function recordUpsertModal(collection, rawRecord, modalSettings) {
|
|||||||
data.originalRecord = structuredClone(record);
|
data.originalRecord = structuredClone(record);
|
||||||
data.record = structuredClone(record);
|
data.record = structuredClone(record);
|
||||||
} else {
|
} else {
|
||||||
// don't overwrite to prevent loosing the reference passed down to the inputs
|
// extend, not overwrite, to prevent reseting the reference passed down to the inputs
|
||||||
Object.assign(data.originalRecord, structuredClone(record));
|
Object.assign(data.originalRecord, structuredClone(record));
|
||||||
Object.assign(data.record, structuredClone(record));
|
Object.assign(data.record, structuredClone(record));
|
||||||
}
|
}
|
||||||
|
|
||||||
modalSettings.onsave?.(structuredClone(record), isNew);
|
modalSettings.onsave?.(record, isNew);
|
||||||
|
|
||||||
// reset all errors
|
// reset all errors
|
||||||
app.store.errors = null;
|
app.store.errors = null;
|
||||||
@@ -360,8 +391,6 @@ function recordUpsertModal(collection, rawRecord, modalSettings) {
|
|||||||
initRecord(clone);
|
initRecord(clone);
|
||||||
}
|
}
|
||||||
|
|
||||||
const watchers = [];
|
|
||||||
|
|
||||||
function mainTab() {
|
function mainTab() {
|
||||||
return [
|
return [
|
||||||
t.div(
|
t.div(
|
||||||
@@ -605,31 +634,12 @@ function recordUpsertModal(collection, rawRecord, modalSettings) {
|
|||||||
onbeforeopen: () => {
|
onbeforeopen: () => {
|
||||||
initRecord(rawRecord);
|
initRecord(rawRecord);
|
||||||
|
|
||||||
watchers.push(
|
|
||||||
watch(() => data.recordHash, (newHash, oldHash) => {
|
|
||||||
if (!oldHash || !newHash || newHash == "{}" || oldHash == "{}") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveDraft(newHash);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return modalSettings.onbeforeopen?.(el);
|
return modalSettings.onbeforeopen?.(el);
|
||||||
},
|
},
|
||||||
onafteropen: (el) => {
|
onafteropen: (el) => {
|
||||||
modalSettings.onafteropen?.(el);
|
modalSettings.onafteropen?.(el);
|
||||||
},
|
},
|
||||||
onbeforeclose: (el, forceClosed) => {
|
onbeforeclose: (el, forceClosed) => {
|
||||||
if (
|
|
||||||
// there are no unsaved changes
|
|
||||||
!data.hasChanges
|
|
||||||
// the form has been edited
|
|
||||||
&& data.initialDraftHash != getDraftHash()
|
|
||||||
) {
|
|
||||||
deleteDraft();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceClosed) {
|
if (forceClosed) {
|
||||||
return modalSettings.onbeforeclose?.(el);
|
return modalSettings.onbeforeclose?.(el);
|
||||||
}
|
}
|
||||||
@@ -656,11 +666,10 @@ function recordUpsertModal(collection, rawRecord, modalSettings) {
|
|||||||
},
|
},
|
||||||
onafterclose: (el) => {
|
onafterclose: (el) => {
|
||||||
modalSettings.onafterclose?.(el);
|
modalSettings.onafterclose?.(el);
|
||||||
watchers.forEach((w) => w?.unwatch());
|
|
||||||
el?.remove();
|
el?.remove();
|
||||||
},
|
},
|
||||||
onunmount: () => {
|
onunmount: () => {
|
||||||
watchers.forEach((w) => w?.unwatch());
|
draftWatcher?.unwatch();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
t.header(
|
t.header(
|
||||||
|
|||||||
@@ -94,9 +94,9 @@ window.app.components.recordsList = function(propsArg = {}) {
|
|||||||
// eagerly expand first level relations
|
// eagerly expand first level relations
|
||||||
// (to prevent too many relation queries)
|
// (to prevent too many relation queries)
|
||||||
const relExpands = [];
|
const relExpands = [];
|
||||||
const relationFields = props.collection.fields.filter(
|
const relationFields = props.collection.fields?.filter(
|
||||||
(f) => !f.hidden && f.type == "relation",
|
(f) => !f.hidden && f.type == "relation",
|
||||||
);
|
) || [];
|
||||||
for (const field of relationFields) {
|
for (const field of relationFields) {
|
||||||
relExpands.push(field.name);
|
relExpands.push(field.name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -348,6 +348,8 @@ const utils = {
|
|||||||
clone[prop] = "";
|
clone[prop] = "";
|
||||||
} else if (typeof clone[prop] == "number") {
|
} else if (typeof clone[prop] == "number") {
|
||||||
clone[prop] = 0;
|
clone[prop] = 0;
|
||||||
|
} else if (typeof clone[prop] == "boolean") {
|
||||||
|
clone[prop] = false;
|
||||||
} else if (Array.isArray(clone[prop])) {
|
} else if (Array.isArray(clone[prop])) {
|
||||||
clone[prop] = [];
|
clone[prop] = [];
|
||||||
} else if (app.utils.isObject(clone[prop])) {
|
} else if (app.utils.isObject(clone[prop])) {
|
||||||
|
|||||||
Reference in New Issue
Block a user