store unsaved record changes in local storage
This commit is contained in:
parent
6127350e91
commit
5eb54c7a3d
|
@ -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" />
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
function calculateFormHash(m) {
|
||||
return JSON.stringify(m);
|
||||
originalSerializedData = JSON.stringify(record);
|
||||
isLoaded = true;
|
||||
}
|
||||
|
||||
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}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue