2022-07-07 05:19:05 +08:00
|
|
|
<script>
|
2022-07-18 05:16:09 +08:00
|
|
|
import { createEventDispatcher, tick } from "svelte";
|
2022-07-07 05:19:05 +08:00
|
|
|
import { Record } from "pocketbase";
|
|
|
|
import CommonHelper from "@/utils/CommonHelper";
|
|
|
|
import ApiClient from "@/utils/ApiClient";
|
2022-10-30 16:28:14 +08:00
|
|
|
import tooltip from "@/actions/tooltip";
|
2022-07-07 05:19:05 +08:00
|
|
|
import { setErrors } from "@/stores/errors";
|
|
|
|
import { confirm } from "@/stores/confirmation";
|
|
|
|
import { addSuccessToast } from "@/stores/toasts";
|
|
|
|
import Field from "@/components/base/Field.svelte";
|
|
|
|
import Toggler from "@/components/base/Toggler.svelte";
|
|
|
|
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
2022-10-30 16:28:14 +08:00
|
|
|
import AuthFields from "@/components/records/fields/AuthFields.svelte";
|
2022-07-07 05:19:05 +08:00
|
|
|
import TextField from "@/components/records/fields/TextField.svelte";
|
|
|
|
import NumberField from "@/components/records/fields/NumberField.svelte";
|
|
|
|
import BoolField from "@/components/records/fields/BoolField.svelte";
|
|
|
|
import EmailField from "@/components/records/fields/EmailField.svelte";
|
|
|
|
import UrlField from "@/components/records/fields/UrlField.svelte";
|
|
|
|
import DateField from "@/components/records/fields/DateField.svelte";
|
|
|
|
import SelectField from "@/components/records/fields/SelectField.svelte";
|
|
|
|
import JsonField from "@/components/records/fields/JsonField.svelte";
|
|
|
|
import FileField from "@/components/records/fields/FileField.svelte";
|
|
|
|
import RelationField from "@/components/records/fields/RelationField.svelte";
|
2023-01-17 19:31:48 +08:00
|
|
|
import EditorField from "@/components/records/fields/EditorField.svelte";
|
2022-10-30 16:28:14 +08:00
|
|
|
import ExternalAuthsList from "@/components/records/ExternalAuthsList.svelte";
|
2022-07-07 05:19:05 +08:00
|
|
|
|
|
|
|
const dispatch = createEventDispatcher();
|
2022-10-30 16:28:14 +08:00
|
|
|
|
2022-07-07 05:19:05 +08:00
|
|
|
const formId = "record_" + CommonHelper.randomString(5);
|
2022-10-30 16:28:14 +08:00
|
|
|
const TAB_FORM = "form";
|
|
|
|
const TAB_PROVIDERS = "providers";
|
2022-07-07 05:19:05 +08:00
|
|
|
|
|
|
|
export let collection;
|
|
|
|
|
|
|
|
let recordPanel;
|
|
|
|
let original = null;
|
|
|
|
let record = new Record();
|
|
|
|
let isSaving = false;
|
|
|
|
let confirmClose = false; // prevent close recursion
|
|
|
|
let uploadedFilesMap = {}; // eg.: {"field1":[File1, File2], ...}
|
|
|
|
let deletedFileIndexesMap = {}; // eg.: {"field1":[0, 1], ...}
|
|
|
|
let initialFormHash = "";
|
2022-10-30 16:28:14 +08:00
|
|
|
let activeTab = TAB_FORM;
|
2022-07-07 05:19:05 +08:00
|
|
|
|
|
|
|
$: hasFileChanges =
|
|
|
|
CommonHelper.hasNonEmptyProps(uploadedFilesMap) ||
|
|
|
|
CommonHelper.hasNonEmptyProps(deletedFileIndexesMap);
|
|
|
|
|
|
|
|
$: hasChanges = hasFileChanges || initialFormHash != calculateFormHash(record);
|
|
|
|
|
|
|
|
$: canSave = record.isNew || hasChanges;
|
|
|
|
|
|
|
|
export function show(model) {
|
|
|
|
load(model);
|
|
|
|
|
|
|
|
confirmClose = true;
|
|
|
|
|
2022-10-30 16:28:14 +08:00
|
|
|
activeTab = TAB_FORM;
|
|
|
|
|
2022-07-07 05:19:05 +08:00
|
|
|
return recordPanel?.show();
|
|
|
|
}
|
|
|
|
|
|
|
|
export function hide() {
|
|
|
|
return recordPanel?.hide();
|
|
|
|
}
|
|
|
|
|
2022-07-18 05:16:09 +08:00
|
|
|
async function load(model) {
|
2022-07-07 05:19:05 +08:00
|
|
|
setErrors({}); // reset errors
|
|
|
|
original = model || {};
|
2022-10-30 16:28:14 +08:00
|
|
|
if (model?.clone) {
|
|
|
|
record = model.clone();
|
|
|
|
} else {
|
|
|
|
record = new Record();
|
|
|
|
}
|
2022-07-07 05:19:05 +08:00
|
|
|
uploadedFilesMap = {};
|
|
|
|
deletedFileIndexesMap = {};
|
2022-07-18 05:16:09 +08:00
|
|
|
await tick(); // wait to populate the fields to get the normalized values
|
2022-07-07 05:19:05 +08:00
|
|
|
initialFormHash = calculateFormHash(record);
|
|
|
|
}
|
|
|
|
|
|
|
|
function calculateFormHash(m) {
|
|
|
|
return JSON.stringify(m);
|
|
|
|
}
|
|
|
|
|
|
|
|
function save() {
|
2022-10-30 16:28:14 +08:00
|
|
|
if (isSaving || !canSave || !collection?.id) {
|
2022-07-07 05:19:05 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
isSaving = true;
|
|
|
|
|
|
|
|
const data = exportFormData();
|
|
|
|
|
|
|
|
let request;
|
|
|
|
if (record.isNew) {
|
2022-10-30 16:28:14 +08:00
|
|
|
request = ApiClient.collection(collection.id).create(data);
|
2022-07-07 05:19:05 +08:00
|
|
|
} else {
|
2022-10-30 16:28:14 +08:00
|
|
|
request = ApiClient.collection(collection.id).update(record.id, data);
|
2022-07-07 05:19:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
request
|
2022-10-30 16:28:14 +08:00
|
|
|
.then((result) => {
|
2022-07-07 05:19:05 +08:00
|
|
|
addSuccessToast(
|
|
|
|
record.isNew ? "Successfully created record." : "Successfully updated record."
|
|
|
|
);
|
|
|
|
confirmClose = false;
|
|
|
|
hide();
|
|
|
|
dispatch("save", result);
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
ApiClient.errorResponseHandler(err);
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
isSaving = false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function deleteConfirm() {
|
|
|
|
if (!original?.id) {
|
|
|
|
return; // nothing to delete
|
|
|
|
}
|
|
|
|
|
|
|
|
confirm(`Do you really want to delete the selected record?`, () => {
|
2022-10-30 16:28:14 +08:00
|
|
|
return ApiClient.collection(original.collectionId)
|
|
|
|
.delete(original.id)
|
2022-07-07 05:19:05 +08:00
|
|
|
.then(() => {
|
|
|
|
hide();
|
|
|
|
addSuccessToast("Successfully deleted record.");
|
|
|
|
dispatch("delete", original);
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
ApiClient.errorResponseHandler(err);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function exportFormData() {
|
|
|
|
const data = record?.export() || {};
|
|
|
|
const formData = new FormData();
|
|
|
|
|
2022-10-30 16:28:14 +08:00
|
|
|
const exportableFields = {};
|
2022-07-07 05:19:05 +08:00
|
|
|
for (const field of collection?.schema || []) {
|
2022-10-30 16:28:14 +08:00
|
|
|
exportableFields[field.name] = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (collection?.isAuth) {
|
|
|
|
exportableFields["username"] = true;
|
|
|
|
exportableFields["email"] = true;
|
|
|
|
exportableFields["emailVisibility"] = true;
|
|
|
|
exportableFields["password"] = true;
|
|
|
|
exportableFields["passwordConfirm"] = true;
|
|
|
|
exportableFields["verified"] = true;
|
2022-07-07 05:19:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// export base fields
|
|
|
|
for (const key in data) {
|
|
|
|
// skip non-schema fields
|
2022-10-30 16:28:14 +08:00
|
|
|
if (!exportableFields[key]) {
|
2022-07-07 05:19:05 +08:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// normalize nullable values
|
|
|
|
if (typeof data[key] === "undefined") {
|
|
|
|
data[key] = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
CommonHelper.addValueToFormData(formData, key, data[key]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// add uploaded files (if any)
|
|
|
|
for (const key in uploadedFilesMap) {
|
|
|
|
const files = CommonHelper.toArray(uploadedFilesMap[key]);
|
|
|
|
for (const file of files) {
|
|
|
|
formData.append(key, file);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// unset deleted files (if any)
|
|
|
|
for (const key in deletedFileIndexesMap) {
|
|
|
|
const indexes = CommonHelper.toArray(deletedFileIndexesMap[key]);
|
|
|
|
for (const index of indexes) {
|
|
|
|
formData.append(key + "." + index, "");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return formData;
|
|
|
|
}
|
2022-10-30 16:28:14 +08:00
|
|
|
|
|
|
|
function sendVerificationEmail() {
|
|
|
|
if (!collection?.id || !original?.email) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
confirm(`Do you really want to sent verification email to ${original.email}?`, () => {
|
|
|
|
return ApiClient.collection(collection.id)
|
|
|
|
.requestVerification(original.email)
|
|
|
|
.then(() => {
|
|
|
|
addSuccessToast(`Successfully sent verification email to ${original.email}.`);
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
ApiClient.errorResponseHandler(err);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function sendPasswordResetEmail() {
|
|
|
|
if (!collection?.id || !original?.email) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
confirm(`Do you really want to sent password reset email to ${original.email}?`, () => {
|
|
|
|
return ApiClient.collection(collection.id)
|
|
|
|
.requestPasswordReset(original.email)
|
|
|
|
.then(() => {
|
|
|
|
addSuccessToast(`Successfully sent password reset email to ${original.email}.`);
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
ApiClient.errorResponseHandler(err);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2022-07-07 05:19:05 +08:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<OverlayPanel
|
|
|
|
bind:this={recordPanel}
|
2022-10-30 16:28:14 +08:00
|
|
|
class="overlay-panel-lg record-panel {collection?.isAuth && !record.isNew ? 'colored-header' : ''}"
|
2022-07-07 05:19:05 +08:00
|
|
|
beforeHide={() => {
|
|
|
|
if (hasChanges && confirmClose) {
|
|
|
|
confirm("You have unsaved changes. Do you really want to close the panel?", () => {
|
|
|
|
confirmClose = false;
|
|
|
|
hide();
|
|
|
|
});
|
|
|
|
return false;
|
|
|
|
}
|
2022-10-30 16:28:14 +08:00
|
|
|
setErrors({});
|
2022-07-07 05:19:05 +08:00
|
|
|
return true;
|
|
|
|
}}
|
|
|
|
on:hide
|
|
|
|
on:show
|
|
|
|
>
|
|
|
|
<svelte:fragment slot="header">
|
|
|
|
<h4>
|
|
|
|
{record.isNew ? "New" : "Edit"}
|
2022-10-30 16:28:14 +08:00
|
|
|
<strong>{collection?.name}</strong> record
|
2022-07-07 05:19:05 +08:00
|
|
|
</h4>
|
|
|
|
|
2022-10-30 16:28:14 +08:00
|
|
|
{#if !record.isNew}
|
2022-07-07 05:19:05 +08:00
|
|
|
<div class="flex-fill" />
|
2023-01-24 03:57:35 +08:00
|
|
|
<button type="button" class="btn btn-sm btn-circle btn-transparent">
|
2022-07-07 05:19:05 +08:00
|
|
|
<div class="content">
|
|
|
|
<i class="ri-more-line" />
|
2022-10-30 16:28:14 +08:00
|
|
|
<Toggler class="dropdown dropdown-right dropdown-nowrap">
|
|
|
|
{#if collection.isAuth && !original.verified && original.email}
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="dropdown-item closable"
|
|
|
|
on:click={() => sendVerificationEmail()}
|
|
|
|
>
|
|
|
|
<i class="ri-mail-check-line" />
|
|
|
|
<span class="txt">Send verification email</span>
|
|
|
|
</button>
|
|
|
|
{/if}
|
|
|
|
{#if collection.isAuth && original.email}
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="dropdown-item closable"
|
|
|
|
on:click={() => sendPasswordResetEmail()}
|
|
|
|
>
|
|
|
|
<i class="ri-mail-lock-line" />
|
|
|
|
<span class="txt">Send password reset email</span>
|
|
|
|
</button>
|
|
|
|
{/if}
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="dropdown-item txt-danger closable"
|
|
|
|
on:click|preventDefault|stopPropagation={() => deleteConfirm()}
|
|
|
|
>
|
2022-07-07 05:19:05 +08:00
|
|
|
<i class="ri-delete-bin-7-line" />
|
|
|
|
<span class="txt">Delete</span>
|
2022-10-30 16:28:14 +08:00
|
|
|
</button>
|
2022-07-07 05:19:05 +08:00
|
|
|
</Toggler>
|
|
|
|
</div>
|
|
|
|
</button>
|
|
|
|
{/if}
|
|
|
|
|
2022-10-30 16:28:14 +08:00
|
|
|
{#if collection.isAuth && !record.isNew}
|
|
|
|
<div class="tabs-header stretched">
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="tab-item"
|
|
|
|
class:active={activeTab === TAB_FORM}
|
|
|
|
on:click={() => (activeTab = TAB_FORM)}
|
|
|
|
>
|
|
|
|
Account
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="tab-item"
|
|
|
|
class:active={activeTab === TAB_PROVIDERS}
|
|
|
|
on:click={() => (activeTab = TAB_PROVIDERS)}
|
|
|
|
>
|
|
|
|
Authorized providers
|
|
|
|
</button>
|
|
|
|
</div>
|
2022-07-07 05:19:05 +08:00
|
|
|
{/if}
|
2022-10-30 16:28:14 +08:00
|
|
|
</svelte:fragment>
|
|
|
|
|
|
|
|
<div class="tabs-content">
|
|
|
|
<form
|
|
|
|
id={formId}
|
|
|
|
class="tab-item"
|
|
|
|
class:active={activeTab === TAB_FORM}
|
|
|
|
on:submit|preventDefault={save}
|
|
|
|
>
|
|
|
|
{#if !record.isNew}
|
|
|
|
<Field class="form-field disabled" name="id" let:uniqueId>
|
|
|
|
<label for={uniqueId}>
|
|
|
|
<i class={CommonHelper.getFieldTypeIcon("primary")} />
|
|
|
|
<span class="txt">id</span>
|
|
|
|
<span class="flex-fill" />
|
|
|
|
</label>
|
|
|
|
<div class="form-field-addon">
|
|
|
|
<i
|
|
|
|
class="ri-calendar-event-line txt-disabled"
|
|
|
|
use:tooltip={{
|
|
|
|
text: `Created: ${record.created}\nUpdated: ${record.updated}`,
|
|
|
|
position: "left",
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<input type="text" id={uniqueId} value={record.id} readonly />
|
|
|
|
</Field>
|
|
|
|
{/if}
|
2022-07-07 05:19:05 +08:00
|
|
|
|
2022-10-30 16:28:14 +08:00
|
|
|
{#if collection?.isAuth}
|
|
|
|
<AuthFields bind:record {collection} />
|
2022-11-16 21:13:04 +08:00
|
|
|
|
|
|
|
{#if collection?.schema?.length}
|
|
|
|
<hr />
|
|
|
|
{/if}
|
2022-07-07 05:19:05 +08:00
|
|
|
{/if}
|
2022-10-30 16:28:14 +08:00
|
|
|
|
|
|
|
{#each collection?.schema || [] as field (field.name)}
|
|
|
|
{#if field.type === "text"}
|
|
|
|
<TextField {field} bind:value={record[field.name]} />
|
|
|
|
{:else if field.type === "number"}
|
|
|
|
<NumberField {field} bind:value={record[field.name]} />
|
|
|
|
{:else if field.type === "bool"}
|
|
|
|
<BoolField {field} bind:value={record[field.name]} />
|
|
|
|
{:else if field.type === "email"}
|
|
|
|
<EmailField {field} bind:value={record[field.name]} />
|
|
|
|
{:else if field.type === "url"}
|
|
|
|
<UrlField {field} bind:value={record[field.name]} />
|
2023-01-17 19:31:48 +08:00
|
|
|
{:else if field.type === "editor"}
|
|
|
|
<EditorField {field} bind:value={record[field.name]} />
|
2022-10-30 16:28:14 +08:00
|
|
|
{:else if field.type === "date"}
|
|
|
|
<DateField {field} bind:value={record[field.name]} />
|
|
|
|
{:else if field.type === "select"}
|
|
|
|
<SelectField {field} bind:value={record[field.name]} />
|
|
|
|
{:else if field.type === "json"}
|
|
|
|
<JsonField {field} bind:value={record[field.name]} />
|
|
|
|
{:else if field.type === "file"}
|
|
|
|
<FileField
|
|
|
|
{field}
|
|
|
|
{record}
|
|
|
|
bind:value={record[field.name]}
|
|
|
|
bind:uploadedFiles={uploadedFilesMap[field.name]}
|
|
|
|
bind:deletedFileIndexes={deletedFileIndexesMap[field.name]}
|
|
|
|
/>
|
|
|
|
{:else if field.type === "relation"}
|
|
|
|
<RelationField {field} bind:value={record[field.name]} />
|
|
|
|
{/if}
|
|
|
|
{/each}
|
|
|
|
</form>
|
|
|
|
|
|
|
|
{#if collection.isAuth && !record.isNew}
|
|
|
|
<div class="tab-item" class:active={activeTab === TAB_PROVIDERS}>
|
|
|
|
<ExternalAuthsList {record} />
|
2022-07-07 05:19:05 +08:00
|
|
|
</div>
|
2022-10-30 16:28:14 +08:00
|
|
|
{/if}
|
|
|
|
</div>
|
2022-07-07 05:19:05 +08:00
|
|
|
|
|
|
|
<svelte:fragment slot="footer">
|
2023-01-24 03:57:35 +08:00
|
|
|
<button type="button" class="btn btn-transparent" disabled={isSaving} on:click={() => hide()}>
|
2022-07-07 05:19:05 +08:00
|
|
|
<span class="txt">Cancel</span>
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
type="submit"
|
|
|
|
form={formId}
|
|
|
|
class="btn btn-expanded"
|
|
|
|
class:btn-loading={isSaving}
|
|
|
|
disabled={!canSave || isSaving}
|
|
|
|
>
|
|
|
|
<span class="txt">{record.isNew ? "Create" : "Save changes"}</span>
|
|
|
|
</button>
|
|
|
|
</svelte:fragment>
|
|
|
|
</OverlayPanel>
|