store unsaved record changes in local storage

This commit is contained in:
Gani Georgiev 2023-04-17 12:43:48 +03:00
parent 6127350e91
commit 5eb54c7a3d
3 changed files with 122 additions and 29 deletions

View File

@ -84,7 +84,7 @@
{:else if field.type === "relation"}
{@const relations = CommonHelper.toArray(rawValue)}
{@const expanded = CommonHelper.toArray(record.expand[field.name])}
{@const relLimit = short ? 20 : 200}
{@const relLimit = short ? 20 : 500}
<div class="inline-flex">
{#if expanded.length}
{#each expanded.slice(0, relLimit) as item, i (i + item)}
@ -103,7 +103,7 @@
</div>
{:else if field.type === "file"}
{@const files = CommonHelper.toArray(rawValue)}
{@const filesLimit = short ? 10 : 200}
{@const filesLimit = short ? 10 : 500}
<div class="inline-flex">
{#each files.slice(0, filesLimit) as filename, i (i + filename)}
<RecordFileThumb {record} {filename} size="sm" />

View File

@ -1,5 +1,6 @@
<script>
import { createEventDispatcher, tick } from "svelte";
import { slide } from "svelte/transition";
import { Record } from "pocketbase";
import CommonHelper from "@/utils/CommonHelper";
import ApiClient from "@/utils/ApiClient";
@ -25,24 +26,25 @@
import ExternalAuthsList from "@/components/records/ExternalAuthsList.svelte";
const dispatch = createEventDispatcher();
const formId = "record_" + CommonHelper.randomString(5);
const TAB_FORM = "form";
const TAB_PROVIDERS = "providers";
const tabFormKey = "form";
const tabProviderKey = "providers";
export let collection;
let recordPanel;
let recordForm;
let original = null;
let record = new Record();
let record = null;
let initialDraft = null;
let isSaving = false;
let confirmClose = false; // prevent close recursion
let uploadedFilesMap = {}; // eg.: {"field1":[File1, File2], ...}
let deletedFileIndexesMap = {}; // eg.: {"field1":[0, 1], ...}
let initialFormHash = "";
let activeTab = TAB_FORM;
let originalSerializedData = JSON.stringify(null);
let serializedData = originalSerializedData;
let activeTab = tabFormKey;
let isNew = true;
let isLoaded = false;
$: hasEditorField = !!collection?.schema?.find((f) => f.type === "editor");
@ -50,18 +52,24 @@
CommonHelper.hasNonEmptyProps(uploadedFilesMap) ||
CommonHelper.hasNonEmptyProps(deletedFileIndexesMap);
$: hasChanges = hasFileChanges || initialFormHash != calculateFormHash(record);
$: serializedData = JSON.stringify(record);
$: hasChanges = hasFileChanges || originalSerializedData != serializedData;
$: isNew = !original || original.isNew;
$: canSave = isNew || hasChanges;
$: if (isLoaded) {
updateDraft(serializedData);
}
export function show(model) {
load(model);
confirmClose = true;
activeTab = TAB_FORM;
activeTab = tabFormKey;
return recordPanel?.show();
}
@ -71,21 +79,73 @@
}
async function load(model) {
isLoaded = false;
setErrors({}); // reset errors
original = model || new Record();
if (model?.$clone) {
record = model.$clone();
} else {
record = new Record();
}
record = original.$clone();
uploadedFilesMap = {};
deletedFileIndexesMap = {};
await tick(); // wait to populate the fields to get the normalized values
initialFormHash = calculateFormHash(record);
// wait to populate the fields to get the normalized values
await tick();
initialDraft = getDraft();
if (!initialDraft || areRecordsEqual(record, initialDraft)) {
initialDraft = null;
}
originalSerializedData = JSON.stringify(record);
isLoaded = true;
}
function calculateFormHash(m) {
return JSON.stringify(m);
function draftKey() {
return "record_draft_" + (collection?.id || "") + "_" + (original?.id || "");
}
function getDraft(fallbackRecord) {
try {
const raw = window.localStorage.getItem(draftKey());
if (raw) {
return new Record(JSON.parse(raw));
}
} catch (_) {}
return fallbackRecord;
}
function updateDraft(newSerializedData) {
window.localStorage.setItem(draftKey(), newSerializedData);
}
function restoreDraft() {
if (initialDraft) {
record = initialDraft;
initialDraft = null;
}
}
function areRecordsEqual(recordA, recordB) {
const cloneA = recordA?.$clone();
const cloneB = recordB?.$clone();
const fileFields = collection?.schema?.filter((f) => f.type === "file");
for (let field of fileFields) {
delete cloneA?.[field.name];
delete cloneB?.[field.name];
}
// delete password props
delete cloneA?.password;
delete cloneA?.passwordConfirm;
delete cloneB?.password;
delete cloneB?.passwordConfirm;
return JSON.stringify(cloneA) == JSON.stringify(cloneB);
}
function deleteDraft() {
initialDraft = null;
window.localStorage.removeItem(draftKey());
}
function save() {
@ -108,6 +168,7 @@
.then((result) => {
addSuccessToast(isNew ? "Successfully created record." : "Successfully updated record.");
confirmClose = false;
deleteDraft();
hide();
dispatch("save", result);
})
@ -258,7 +319,7 @@
await tick();
initialFormHash = "";
originalSerializedData = "";
}
</script>
@ -275,9 +336,13 @@
confirmClose = false;
hide();
});
return false;
}
setErrors({});
deleteDraft();
return true;
}}
on:hide
@ -335,16 +400,16 @@
<button
type="button"
class="tab-item"
class:active={activeTab === TAB_FORM}
on:click={() => (activeTab = TAB_FORM)}
class:active={activeTab === tabFormKey}
on:click={() => (activeTab = tabFormKey)}
>
Account
</button>
<button
type="button"
class="tab-item"
class:active={activeTab === TAB_PROVIDERS}
on:click={() => (activeTab = TAB_PROVIDERS)}
class:active={activeTab === tabProviderKey}
on:click={() => (activeTab = tabProviderKey)}
>
Authorized providers
</button>
@ -354,12 +419,41 @@
<div class="tabs-content">
<form
bind:this={recordForm}
id={formId}
class="tab-item"
class:active={activeTab === TAB_FORM}
class:active={activeTab === tabFormKey}
on:submit|preventDefault={save}
>
{#if !hasChanges && initialDraft}
<div class="block" out:slide={{ duration: 150 }}>
<div class="alert alert-info m-0">
<div class="icon">
<i class="ri-information-line" />
</div>
<div class="content">
The record has previous unsaved changes.
<button
type="button"
class="btn btn-sm btn-secondary"
on:click={() => restoreDraft()}
>
Restore draft
</button>
</div>
<button
type="button"
class="close"
aria-label="Discard draft"
use:tooltip={"Discard draft"}
on:click|preventDefault={() => deleteDraft()}
>
<i class="ri-close-line" />
</button>
</div>
<div class="clearfix p-b-base" />
</div>
{/if}
<Field class="form-field {!isNew ? 'readonly' : ''}" name="id" let:uniqueId>
<label for={uniqueId}>
<i class={CommonHelper.getFieldTypeIcon("primary")} />
@ -429,7 +523,7 @@
</form>
{#if collection.$isAuth && !isNew}
<div class="tab-item" class:active={activeTab === TAB_PROVIDERS}>
<div class="tab-item" class:active={activeTab === tabProviderKey}>
<ExternalAuthsList {record} />
</div>
{/if}

View File

@ -293,7 +293,6 @@ button {
height: var(--loaderSize);
line-height: var(--loaderSize);
font-size: var(--loaderSize);
font-weight: normal;
color: inherit;
text-align: center;
font-weight: normal;