pocketbase/ui/src/components/collections/CollectionUpsertPanel.svelte

447 lines
15 KiB
Svelte
Raw Normal View History

2022-07-07 05:19:05 +08:00
<script>
import { createEventDispatcher, tick } from "svelte";
import { scale } from "svelte/transition";
2023-02-19 01:33:42 +08:00
import { Collection } from "pocketbase";
2022-07-07 05:19:05 +08:00
import CommonHelper from "@/utils/CommonHelper";
import ApiClient from "@/utils/ApiClient";
import { errors, setErrors, removeError } from "@/stores/errors";
2022-07-07 05:19:05 +08:00
import { confirm } from "@/stores/confirmation";
import { removeAllToasts, addSuccessToast } from "@/stores/toasts";
import { loadCollections, removeCollection } from "@/stores/collections";
2022-07-07 05:19:05 +08:00
import tooltip from "@/actions/tooltip";
import Field from "@/components/base/Field.svelte";
import Toggler from "@/components/base/Toggler.svelte";
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
import CollectionFieldsTab from "@/components/collections/CollectionFieldsTab.svelte";
import CollectionRulesTab from "@/components/collections/CollectionRulesTab.svelte";
2023-02-19 01:33:42 +08:00
import CollectionQueryTab from "@/components/collections/CollectionQueryTab.svelte";
2022-10-30 16:28:14 +08:00
import CollectionAuthOptionsTab from "@/components/collections/CollectionAuthOptionsTab.svelte";
2022-07-07 05:19:05 +08:00
import CollectionUpdateConfirm from "@/components/collections/CollectionUpdateConfirm.svelte";
2023-02-19 01:33:42 +08:00
const TAB_SCHEMA = "schema";
2022-07-07 05:19:05 +08:00
const TAB_RULES = "api_rules";
2022-10-30 16:28:14 +08:00
const TAB_OPTIONS = "options";
const TYPE_BASE = "base";
const TYPE_AUTH = "auth";
2023-02-19 01:33:42 +08:00
const TYPE_VIEW = "view";
2022-10-30 16:28:14 +08:00
const collectionTypes = {};
collectionTypes[TYPE_BASE] = "Base";
2023-02-19 01:33:42 +08:00
collectionTypes[TYPE_VIEW] = "View";
2022-10-30 16:28:14 +08:00
collectionTypes[TYPE_AUTH] = "Auth";
2022-07-07 05:19:05 +08:00
const dispatch = createEventDispatcher();
let collectionPanel;
let confirmChangesPanel;
let original = null;
let collection = new Collection();
let isSaving = false;
let confirmClose = false; // prevent close recursion
2023-02-19 01:33:42 +08:00
let activeTab = TAB_SCHEMA;
2022-07-07 05:19:05 +08:00
let initialFormHash = calculateFormHash(collection);
2023-02-19 01:33:42 +08:00
let schemaTabError = "";
2022-07-07 05:19:05 +08:00
2023-02-19 01:33:42 +08:00
$: if ($errors.schema || $errors.options?.query) {
2022-07-07 05:19:05 +08:00
// extract the direct schema field error, otherwise - return a generic message
2023-02-19 01:33:42 +08:00
schemaTabError = CommonHelper.getNestedVal($errors, "schema.message") || "Has errors";
} else {
schemaTabError = "";
}
2022-07-07 05:19:05 +08:00
$: isSystemUpdate = !collection.isNew && collection.system;
$: hasChanges = initialFormHash != calculateFormHash(collection);
$: canSave = collection.isNew || hasChanges;
2022-10-30 16:28:14 +08:00
$: if (activeTab === TAB_OPTIONS && collection.type !== TYPE_AUTH) {
// reset selected tab
2023-02-19 01:33:42 +08:00
changeTab(TAB_SCHEMA);
2022-10-30 16:28:14 +08:00
}
2022-07-07 05:19:05 +08:00
export function changeTab(newTab) {
activeTab = newTab;
}
export function show(model) {
load(model);
confirmClose = true;
2023-02-19 01:33:42 +08:00
changeTab(TAB_SCHEMA);
2022-07-07 05:19:05 +08:00
return collectionPanel?.show();
}
export function hide() {
return collectionPanel?.hide();
}
async function load(model) {
setErrors({}); // reset errors
2022-07-07 05:19:05 +08:00
if (typeof model !== "undefined") {
original = model;
collection = model?.clone();
} else {
original = null;
collection = new Collection();
}
2022-07-07 05:19:05 +08:00
// normalize
collection.schema = collection.schema || [];
collection.originalName = collection.name || "";
await tick();
initialFormHash = calculateFormHash(collection);
}
function saveWithConfirm() {
if (collection.isNew) {
return save();
} else {
confirmChangesPanel?.show(collection);
}
}
function save() {
if (isSaving) {
return;
}
isSaving = true;
const data = exportFormData();
let request;
if (collection.isNew) {
2022-08-02 22:00:14 +08:00
request = ApiClient.collections.create(data);
2022-07-07 05:19:05 +08:00
} else {
2022-08-02 22:00:14 +08:00
request = ApiClient.collections.update(collection.id, data);
2022-07-07 05:19:05 +08:00
}
request
.then((result) => {
removeAllToasts();
loadCollections(result.id);
2022-07-07 05:19:05 +08:00
confirmClose = false;
hide();
2022-07-07 05:19:05 +08:00
addSuccessToast(
collection.isNew ? "Successfully created collection." : "Successfully updated collection."
);
2022-10-30 16:28:14 +08:00
dispatch("save", {
isNew: collection.isNew,
collection: result,
});
2022-07-07 05:19:05 +08:00
})
.catch((err) => {
ApiClient.errorResponseHandler(err);
})
.finally(() => {
isSaving = false;
});
}
function exportFormData() {
const data = collection.export();
data.schema = data.schema.slice(0);
// remove deleted fields
for (let i = data.schema.length - 1; i >= 0; i--) {
const field = data.schema[i];
if (field.toDelete) {
data.schema.splice(i, 1);
}
}
return data;
}
function deleteConfirm() {
if (!original?.id) {
return; // nothing to delete
}
confirm(`Do you really want to delete collection "${original?.name}" and all its records?`, () => {
return ApiClient.collections
.delete(original?.id)
2022-07-07 05:19:05 +08:00
.then(() => {
hide();
addSuccessToast(`Successfully deleted collection "${original?.name}".`);
dispatch("delete", original);
removeCollection(original);
})
.catch((err) => {
ApiClient.errorResponseHandler(err);
});
});
}
function calculateFormHash(m) {
return JSON.stringify(m);
}
2022-10-30 16:28:14 +08:00
function setCollectionType(t) {
collection.type = t;
// reset schema errors on type change
removeError("schema");
2022-10-30 16:28:14 +08:00
}
function duplicateConfirm() {
if (hasChanges) {
confirm("You have unsaved changes. Do you really want to discard them?", () => {
duplicate();
});
} else {
duplicate();
}
}
async function duplicate() {
const clone = original?.clone();
if (clone) {
clone.id = "";
clone.created = "";
clone.updated = "";
clone.name += "_duplicate";
// reset the schema
if (!CommonHelper.isEmpty(clone.schema)) {
for (const field of clone.schema) {
field.id = "";
}
}
}
show(clone);
await tick();
initialFormHash = "";
}
2022-07-07 05:19:05 +08:00
</script>
<OverlayPanel
bind:this={collectionPanel}
2022-10-30 16:28:14 +08:00
class="overlay-panel-lg colored-header collection-panel"
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;
}
return true;
}}
on:hide
on:show
>
<svelte:fragment slot="header">
<h4 class="upsert-panel-title">
2022-07-07 05:19:05 +08:00
{collection.isNew ? "New collection" : "Edit collection"}
</h4>
{#if !collection.isNew && !collection.system}
<div class="flex-fill" />
<button type="button" aria-label="More" class="btn btn-sm btn-circle btn-transparent flex-gap-0">
2022-07-07 05:19:05 +08:00
<i class="ri-more-line" />
<Toggler class="dropdown dropdown-right m-t-5">
<button type="button" class="dropdown-item closable" on:click={() => duplicateConfirm()}>
<i class="ri-file-copy-line" />
<span class="txt">Duplicate</span>
</button>
2022-10-30 16:28:14 +08:00
<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>
</button>
</Toggler>
</button>
{/if}
<form
class="block"
on:submit|preventDefault={() => {
canSave && saveWithConfirm();
}}
>
<Field
2022-10-30 16:28:14 +08:00
class="form-field collection-field-name required m-b-0 {isSystemUpdate ? 'disabled' : ''}"
2022-07-07 05:19:05 +08:00
name="name"
let:uniqueId
>
<label for={uniqueId}>Name</label>
2022-10-30 16:28:14 +08:00
2022-07-07 05:19:05 +08:00
<!-- svelte-ignore a11y-autofocus -->
<input
type="text"
id={uniqueId}
required
disabled={isSystemUpdate}
spellcheck="false"
autofocus={collection.isNew}
placeholder={collection.isAuth ? `eg. "users"` : `eg. "posts"`}
2022-07-07 05:19:05 +08:00
value={collection.name}
on:input={(e) => {
collection.name = CommonHelper.slugify(e.target.value);
e.target.value = collection.name;
}}
/>
2022-10-30 16:28:14 +08:00
<div class="form-field-addon">
<button
type="button"
class="btn btn-sm p-r-10 p-l-10 {collection.isNew
2023-02-19 01:33:42 +08:00
? 'btn-outline'
: 'btn-transparent'}"
2022-10-30 16:28:14 +08:00
disabled={!collection.isNew}
>
<!-- empty span for alignment -->
<span />
<span class="txt">Type: {collectionTypes[collection.type] || "N/A"}</span>
{#if collection.isNew}
<i class="ri-arrow-down-s-fill" />
<Toggler class="dropdown dropdown-right dropdown-nowrap m-t-5">
{#each Object.entries(collectionTypes) as [type, label]}
<button
type="button"
class="dropdown-item closable"
class:selected={type == collection.type}
on:click={() => setCollectionType(type)}
>
<i class={CommonHelper.getCollectionTypeIcon(type)} />
<span class="txt">{label} collection</span>
</button>
{/each}
</Toggler>
{/if}
</button>
</div>
2022-07-07 05:19:05 +08:00
{#if collection.system}
<div class="help-block">System collection</div>
{/if}
</Field>
<input type="submit" class="hidden" tabindex="-1" />
</form>
<div class="tabs-header stretched">
<button
type="button"
class="tab-item"
2023-02-19 01:33:42 +08:00
class:active={activeTab === TAB_SCHEMA}
on:click={() => changeTab(TAB_SCHEMA)}
2022-07-07 05:19:05 +08:00
>
2023-02-19 01:33:42 +08:00
<span class="txt">{collection?.isView ? "Query" : "Fields"}</span>
{#if !CommonHelper.isEmpty(schemaTabError)}
2022-07-07 05:19:05 +08:00
<i
class="ri-error-warning-fill txt-danger"
transition:scale|local={{ duration: 150, start: 0.7 }}
use:tooltip={schemaTabError}
/>
{/if}
</button>
<button
type="button"
class="tab-item"
class:active={activeTab === TAB_RULES}
on:click={() => changeTab(TAB_RULES)}
>
<span class="txt">API Rules</span>
2022-10-30 16:28:14 +08:00
{#if !CommonHelper.isEmpty($errors?.listRule) || !CommonHelper.isEmpty($errors?.viewRule) || !CommonHelper.isEmpty($errors?.createRule) || !CommonHelper.isEmpty($errors?.updateRule) || !CommonHelper.isEmpty($errors?.deleteRule) || !CommonHelper.isEmpty($errors?.options?.manageRule)}
2022-07-07 05:19:05 +08:00
<i
class="ri-error-warning-fill txt-danger"
transition:scale|local={{ duration: 150, start: 0.7 }}
use:tooltip={"Has errors"}
/>
{/if}
</button>
2022-10-30 16:28:14 +08:00
{#if collection.isAuth}
<button
type="button"
class="tab-item"
class:active={activeTab === TAB_OPTIONS}
on:click={() => changeTab(TAB_OPTIONS)}
>
<span class="txt">Options</span>
{#if !CommonHelper.isEmpty($errors?.options) && !$errors?.options?.manageRule}
<i
class="ri-error-warning-fill txt-danger"
transition:scale|local={{ duration: 150, start: 0.7 }}
use:tooltip={"Has errors"}
/>
{/if}
</button>
{/if}
2022-07-07 05:19:05 +08:00
</div>
</svelte:fragment>
<div class="tabs-content">
<!-- avoid rerendering the fields tab -->
2023-02-19 01:33:42 +08:00
<div class="tab-item" class:active={activeTab === TAB_SCHEMA}>
{#if collection.isView}
<CollectionQueryTab bind:collection />
{:else}
<CollectionFieldsTab bind:collection />
{/if}
2022-07-07 05:19:05 +08:00
</div>
{#if activeTab === TAB_RULES}
<div class="tab-item active">
<CollectionRulesTab bind:collection />
</div>
{/if}
2022-10-30 16:28:14 +08:00
{#if collection.isAuth}
<div class="tab-item" class:active={activeTab === TAB_OPTIONS}>
<CollectionAuthOptionsTab bind:collection />
</div>
{/if}
2022-07-07 05:19:05 +08:00
</div>
<svelte:fragment slot="footer">
<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="button"
class="btn btn-expanded"
class:btn-loading={isSaving}
disabled={!canSave || isSaving}
on:click={() => saveWithConfirm()}
>
<span class="txt">{collection.isNew ? "Create" : "Save changes"}</span>
</button>
</svelte:fragment>
</OverlayPanel>
<CollectionUpdateConfirm bind:this={confirmChangesPanel} on:confirm={() => save()} />
<style>
.upsert-panel-title {
display: inline-flex;
align-items: center;
min-height: var(--smBtnHeight);
}
2023-02-19 01:33:42 +08:00
.tabs-content:focus-within {
z-index: 9; /* autocomplete dropdown overlay fix */
2022-07-07 05:19:05 +08:00
}
</style>