merge v0.23.0-rc changes
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
<script>
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import { scale } from "svelte/transition";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import { errors, setErrors, removeError } from "@/stores/errors";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { removeAllToasts, addSuccessToast } from "@/stores/toasts";
|
||||
import { addCollection, removeCollection } from "@/stores/collections";
|
||||
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";
|
||||
import CollectionQueryTab from "@/components/collections/CollectionQueryTab.svelte";
|
||||
import Toggler from "@/components/base/Toggler.svelte";
|
||||
import CollectionAuthOptionsTab from "@/components/collections/CollectionAuthOptionsTab.svelte";
|
||||
import CollectionFieldsTab from "@/components/collections/CollectionFieldsTab.svelte";
|
||||
import CollectionQueryTab from "@/components/collections/CollectionQueryTab.svelte";
|
||||
import CollectionRulesTab from "@/components/collections/CollectionRulesTab.svelte";
|
||||
import CollectionUpdateConfirm from "@/components/collections/CollectionUpdateConfirm.svelte";
|
||||
import { addCollection, removeCollection, scaffolds, activeCollection } from "@/stores/collections";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { errors, removeError, setErrors } from "@/stores/errors";
|
||||
import { addSuccessToast, removeAllToasts } from "@/stores/toasts";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import { scale } from "svelte/transition";
|
||||
|
||||
const TAB_SCHEMA = "schema";
|
||||
const TAB_RULES = "api_rules";
|
||||
@@ -35,26 +35,31 @@
|
||||
let collectionPanel;
|
||||
let confirmChangesPanel;
|
||||
let original = null;
|
||||
let collection = CommonHelper.initCollection();
|
||||
let collection = {};
|
||||
let isSaving = false;
|
||||
let confirmClose = false; // prevent close recursion
|
||||
let activeTab = TAB_SCHEMA;
|
||||
let initialFormHash = calculateFormHash(collection);
|
||||
let schemaTabError = "";
|
||||
let fieldsTabError = "";
|
||||
let baseCollectionKeys = [];
|
||||
|
||||
$: baseCollectionKeys = Object.keys($scaffolds["base"] || {});
|
||||
|
||||
$: isAuth = collection.type === TYPE_AUTH;
|
||||
|
||||
$: isView = collection.type === TYPE_VIEW;
|
||||
|
||||
$: if ($errors.schema || $errors.options?.query) {
|
||||
// extract the direct schema field error, otherwise - return a generic message
|
||||
schemaTabError = CommonHelper.getNestedVal($errors, "schema.message") || "Has errors";
|
||||
$: if ($errors.fields || $errors.viewQuery) {
|
||||
// extract the direct fields list error, otherwise - return a generic message
|
||||
fieldsTabError = CommonHelper.getNestedVal($errors, "fields.message") || "Has errors";
|
||||
} else {
|
||||
schemaTabError = "";
|
||||
fieldsTabError = "";
|
||||
}
|
||||
|
||||
$: isSystemUpdate = !!collection.id && collection.system;
|
||||
|
||||
$: isSuperusers = !!collection.id && collection.system && collection.name == "_superusers";
|
||||
|
||||
$: hasChanges = initialFormHash != calculateFormHash(collection);
|
||||
|
||||
$: canSave = !collection.id || hasChanges;
|
||||
@@ -110,27 +115,40 @@
|
||||
collection = structuredClone(model);
|
||||
} else {
|
||||
original = null;
|
||||
collection = CommonHelper.initCollection();
|
||||
collection = structuredClone($scaffolds["base"]);
|
||||
|
||||
// add default timestamp fields
|
||||
collection.fields.push({
|
||||
type: "autodate",
|
||||
name: "created",
|
||||
onCreate: true,
|
||||
});
|
||||
collection.fields.push({
|
||||
type: "autodate",
|
||||
name: "updated",
|
||||
onCreate: true,
|
||||
onUpdate: true,
|
||||
});
|
||||
}
|
||||
|
||||
// normalize
|
||||
collection.schema = collection.schema || [];
|
||||
collection.originalName = collection.name || "";
|
||||
collection.fields = collection.fields || [];
|
||||
collection._originalName = collection.name || "";
|
||||
|
||||
await tick();
|
||||
|
||||
initialFormHash = calculateFormHash(collection);
|
||||
}
|
||||
|
||||
function saveConfirm() {
|
||||
function saveConfirm(hideAfterSave = true) {
|
||||
if (!collection.id) {
|
||||
save();
|
||||
save(hideAfterSave);
|
||||
} else {
|
||||
confirmChangesPanel?.show(original, collection);
|
||||
confirmChangesPanel?.show(original, collection, hideAfterSave);
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
function save(hideAfterSave = true) {
|
||||
if (isSaving) {
|
||||
return;
|
||||
}
|
||||
@@ -138,9 +156,10 @@
|
||||
isSaving = true;
|
||||
|
||||
const data = exportFormData();
|
||||
const isNew = !collection.id;
|
||||
|
||||
let request;
|
||||
if (!collection.id) {
|
||||
if (isNew) {
|
||||
request = ApiClient.collections.create(data);
|
||||
} else {
|
||||
request = ApiClient.collections.update(collection.id, data);
|
||||
@@ -152,17 +171,25 @@
|
||||
|
||||
addCollection(result);
|
||||
|
||||
confirmClose = false;
|
||||
hide();
|
||||
if (hideAfterSave) {
|
||||
confirmClose = false;
|
||||
hide();
|
||||
} else {
|
||||
load(result);
|
||||
}
|
||||
|
||||
addSuccessToast(
|
||||
!collection.id ? "Successfully created collection." : "Successfully updated collection.",
|
||||
);
|
||||
|
||||
dispatch("save", {
|
||||
isNew: !collection.id,
|
||||
isNew: isNew,
|
||||
collection: result,
|
||||
});
|
||||
|
||||
if (isNew) {
|
||||
$activeCollection = result;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.error(err);
|
||||
@@ -174,19 +201,41 @@
|
||||
|
||||
function exportFormData() {
|
||||
const data = Object.assign({}, collection);
|
||||
data.schema = data.schema.slice(0);
|
||||
data.fields = data.fields.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);
|
||||
for (let i = data.fields.length - 1; i >= 0; i--) {
|
||||
const field = data.fields[i];
|
||||
if (field._toDelete) {
|
||||
data.fields.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function truncateConfirm() {
|
||||
if (!original?.id) {
|
||||
return; // nothing to truncate
|
||||
}
|
||||
|
||||
confirm(
|
||||
`Do you really want to delete all "${original.name}" records, including their cascade delete references and files?`,
|
||||
() => {
|
||||
return ApiClient.collections
|
||||
.truncate(original.id)
|
||||
.then(() => {
|
||||
forceHide();
|
||||
addSuccessToast(`Successfully truncated collection "${original.name}".`);
|
||||
dispatch("truncate");
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.error(err);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function deleteConfirm() {
|
||||
if (!original?.id) {
|
||||
return; // nothing to delete
|
||||
@@ -196,7 +245,7 @@
|
||||
return ApiClient.collections
|
||||
.delete(original.id)
|
||||
.then(() => {
|
||||
hide();
|
||||
forceHide();
|
||||
addSuccessToast(`Successfully deleted collection "${original.name}".`);
|
||||
dispatch("delete", original);
|
||||
removeCollection(original);
|
||||
@@ -214,8 +263,11 @@
|
||||
function setCollectionType(t) {
|
||||
collection.type = t;
|
||||
|
||||
// reset schema errors on type change
|
||||
removeError("schema");
|
||||
// merge with the scaffold to ensure that the minimal props are set
|
||||
collection = Object.assign(structuredClone($scaffolds[t]), collection);
|
||||
|
||||
// reset fields list errors on type change
|
||||
removeError("fields");
|
||||
}
|
||||
|
||||
function duplicateConfirm() {
|
||||
@@ -237,9 +289,9 @@
|
||||
clone.updated = "";
|
||||
clone.name += "_duplicate";
|
||||
|
||||
// reset the schema
|
||||
if (!CommonHelper.isEmpty(clone.schema)) {
|
||||
for (const field of clone.schema) {
|
||||
// reset the fields list
|
||||
if (!CommonHelper.isEmpty(clone.fields)) {
|
||||
for (const field of clone.fields) {
|
||||
field.id = "";
|
||||
}
|
||||
}
|
||||
@@ -248,7 +300,7 @@
|
||||
if (!CommonHelper.isEmpty(clone.indexes)) {
|
||||
for (let i = 0; i < clone.indexes.length; i++) {
|
||||
const parsed = CommonHelper.parseIndex(clone.indexes[i]);
|
||||
parsed.indexName = "idx_" + CommonHelper.randomString(7);
|
||||
parsed.indexName = "idx_" + CommonHelper.randomString(10);
|
||||
parsed.tableName = clone.name;
|
||||
clone.indexes[i] = CommonHelper.buildIndex(parsed);
|
||||
}
|
||||
@@ -261,9 +313,25 @@
|
||||
|
||||
initialFormHash = "";
|
||||
}
|
||||
|
||||
function hasOtherKeys(obj, excludes = []) {
|
||||
if (CommonHelper.isEmpty(obj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const errorKeys = Object.keys(obj);
|
||||
for (let key of errorKeys) {
|
||||
if (!excludes.includes(key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<OverlayPanel
|
||||
bind:this={collectionPanel}
|
||||
class="overlay-panel-lg colored-header collection-panel"
|
||||
@@ -306,6 +374,16 @@
|
||||
<i class="ri-file-copy-line" aria-hidden="true" />
|
||||
<span class="txt">Duplicate</span>
|
||||
</button>
|
||||
<hr />
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item txt-danger"
|
||||
role="menuitem"
|
||||
on:click={() => truncateConfirm()}
|
||||
>
|
||||
<i class="ri-eraser-line" aria-hidden="true"></i>
|
||||
<span class="txt">Truncate</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item txt-danger"
|
||||
@@ -325,11 +403,7 @@
|
||||
canSave && saveConfirm();
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
class="form-field collection-field-name required m-b-0 {isSystemUpdate ? 'disabled' : ''}"
|
||||
name="name"
|
||||
let:uniqueId
|
||||
>
|
||||
<Field class="form-field collection-field-name required m-b-0" name="name" let:uniqueId>
|
||||
<label for={uniqueId}>Name</label>
|
||||
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
@@ -339,6 +413,7 @@
|
||||
required
|
||||
disabled={isSystemUpdate}
|
||||
spellcheck="false"
|
||||
class:txt-bold={collection.system}
|
||||
autofocus={!collection.id}
|
||||
placeholder={isAuth ? `eg. "users"` : `eg. "posts"`}
|
||||
value={collection.name}
|
||||
@@ -398,30 +473,32 @@
|
||||
on:click={() => changeTab(TAB_SCHEMA)}
|
||||
>
|
||||
<span class="txt">{isView ? "Query" : "Fields"}</span>
|
||||
{#if !CommonHelper.isEmpty(schemaTabError)}
|
||||
{#if !CommonHelper.isEmpty(fieldsTabError)}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={schemaTabError}
|
||||
use:tooltip={fieldsTabError}
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="tab-item"
|
||||
class:active={activeTab === TAB_RULES}
|
||||
on:click={() => changeTab(TAB_RULES)}
|
||||
>
|
||||
<span class="txt">API Rules</span>
|
||||
{#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)}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={"Has errors"}
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
{#if !isSuperusers}
|
||||
<button
|
||||
type="button"
|
||||
class="tab-item"
|
||||
class:active={activeTab === TAB_RULES}
|
||||
on:click={() => changeTab(TAB_RULES)}
|
||||
>
|
||||
<span class="txt">API Rules</span>
|
||||
{#if !CommonHelper.isEmpty($errors?.listRule) || !CommonHelper.isEmpty($errors?.viewRule) || !CommonHelper.isEmpty($errors?.createRule) || !CommonHelper.isEmpty($errors?.updateRule) || !CommonHelper.isEmpty($errors?.deleteRule) || !CommonHelper.isEmpty($errors?.authRule) || !CommonHelper.isEmpty($errors?.manageRule)}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={"Has errors"}
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if isAuth}
|
||||
<button
|
||||
@@ -431,7 +508,7 @@
|
||||
on:click={() => changeTab(TAB_OPTIONS)}
|
||||
>
|
||||
<span class="txt">Options</span>
|
||||
{#if !CommonHelper.isEmpty($errors?.options) && !$errors?.options?.manageRule}
|
||||
{#if $errors && hasOtherKeys($errors, baseCollectionKeys.concat( ["manageRule", "authRule"], ))}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
@@ -453,7 +530,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if activeTab === TAB_RULES}
|
||||
{#if !isSuperusers && activeTab === TAB_RULES}
|
||||
<div class="tab-item active">
|
||||
<CollectionRulesTab bind:collection />
|
||||
</div>
|
||||
@@ -470,19 +547,40 @@
|
||||
<button type="button" class="btn btn-transparent" disabled={isSaving} on:click={() => hide()}>
|
||||
<span class="txt">Cancel</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-expanded"
|
||||
class:btn-loading={isSaving}
|
||||
disabled={!canSave || isSaving}
|
||||
on:click={() => saveConfirm()}
|
||||
>
|
||||
<span class="txt">{!collection.id ? "Create" : "Save changes"}</span>
|
||||
</button>
|
||||
|
||||
<div class="btns-group no-gap">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-expanded"
|
||||
title="Save and close"
|
||||
class:btn-loading={isSaving}
|
||||
disabled={!canSave || isSaving}
|
||||
on:click={() => saveConfirm()}
|
||||
>
|
||||
<span class="txt">{!collection.id ? "Create" : "Save changes"}</span>
|
||||
</button>
|
||||
|
||||
{#if collection.id}
|
||||
<button type="button" class="btn p-l-5 p-r-5 flex-gap-0" disabled={!canSave || isSaving}>
|
||||
<i class="ri-arrow-down-s-line" aria-hidden="true"></i>
|
||||
|
||||
<Toggler class="dropdown dropdown-upside dropdown-right dropdown-nowrap m-b-5">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
role="menuitem"
|
||||
on:click={() => saveConfirm(false)}
|
||||
>
|
||||
<span class="txt">Save and continue</span>
|
||||
</button>
|
||||
</Toggler>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</OverlayPanel>
|
||||
|
||||
<CollectionUpdateConfirm bind:this={confirmChangesPanel} on:confirm={() => save()} />
|
||||
<CollectionUpdateConfirm bind:this={confirmChangesPanel} on:confirm={(e) => save(e.detail)} />
|
||||
|
||||
<style>
|
||||
.upsert-panel-title {
|
||||
@@ -493,4 +591,8 @@
|
||||
.tabs-content:focus-within {
|
||||
z-index: 9; /* autocomplete dropdown overlay fix */
|
||||
}
|
||||
:global(.collection-panel .panel-content) {
|
||||
scrollbar-gutter: stable;
|
||||
padding-right: calc(var(--baseSpacing) - 5px);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user