added view collection type

This commit is contained in:
Gani Georgiev
2023-02-18 19:33:42 +02:00
parent 0052e2ab2a
commit a07f67002f
98 changed files with 3259 additions and 829 deletions
@@ -93,6 +93,12 @@
if (!collection?.options.allowOAuth2Auth) {
delete tabs["auth-with-oauth2"];
}
} else if (collection.isView) {
tabs = Object.assign({}, baseTabs);
delete tabs.create;
delete tabs.update;
delete tabs.delete;
delete tabs.realtime;
} else {
tabs = Object.assign({}, baseTabs);
}
@@ -0,0 +1,105 @@
<script>
import { onMount } from "svelte";
import { Collection } from "pocketbase";
import { errors, removeError } from "@/stores/errors";
import CommonHelper from "@/utils/CommonHelper";
import Field from "@/components/base/Field.svelte";
export let collection = new Collection();
let codeEditorComponent;
let isCodeEditorComponentLoading = false;
let schemaErrors = [];
$: checkSchemaErrors($errors);
function checkSchemaErrors(errs) {
schemaErrors = [];
const raw = CommonHelper.getNestedVal(errs, "schema", null);
if (CommonHelper.isEmpty(raw)) {
return;
}
// generic schema error
// ---
if (raw?.message) {
schemaErrors.push(raw?.message);
return;
}
// schema fields error
// ---
const columns = CommonHelper.extractColumnsFromQuery(collection?.options?.query);
// remove base system fields
CommonHelper.removeByValue(columns, "id");
CommonHelper.removeByValue(columns, "created");
CommonHelper.removeByValue(columns, "updated");
for (let idx in raw) {
for (let key in raw[idx]) {
const message = raw[idx][key].message;
const fieldName = columns[idx] || idx;
schemaErrors.push(CommonHelper.sentenize(fieldName + ": " + message));
}
}
}
onMount(async () => {
isCodeEditorComponentLoading = true;
try {
codeEditorComponent = (await import("@/components/base/CodeEditor.svelte")).default;
} catch (err) {
console.warn(err);
}
isCodeEditorComponentLoading = false;
});
</script>
<Field class="form-field required {schemaErrors.length ? 'error' : ''}" name="options.query" let:uniqueId>
<label for={uniqueId}>Select query</label>
{#if isCodeEditorComponentLoading}
<textarea disabled rows="7" placeholder="Loading..." />
{:else}
<svelte:component
this={codeEditorComponent}
id={uniqueId}
placeholder="eg. SELECT id, name from posts"
language="sql"
minHeight="150"
on:change={() => {
if (schemaErrors.length) {
removeError("schema");
}
}}
bind:value={collection.options.query}
/>
{/if}
<div class="help-block">
<ul>
<li>Wildcard (<code>*</code>) columns are not supported.</li>
<li>
The query must have a unique <code>id</code> column.
<br />
If your query doesn't have a suitable one, you can use
<code>(ROW_NUMBER() OVER()) as id</code>.
</li>
</ul>
</div>
{#if schemaErrors.length}
<div class="help-block help-block-error">
<div class="content">
{#each schemaErrors as err}
<p>{err}</p>
{/each}
</div>
</div>
{/if}
</Field>
@@ -1,10 +1,13 @@
<script>
import { slide } from "svelte/transition";
import { Collection } from "pocketbase";
import CommonHelper from "@/utils/CommonHelper";
import RuleField from "@/components/collections/RuleField.svelte";
export let collection = new Collection();
$: fields = CommonHelper.getAllCollectionIdentifiers(collection);
let showFiltersInfo = false;
</script>
@@ -31,15 +34,8 @@
<div class="content">
<p class="m-b-0">The following record fields are available:</p>
<div class="inline-flex flex-gap-5">
<code>id</code>
<code>created</code>
<code>updated</code>
{#each collection.schema as field}
{#if field.type === "relation" || field.type === "user"}
<code>{field.name}.*</code>
{:else}
<code>{field.name}</code>
{/if}
{#each fields as name}
<code>{name}</code>
{/each}
</div>
@@ -84,14 +80,16 @@
<hr class="m-t-sm m-b-sm" />
<RuleField label="View action" formKey="viewRule" {collection} bind:rule={collection.viewRule} />
<hr class="m-t-sm m-b-sm" />
<RuleField label="Create action" formKey="createRule" {collection} bind:rule={collection.createRule} />
{#if !collection?.isView}
<hr class="m-t-sm m-b-sm" />
<RuleField label="Create action" formKey="createRule" {collection} bind:rule={collection.createRule} />
<hr class="m-t-sm m-b-sm" />
<RuleField label="Update action" formKey="updateRule" {collection} bind:rule={collection.updateRule} />
<hr class="m-t-sm m-b-sm" />
<RuleField label="Update action" formKey="updateRule" {collection} bind:rule={collection.updateRule} />
<hr class="m-t-sm m-b-sm" />
<RuleField label="Delete action" formKey="deleteRule" {collection} bind:rule={collection.deleteRule} />
<hr class="m-t-sm m-b-sm" />
<RuleField label="Delete action" formKey="deleteRule" {collection} bind:rule={collection.deleteRule} />
{/if}
{#if collection?.isAuth}
<hr class="m-t-sm m-b-sm" />
@@ -16,6 +16,8 @@
$: deletedFields = collection?.schema.filter((field) => field.id && field.toDelete) || [];
$: showChanges = isCollectionRenamed || !collection?.isView;
export async function show(collectionToCheck) {
collection = collectionToCheck;
@@ -50,8 +52,8 @@
</div>
<div class="content txt-bold">
<p>
If any of the following changes is part of another collection rule or filter, you'll have to
update it manually!
If any of the collection changes is part of another collection rule, filter or view query,
you'll have to update it manually!
</p>
{#if deletedFields.length}
<p>All data associated with the removed fields will be permanently deleted!</p>
@@ -59,36 +61,40 @@
</div>
</div>
<h6>Changes:</h6>
<ul class="changes-list">
{#if isCollectionRenamed}
<li>
<div class="inline-flex">
Renamed collection
<strong class="txt-strikethrough txt-hint">{collection.originalName}</strong>
<i class="ri-arrow-right-line txt-sm" />
<strong class="txt"> {collection.name}</strong>
</div>
</li>
{/if}
{#if showChanges}
<h6>Changes:</h6>
<ul class="changes-list">
{#if isCollectionRenamed}
<li>
<div class="inline-flex">
Renamed collection
<strong class="txt-strikethrough txt-hint">{collection.originalName}</strong>
<i class="ri-arrow-right-line txt-sm" />
<strong class="txt"> {collection.name}</strong>
</div>
</li>
{/if}
{#each renamedFields as field}
<li>
<div class="inline-flex">
Renamed field
<strong class="txt-strikethrough txt-hint">{field.originalName}</strong>
<i class="ri-arrow-right-line txt-sm" />
<strong class="txt"> {field.name}</strong>
</div>
</li>
{/each}
{#if !collection?.isView}
{#each renamedFields as field}
<li>
<div class="inline-flex">
Renamed field
<strong class="txt-strikethrough txt-hint">{field.originalName}</strong>
<i class="ri-arrow-right-line txt-sm" />
<strong class="txt"> {field.name}</strong>
</div>
</li>
{/each}
{#each deletedFields as field}
<li class="txt-danger">
Removed field <span class="txt-bold">{field.name}</span>
</li>
{/each}
</ul>
{#each deletedFields as field}
<li class="txt-danger">
Removed field <span class="txt-bold">{field.name}</span>
</li>
{/each}
{/if}
</ul>
{/if}
<svelte:fragment slot="footer">
<!-- svelte-ignore a11y-autofocus -->
@@ -1,7 +1,7 @@
<script>
import { Collection } from "pocketbase";
import { createEventDispatcher, tick } from "svelte";
import { scale } from "svelte/transition";
import { Collection } from "pocketbase";
import CommonHelper from "@/utils/CommonHelper";
import ApiClient from "@/utils/ApiClient";
import { errors, setErrors, removeError } from "@/stores/errors";
@@ -14,18 +14,21 @@
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 CollectionAuthOptionsTab from "@/components/collections/CollectionAuthOptionsTab.svelte";
import CollectionUpdateConfirm from "@/components/collections/CollectionUpdateConfirm.svelte";
const TAB_FIELDS = "fields";
const TAB_SCHEMA = "schema";
const TAB_RULES = "api_rules";
const TAB_OPTIONS = "options";
const TYPE_BASE = "base";
const TYPE_AUTH = "auth";
const TYPE_VIEW = "view";
const collectionTypes = {};
collectionTypes[TYPE_BASE] = "Base";
collectionTypes[TYPE_VIEW] = "View";
collectionTypes[TYPE_AUTH] = "Auth";
const dispatch = createEventDispatcher();
@@ -37,14 +40,16 @@
let collection = new Collection();
let isSaving = false;
let confirmClose = false; // prevent close recursion
let activeTab = TAB_FIELDS;
let activeTab = TAB_SCHEMA;
let initialFormHash = calculateFormHash(collection);
let schemaTabError = "";
$: schemaTabError =
$: if ($errors.schema || $errors.options?.query) {
// extract the direct schema field error, otherwise - return a generic message
typeof CommonHelper.getNestedVal($errors, "schema.message", null) === "string"
? CommonHelper.getNestedVal($errors, "schema.message")
: "Has errors";
schemaTabError = CommonHelper.getNestedVal($errors, "schema.message") || "Has errors";
} else {
schemaTabError = "";
}
$: isSystemUpdate = !collection.isNew && collection.system;
@@ -54,7 +59,7 @@
$: if (activeTab === TAB_OPTIONS && collection.type !== TYPE_AUTH) {
// reset selected tab
changeTab(TAB_FIELDS);
changeTab(TAB_SCHEMA);
}
export function changeTab(newTab) {
@@ -66,7 +71,7 @@
confirmClose = true;
changeTab(TAB_FIELDS);
changeTab(TAB_SCHEMA);
return collectionPanel?.show();
}
@@ -301,7 +306,7 @@
<button
type="button"
class="btn btn-sm p-r-10 p-l-10 {collection.isNew
? 'btn-secondary'
? 'btn-outline'
: 'btn-transparent'}"
disabled={!collection.isNew}
>
@@ -339,11 +344,11 @@
<button
type="button"
class="tab-item"
class:active={activeTab === TAB_FIELDS}
on:click={() => changeTab(TAB_FIELDS)}
class:active={activeTab === TAB_SCHEMA}
on:click={() => changeTab(TAB_SCHEMA)}
>
<span class="txt">Fields</span>
{#if !CommonHelper.isEmpty($errors?.schema)}
<span class="txt">{collection?.isView ? "Query" : "Fields"}</span>
{#if !CommonHelper.isEmpty(schemaTabError)}
<i
class="ri-error-warning-fill txt-danger"
transition:scale|local={{ duration: 150, start: 0.7 }}
@@ -390,8 +395,12 @@
<div class="tabs-content">
<!-- avoid rerendering the fields tab -->
<div class="tab-item" class:active={activeTab === TAB_FIELDS}>
<CollectionFieldsTab bind:collection />
<div class="tab-item" class:active={activeTab === TAB_SCHEMA}>
{#if collection.isView}
<CollectionQueryTab bind:collection />
{:else}
<CollectionFieldsTab bind:collection />
{/if}
</div>
{#if activeTab === TAB_RULES}
@@ -431,7 +440,7 @@
align-items: center;
min-height: var(--smBtnHeight);
}
.tabs-content {
z-index: 3; /* autocomplete dropdown overlay fix */
.tabs-content:focus-within {
z-index: 9; /* autocomplete dropdown overlay fix */
}
</style>
@@ -8,21 +8,21 @@
let collectionPanel;
let searchTerm = "";
$: if ($collections) {
scrollIntoView();
}
$: normalizedSearch = searchTerm.replace(/\s+/g, "").toLowerCase();
$: hasSearch = searchTerm !== "";
$: filteredCollections = $collections.filter((collection) => {
$: filtered = $collections.filter((collection) => {
return (
collection.id == searchTerm ||
collection.name.replace(/\s+/g, "").toLowerCase().includes(normalizedSearch)
);
});
$: if ($collections) {
scrollIntoView();
}
function selectCollection(collection) {
$activeCollection = collection;
}
@@ -59,9 +59,9 @@
<div
class="sidebar-content"
class:fade={$isCollectionsLoading}
class:sidebar-content-compact={filteredCollections.length > 20}
class:sidebar-content-compact={filtered.length > 20}
>
{#each filteredCollections as collection (collection.id)}
{#each filtered as collection (collection.id)}
<a
href="/collections?collectionId={collection.id}"
class="sidebar-list-item"
@@ -69,7 +69,6 @@
use:link
>
<i class={CommonHelper.getCollectionTypeIcon(collection.type)} />
<span class="txt">{collection.name}</span>
</a>
{:else}
@@ -206,7 +206,7 @@
<div class="grid">
<div class="col-sm-6">
<Field
class="form-field required {field.id ? 'disabled' : ''}"
class="form-field required {field.id ? 'readonly' : ''}"
name="schema.{key}.type"
let:uniqueId
>
@@ -114,9 +114,9 @@
right: 0px;
top: 0px;
min-width: 135px;
padding: 10px 10px;
padding: 10px;
border-top-left-radius: 0;
border-bottom-right-radius: 0;
background: rgba(53, 71, 104, 0.1);
background: rgba(53, 71, 104, 0.08);
}
</style>