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
@@ -142,10 +142,10 @@
<form id={formId} class="grid" autocomplete="off" on:submit|preventDefault={save}>
{#if !admin.isNew}
<Field class="form-field disabled" name="id" let:uniqueId>
<Field class="form-field readonly" name="id" let:uniqueId>
<label for={uniqueId}>
<i class={CommonHelper.getFieldTypeIcon("primary")} />
<span class="txt">ID</span>
<span class="txt">id</span>
</label>
<div class="form-field-addon">
<i
@@ -156,7 +156,7 @@
}}
/>
</div>
<input type="text" id={uniqueId} value={admin.id} disabled />
<input type="text" id={uniqueId} value={admin.id} readonly />
</Field>
{/if}
+39 -2
View File
@@ -53,13 +53,17 @@
closeBracketsKeymap,
} from "@codemirror/autocomplete";
import { html as htmlLang } from "@codemirror/lang-html";
import { sql, SQLDialect } from "@codemirror/lang-sql";
import { javascript as javascriptLang } from "@codemirror/lang-javascript";
// ---
import CommonHelper from "@/utils/CommonHelper";
import { collections } from "@/stores/collections";
const dispatch = createEventDispatcher();
export let id = "";
export let value = "";
export let minHeight = null;
export let maxHeight = null;
export let disabled = false;
export let placeholder = "";
@@ -123,6 +127,8 @@
bubbles: true,
})
);
dispatch("change", value);
}
// Remove any attached label listeners.
@@ -153,7 +159,33 @@
// Returns the current active editor language.
function getEditorLang() {
return language === "html" ? htmlLang() : javascriptLang();
switch (language) {
case "html":
return htmlLang();
case "sql":
let schema = {};
for (let collection of $collections) {
schema[collection.name] = CommonHelper.getAllCollectionIdentifiers(collection);
}
return sql({
// lightweight sql dialect with mostly SELECT statements keywords
dialect: SQLDialect.define({
keywords:
"select from where having group by order limit offset join left right inner with like not in match asc desc regexp isnull notnull glob " +
"count avg sum min max current random cast as int real text " +
"date time datetime unixepoch strftime coalesce lower upper substr " +
"case when then iif if else json_extract json_each json_tree json_array_length json_valid ",
operatorChars: "*+-%<>!=&|/~",
identifierQuotes: '`"',
specialVar: "@:?$",
}),
schema: schema,
upperCaseKeywords: true,
});
default:
return javascriptLang();
}
}
onMount(() => {
@@ -222,4 +254,9 @@
});
</script>
<div bind:this={container} class="code-editor" style:max-height={maxHeight ? maxHeight + "px" : "auto"} />
<div
bind:this={container}
class="code-editor"
style:min-height={minHeight ? minHeight + "px" : null}
style:max-height={maxHeight ? maxHeight + "px" : "auto"}
/>
@@ -217,25 +217,11 @@
return [];
}
let result = [
// base model fields
prefix + "id",
prefix + "created",
prefix + "updated",
];
if (collection.isAuth) {
result.push(prefix + "username");
result.push(prefix + "email");
result.push(prefix + "emailVisibility");
result.push(prefix + "verified");
}
let result = CommonHelper.getAllCollectionIdentifiers(collection, prefix);
for (const field of collection.schema) {
const key = prefix + field.name;
result.push(key);
// add relation fields
if (field.type === "relation" && field.options?.collectionId) {
const subKeys = getCollectionFieldKeys(field.options.collectionId, key + ".", level + 1);
@@ -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>
+1 -1
View File
@@ -24,7 +24,7 @@
<h4>Request log</h4>
</svelte:fragment>
<table class="table-compact table-border">
<table class="table-border">
<tbody>
<tr>
<td class="min-width txt-hint txt-bold">ID</td>
+30 -11
View File
@@ -1,5 +1,6 @@
<script>
import { replace, querystring } from "svelte-spa-router";
import CommonHelper from "@/utils/CommonHelper";
import {
collections,
activeCollection,
@@ -16,16 +17,18 @@
import CollectionUpsertPanel from "@/components/collections/CollectionUpsertPanel.svelte";
import CollectionDocsPanel from "@/components/collections/CollectionDocsPanel.svelte";
import RecordUpsertPanel from "@/components/records/RecordUpsertPanel.svelte";
import RecordPreviewPanel from "@/components/records/RecordPreviewPanel.svelte";
import RecordsList from "@/components/records/RecordsList.svelte";
const queryParams = new URLSearchParams($querystring);
let collectionUpsertPanel;
let collectionDocsPanel;
let recordPanel;
let recordUpsertPanel;
let recordPreviewPanel;
let recordsList;
let filter = queryParams.get("filter") || "";
let sort = queryParams.get("sort") || "-created";
let sort = queryParams.get("sort") || "";
let selectedCollectionId = queryParams.get("collectionId") || $activeCollection?.id;
$: reactiveParams = new URLSearchParams($querystring);
@@ -56,9 +59,17 @@
$: $pageTitle = $activeCollection?.name || "Collections";
function reset() {
selectedCollectionId = $activeCollection.id;
sort = "-created";
selectedCollectionId = $activeCollection?.id;
filter = "";
sort = "-created";
// clear default sort if created field is not available
if (
$activeCollection?.isView &&
!CommonHelper.extractColumnsFromQuery($activeCollection.options.query).includes("created")
) {
sort = "";
}
}
loadCollections(selectedCollectionId);
@@ -128,10 +139,12 @@
<span class="txt">API Preview</span>
</button>
<button type="button" class="btn btn-expanded" on:click={() => recordPanel?.show()}>
<i class="ri-add-line" />
<span class="txt">New record</span>
</button>
{#if !$activeCollection.isView}
<button type="button" class="btn btn-expanded" on:click={() => recordUpsertPanel?.show()}>
<i class="ri-add-line" />
<span class="txt">New record</span>
</button>
{/if}
</div>
</header>
@@ -147,8 +160,12 @@
collection={$activeCollection}
bind:filter
bind:sort
on:select={(e) => recordPanel?.show(e?.detail)}
on:new={() => recordPanel?.show()}
on:select={(e) => {
$activeCollection.isView
? recordPreviewPanel.show(e?.detail)
: recordUpsertPanel?.show(e?.detail);
}}
on:new={() => recordUpsertPanel?.show()}
/>
</PageWrapper>
{/if}
@@ -158,8 +175,10 @@
<CollectionDocsPanel bind:this={collectionDocsPanel} />
<RecordUpsertPanel
bind:this={recordPanel}
bind:this={recordUpsertPanel}
collection={$activeCollection}
on:save={() => recordsList?.reloadLoadedPages()}
on:delete={() => recordsList?.reloadLoadedPages()}
/>
<RecordPreviewPanel bind:this={recordPreviewPanel} collection={$activeCollection} />
@@ -1,82 +0,0 @@
<script>
import CommonHelper from "@/utils/CommonHelper";
import tooltip from "@/actions/tooltip";
import FormattedDate from "@/components/base/FormattedDate.svelte";
import RecordFileThumb from "@/components/records/RecordFileThumb.svelte";
import RecordInfo from "@/components/records/RecordInfo.svelte";
export let record;
export let field;
</script>
<td class="col-type-{field.type} col-field-{field.name}">
{#if field.type === "json"}
<span class="txt txt-ellipsis">
{CommonHelper.truncate(JSON.stringify(record[field.name]))}
</span>
{:else if CommonHelper.isEmpty(record[field.name])}
<span class="txt-hint">N/A</span>
{:else if field.type === "bool"}
<span class="txt">{record[field.name] ? "True" : "False"}</span>
{:else if field.type === "number"}
<span class="txt">{record[field.name]}</span>
{:else if field.type === "url"}
<a
class="txt-ellipsis"
href={record[field.name]}
target="_blank"
rel="noopener noreferrer"
use:tooltip={"Open in new tab"}
on:click|stopPropagation
>
{CommonHelper.truncate(record[field.name])}
</a>
{:else if field.type === "editor"}
<span class="txt">
{CommonHelper.truncate(CommonHelper.plainText(record[field.name]), 300, true)}
</span>
{:else if field.type === "date"}
<FormattedDate date={record[field.name]} />
{:else if field.type === "select"}
<div class="inline-flex">
{#each CommonHelper.toArray(record[field.name]) as item, i (i + item)}
<span class="label">{item}</span>
{/each}
</div>
{:else if field.type === "relation" || field.type === "user"}
{@const relations = CommonHelper.toArray(record[field.name])}
{@const expanded = CommonHelper.toArray(record.expand[field.name])}
<div class="inline-flex">
{#if expanded.length}
{#each expanded.slice(0, 20) as item, i (i + item)}
<span class="label">
<RecordInfo record={item} displayFields={field.options?.displayFields} />
</span>
{/each}
{:else}
{#each relations.slice(0, 20) as id}
<span class="label">{id}</span>
{/each}
{/if}
{#if relations.length > 20}
...
{/if}
</div>
{:else if field.type === "file"}
<div class="inline-flex">
{#each CommonHelper.toArray(record[field.name]) as filename, i (i + filename)}
<RecordFileThumb {record} {filename} size="sm" />
{/each}
</div>
{:else}
<span class="txt txt-ellipsis" title={CommonHelper.truncate(record[field.name])}>
{CommonHelper.truncate(record[field.name])}
</span>
{/if}
</td>
<style>
.filename {
max-width: 200px;
}
</style>
@@ -0,0 +1,107 @@
<script>
import CommonHelper from "@/utils/CommonHelper";
import tooltip from "@/actions/tooltip";
import FormattedDate from "@/components/base/FormattedDate.svelte";
import RecordFileThumb from "@/components/records/RecordFileThumb.svelte";
import RecordInfo from "@/components/records/RecordInfo.svelte";
import TinyMCE from "@tinymce/tinymce-svelte";
export let record;
export let field;
export let short = false;
$: rawValue = record[field.name];
</script>
{#if field.type === "json"}
<span class="txt txt-ellipsis">
{short
? CommonHelper.truncate(JSON.stringify(rawValue))
: CommonHelper.truncate(JSON.stringify(rawValue, null, 2), 2000, true)}
</span>
{:else if CommonHelper.isEmpty(rawValue)}
<span class="txt-hint">N/A</span>
{:else if field.type === "bool"}
<span class="txt">{rawValue ? "True" : "False"}</span>
{:else if field.type === "number"}
<span class="txt">{rawValue}</span>
{:else if field.type === "url"}
<a
class="txt-ellipsis"
href={rawValue}
target="_blank"
rel="noopener noreferrer"
use:tooltip={"Open in new tab"}
on:click|stopPropagation
>
{CommonHelper.truncate(rawValue)}
</a>
{:else if field.type === "editor"}
{#if short}
<span class="txt">
{CommonHelper.truncate(CommonHelper.plainText(rawValue), 250)}
</span>
{:else}
<TinyMCE
scriptSrc="{import.meta.env.BASE_URL}libs/tinymce/tinymce.min.js"
cssClass="tinymce-preview"
conf={{
branding: false,
promotion: false,
menubar: false,
min_height: 30,
statusbar: false,
height: 59,
max_height: 500,
autoresize_bottom_margin: 5,
resize: false,
skin: "pocketbase",
content_style: "body { font-size: 14px }",
toolbar: "",
plugins: ["autoresize"],
}}
value={rawValue}
disabled
/>
{/if}
{:else if field.type === "date"}
<FormattedDate date={rawValue} />
{:else if field.type === "select"}
<div class="inline-flex">
{#each CommonHelper.toArray(rawValue) as item, i (i + item)}
<span class="label">{item}</span>
{/each}
</div>
{:else if field.type === "relation"}
{@const relations = CommonHelper.toArray(rawValue)}
{@const expanded = CommonHelper.toArray(record.expand[field.name])}
{@const relLimit = short ? 20 : 200}
<div class="inline-flex">
{#if expanded.length}
{#each expanded.slice(0, relLimit) as item, i (i + item)}
<span class="label">
<RecordInfo record={item} displayFields={field.options?.displayFields} />
</span>
{/each}
{:else}
{#each relations.slice(0, relLimit) as id}
<span class="label">{id}</span>
{/each}
{/if}
{#if relations.length > relLimit}
...
{/if}
</div>
{:else if field.type === "file"}
<div class="inline-flex">
{#each CommonHelper.toArray(rawValue) as filename, i (i + filename)}
<RecordFileThumb {record} {filename} size="sm" />
{/each}
</div>
{:else if short}
<span class="txt txt-ellipsis" title={CommonHelper.truncate(rawValue)}>
{CommonHelper.truncate(rawValue)}
</span>
{:else}
<span class="block txt-break">{CommonHelper.truncate(rawValue, 2000)}</span>
{/if}
+26 -2
View File
@@ -1,11 +1,23 @@
<script>
import CommonHelper from "@/utils/CommonHelper";
import tooltip from "@/actions/tooltip";
import { collections } from "@/stores/collections";
import RecordFileThumb from "@/components/records/RecordFileThumb.svelte";
export let record;
export let displayFields = [];
$: displayValue = CommonHelper.displayValue(record, displayFields);
$: collection = $collections?.find((item) => item.id == record?.collectionId);
$: fileDisplayFields =
displayFields?.filter((name) => {
return !!collection?.schema?.find((field) => field.name == name && field.type == "file");
}) || [];
$: textDisplayFields =
(!fileDisplayFields.length
? displayFields
: displayFields?.filter((name) => !fileDisplayFields.includes(name))) || [];
</script>
<div class="record-info">
@@ -21,7 +33,16 @@
position: "left",
}}
/>
<span class="txt txt-ellipsis">{CommonHelper.truncate(displayValue, 150)}</span>
{#each fileDisplayFields as name}
{#if record[name]}
<RecordFileThumb {record} filename={record[name]} size="xs" />
{/if}
{/each}
<span class="txt txt-ellipsis">
{CommonHelper.truncate(CommonHelper.displayValue(record, textDisplayFields), 70)}
</span>
</div>
<style lang="scss">
@@ -36,5 +57,8 @@
> * {
line-height: inherit;
}
:global(.thumb) {
box-shadow: none;
}
}
</style>
@@ -0,0 +1,78 @@
<script>
import { Record } from "pocketbase";
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
import RecordFieldValue from "./RecordFieldValue.svelte";
import CopyIcon from "@/components/base/CopyIcon.svelte";
import FormattedDate from "@/components/base/FormattedDate.svelte";
export let collection;
let recordPanel;
let record = new Record();
$: hasEditorField = !!collection?.schema?.find((f) => f.type === "editor");
export function show(model) {
record = model;
return recordPanel?.show();
}
export function hide() {
return recordPanel?.hide();
}
</script>
<OverlayPanel
bind:this={recordPanel}
class="record-preview-panel {hasEditorField ? 'overlay-panel-xl' : 'overlay-panel-lg'}"
on:hide
on:show
>
<svelte:fragment slot="header">
<h4><strong>{collection?.name}</strong> record preview</h4>
</svelte:fragment>
<table class="table-border">
<tbody>
<tr>
<td class="min-width txt-hint txt-bold">id</td>
<td>
<div class="label">
<CopyIcon value={record.id} />
<span class="txt">{record.id}</span>
</div>
</td>
</tr>
{#each collection?.schema as field}
<tr>
<td class="min-width txt-hint txt-bold">{field.name}</td>
<td>
<RecordFieldValue {field} {record} />
</td>
</tr>
{/each}
{#if record.created}
<tr>
<td class="min-width txt-hint txt-bold">created</td>
<td><FormattedDate date={record.created} /></td>
</tr>
{/if}
{#if record.updated}
<tr>
<td class="min-width txt-hint txt-bold">updated</td>
<td><FormattedDate date={record.updated} /></td>
</tr>
{/if}
</tbody>
</table>
<svelte:fragment slot="footer">
<button type="button" class="btn btn-transparent" on:click={() => hide()}>
<span class="txt">Close</span>
</button>
</svelte:fragment>
</OverlayPanel>
@@ -355,7 +355,7 @@
on:submit|preventDefault={save}
>
{#if !record.isNew}
<Field class="form-field disabled" name="id" let:uniqueId>
<Field class="form-field readonly" name="id" let:uniqueId>
<label for={uniqueId}>
<i class={CommonHelper.getFieldTypeIcon("primary")} />
<span class="txt">id</span>
@@ -426,6 +426,7 @@
<button type="button" class="btn btn-transparent" disabled={isSaving} on:click={() => hide()}>
<span class="txt">Cancel</span>
</button>
<button
type="submit"
form={formId}
+83 -68
View File
@@ -12,7 +12,7 @@
import CopyIcon from "@/components/base/CopyIcon.svelte";
import FormattedDate from "@/components/base/FormattedDate.svelte";
import HorizontalScroller from "@/components/base/HorizontalScroller.svelte";
import RecordFieldCell from "@/components/records/RecordFieldCell.svelte";
import RecordFieldValue from "@/components/records/RecordFieldValue.svelte";
const dispatch = createEventDispatcher();
const sortRegex = /^([\+\-])?(\w+)$/;
@@ -57,6 +57,10 @@
updateStoredHiddenColumns();
}
$: hasCreated = !collection?.isView || (records.length > 0 && records[0].created != "");
$: hasUpdated = !collection?.isView || (records.length > 0 && records[0].updated != "");
$: collumnsToHide = [].concat(
collection.isAuth
? [
@@ -67,10 +71,8 @@
fields.map((f) => {
return { id: f.id, name: f.name };
}),
[
{ id: "@created", name: "created" },
{ id: "@updated", name: "updated" },
]
hasCreated ? { id: "@created", name: "created" } : [],
hasUpdated ? { id: "@updated", name: "updated" } : []
);
function updateStoredHiddenColumns() {
@@ -242,48 +244,55 @@
<HorizontalScroller class="table-wrapper">
<svelte:fragment slot="before">
<Toggler class="dropdown dropdown-right dropdown-nowrap columns-dropdown" trigger={columnsTrigger}>
<div class="txt-hint txt-sm p-5 m-b-5">Toggle columns</div>
{#each collumnsToHide as column (column.id + column.name)}
<Field class="form-field form-field-sm form-field-toggle m-0 p-5" let:uniqueId>
<input
type="checkbox"
id={uniqueId}
checked={!hiddenColumns.includes(column.id)}
on:change={(e) => {
if (e.target.checked) {
CommonHelper.removeByValue(hiddenColumns, column.id);
} else {
CommonHelper.pushUnique(hiddenColumns, column.id);
}
hiddenColumns = hiddenColumns;
}}
/>
<label for={uniqueId}>{column.name}</label>
</Field>
{/each}
</Toggler>
{#if columnsTrigger}
<Toggler
class="dropdown dropdown-right dropdown-nowrap columns-dropdown"
trigger={columnsTrigger}
>
<div class="txt-hint txt-sm p-5 m-b-5">Toggle columns</div>
{#each collumnsToHide as column (column.id + column.name)}
<Field class="form-field form-field-sm form-field-toggle m-0 p-5" let:uniqueId>
<input
type="checkbox"
id={uniqueId}
checked={!hiddenColumns.includes(column.id)}
on:change={(e) => {
if (e.target.checked) {
CommonHelper.removeByValue(hiddenColumns, column.id);
} else {
CommonHelper.pushUnique(hiddenColumns, column.id);
}
hiddenColumns = hiddenColumns;
}}
/>
<label for={uniqueId}>{column.name}</label>
</Field>
{/each}
</Toggler>
{/if}
</svelte:fragment>
<table class="table" class:table-loading={isLoading}>
<thead>
<tr>
<th class="bulk-select-col min-width">
{#if isLoading}
<span class="loader loader-sm" />
{:else}
<div class="form-field">
<input
type="checkbox"
id="checkbox_0"
disabled={!records.length}
checked={areAllRecordsSelected}
on:change={() => toggleSelectAllRecords()}
/>
<label for="checkbox_0" />
</div>
{/if}
</th>
{#if !collection.isView}
<th class="bulk-select-col min-width">
{#if isLoading}
<span class="loader loader-sm" />
{:else}
<div class="form-field">
<input
type="checkbox"
id="checkbox_0"
disabled={!records.length}
checked={areAllRecordsSelected}
on:change={() => toggleSelectAllRecords()}
/>
<label for="checkbox_0" />
</div>
{/if}
</th>
{/if}
{#if !hiddenColumns.includes("@id")}
<SortHeader class="col-type-text col-field-id" name="id" bind:sort>
@@ -326,7 +335,7 @@
</SortHeader>
{/each}
{#if !hiddenColumns.includes("@created")}
{#if hasCreated && !hiddenColumns.includes("@created")}
<SortHeader class="col-type-date col-field-created" name="created" bind:sort>
<div class="col-header-content">
<i class={CommonHelper.getFieldTypeIcon("date")} />
@@ -335,7 +344,7 @@
</SortHeader>
{/if}
{#if !hiddenColumns.includes("@updated")}
{#if hasUpdated && !hiddenColumns.includes("@updated")}
<SortHeader class="col-type-date col-field-updated" name="updated" bind:sort>
<div class="col-header-content">
<i class={CommonHelper.getFieldTypeIcon("date")} />
@@ -345,19 +354,21 @@
{/if}
<th class="col-type-action min-width">
<button
bind:this={columnsTrigger}
type="button"
aria-label="Toggle columns"
class="btn btn-sm btn-transparent p-0"
>
<i class="ri-more-line" />
</button>
{#if collumnsToHide.length}
<button
bind:this={columnsTrigger}
type="button"
aria-label="Toggle columns"
class="btn btn-sm btn-transparent p-0"
>
<i class="ri-more-line" />
</button>
{/if}
</th>
</tr>
</thead>
<tbody>
{#each records as record (record.id)}
{#each records as record (!collection.isView ? record.id : record)}
<tr
tabindex="0"
class="row-handle"
@@ -369,18 +380,20 @@
}
}}
>
<td class="bulk-select-col min-width">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="form-field" on:click|stopPropagation>
<input
type="checkbox"
id="checkbox_{record.id}"
checked={bulkSelected[record.id]}
on:change={() => toggleSelectRecord(record)}
/>
<label for="checkbox_{record.id}" />
</div>
</td>
{#if !collection.isView}
<td class="bulk-select-col min-width">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="form-field" on:click|stopPropagation>
<input
type="checkbox"
id="checkbox_{record.id}"
checked={bulkSelected[record.id]}
on:change={() => toggleSelectRecord(record)}
/>
<label for="checkbox_{record.id}" />
</div>
</td>
{/if}
{#if !hiddenColumns.includes("@id")}
<td class="col-type-text col-field-id">
@@ -433,16 +446,18 @@
{/if}
{#each visibleFields as field (field.name)}
<RecordFieldCell {record} {field} />
<td class="col-type-{field.type} col-field-{field.name}">
<RecordFieldValue short {record} {field} />
</td>
{/each}
{#if !hiddenColumns.includes("@created")}
{#if hasCreated && !hiddenColumns.includes("@created")}
<td class="col-type-date col-field-created">
<FormattedDate date={record.created} />
</td>
{/if}
{#if !hiddenColumns.includes("@updated")}
{#if hasUpdated && !hiddenColumns.includes("@updated")}
<td class="col-type-date col-field-updated">
<FormattedDate date={record.updated} />
</td>
+2 -2
View File
@@ -198,7 +198,7 @@
</div>
<div class="col-lg-3">
<Field class="form-field required" name="smtp.tls" let:uniqueId>
<label for={uniqueId}>TLS Encryption</label>
<label for={uniqueId}>TLS encryption</label>
<ObjectSelect
id={uniqueId}
items={tlsOptions}
@@ -208,7 +208,7 @@
</div>
<div class="col-lg-3">
<Field class="form-field" name="smtp.authMethod" let:uniqueId>
<label for={uniqueId}>AUTH Method</label>
<label for={uniqueId}>AUTH method</label>
<ObjectSelect
id={uniqueId}
items={authMethods}
+5 -1
View File
@@ -123,7 +123,7 @@ code {
display: inline-block;
font-family: var(--monospaceFontFamily);
font-style: normal;
font-size: var(--lgFontSize);
font-size: 1em;
line-height: 1.379rem;
padding: 0px 4px;
white-space: nowrap;
@@ -522,6 +522,10 @@ a,
border-radius: inherit;
overflow: hidden;
}
&.thumb-xs {
--thumbSize: 24px;
font-size: 0.85rem;
}
&.thumb-sm {
--thumbSize: 32px;
font-size: 0.92rem;
+20 -20
View File
@@ -1,10 +1,10 @@
/* remixicon */
@font-face {
font-family: 'remixicon';
src: url('../fonts/remixicon/remixicon.woff2?v=1') format('woff2'),
url('../fonts/remixicon/remixicon.woff?v=1') format('woff'),
url('../fonts/remixicon/remixicon.ttf?v=1') format('truetype'),
url('../fonts/remixicon/remixicon.svg?v=1#remixicon') format('svg'); /* iOS 4.1- */
src: url('/fonts/remixicon/remixicon.woff2?v=1') format('woff2'),
url('/fonts/remixicon/remixicon.woff?v=1') format('woff'),
url('/fonts/remixicon/remixicon.ttf?v=1') format('truetype'),
url('/fonts/remixicon/remixicon.svg?v=1#remixicon') format('svg'); /* iOS 4.1- */
font-display: swap;
}
@@ -14,8 +14,8 @@
font-style: normal;
font-weight: 400;
src: local(''),
url('../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
url('/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* source-sans-pro-italic - latin_cyrillic */
@@ -24,8 +24,8 @@
font-style: italic;
font-weight: 400;
src: local(''),
url('../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
url('/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* source-sans-pro-600 - latin_cyrillic */
@@ -34,8 +34,8 @@
font-style: normal;
font-weight: 600;
src: local(''),
url('../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
url('/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* source-sans-pro-600italic - latin_cyrillic */
@@ -44,8 +44,8 @@
font-style: italic;
font-weight: 600;
src: local(''),
url('../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
url('/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* source-sans-pro-700 - latin_cyrillic */
@@ -54,8 +54,8 @@
font-style: normal;
font-weight: 700;
src: local(''),
url('../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
url('/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* source-sans-pro-700italic - latin_cyrillic */
@@ -64,8 +64,8 @@
font-style: italic;
font-weight: 700;
src: local(''),
url('../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
url('/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* jetbrains-mono-regular - latin */
@@ -74,8 +74,8 @@
font-style: normal;
font-weight: 400;
src: local(''),
url('../fonts/jetbrains-mono/jetbrains-mono-v12-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/jetbrains-mono/jetbrains-mono-v12-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
url('/fonts/jetbrains-mono/jetbrains-mono-v12-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('/fonts/jetbrains-mono/jetbrains-mono-v12-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* jetbrains-mono-600 - latin */
@@ -84,6 +84,6 @@
font-style: normal;
font-weight: 600;
src: local(''),
url('../fonts/jetbrains-mono/jetbrains-mono-v12-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/jetbrains-mono/jetbrains-mono-v12-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
url('/fonts/jetbrains-mono/jetbrains-mono-v12-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('/fonts/jetbrains-mono/jetbrains-mono-v12-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
+25 -8
View File
@@ -454,11 +454,10 @@ select {
user-select: none;
font-weight: 600;
color: var(--txtHintColor);
font-size: var(--xsFontSize);
text-transform: uppercase;
font-size: var(--smFontSize);
line-height: 1;
padding-top: 12px;
padding-bottom: 2px;
padding-bottom: 3px;
padding-left: var(--hPadding);
padding-right: var(--hPadding);
border: 0;
@@ -474,8 +473,11 @@ select {
i {
font-size: 0.96rem;
line-height: 1;
margin-top: -2px;
margin-bottom: -2px;
margin-top: -1px;
margin-bottom: -1px;
&:before {
margin: 0;
}
}
}
%input, label {
@@ -550,14 +552,23 @@ select {
margin-left: -2px;
}
}
&.readonly,
&.disabled {
label, %input {
background: var(--baseAlt1Color);
}
> label {
color: var(--txtDisabledColor);
color: var(--txtHintColor);
}
&.required > label:after {
opacity: 0.5;
}
}
&.disabled {
> label {
color: var(--txtDisabledColor);
}
}
// checkbox/radio
input[type="radio"],
@@ -1084,13 +1095,15 @@ select {
// codemirror field
.code-editor {
@extend %input;
display: flex;
flex-direction: column;
width: 100%;
.form-field label ~ & {
padding-bottom: 6px;
padding-top: 4px;
}
.cm-editor {
flex-grow: 1;
border: 0 !important;
outline: none !important;
.cm-line {
@@ -1122,6 +1135,7 @@ select {
}
}
.cm-scroller {
flex-grow: 1;
outline: 0 !important;
font-family: var(--monospaceFontFamily);
font-size: var(--baseFontSize);
@@ -1145,6 +1159,9 @@ select {
background-color: rgba(50, 140, 130, 0.1);
}
}
.ͼf {
color: var(--dangerColor);
}
}
// tinymce field
+28 -6
View File
@@ -17,7 +17,7 @@ table {
vertical-align: middle;
position: relative;
text-align: left;
padding: 5px 10px;
padding: 10px;
border-bottom: 1px solid var(--baseAlt2Color);
&:first-child {
padding-left: 20px;
@@ -191,16 +191,15 @@ table {
}
// styles
&.table-compact {
td, th {
height: auto;
}
}
&.table-border {
border: 1px solid var(--baseAlt2Color);
border-radius: var(--baseRadius);
tr {
background: var(--baseColor);
}
td, th {
height: 45px;
}
th {
background: var(--baseAlt1Color);
}
@@ -209,6 +208,29 @@ table {
border-bottom: 0;
}
}
> tr:first-child,
> :first-child > tr:first-child {
> :first-child {
border-top-left-radius: var(--baseRadius);
}
> :last-child {
border-top-right-radius: var(--baseRadius);
}
}
> tr:last-child,
> :last-child > tr:last-child {
> :first-child {
border-bottom-left-radius: var(--baseRadius);
}
> :last-child {
border-bottom-right-radius: var(--baseRadius);
}
}
}
&.table-compact {
td, th {
height: auto;
}
}
// states
+4 -4
View File
@@ -18,13 +18,13 @@
--baseAlt4Color: #a5b0c0;
--infoColor: #3da9fc;
--infoAltColor: #d8eefe;
--successColor: #2cb67d;
--successAltColor: #d6f5e8;
--infoAltColor: #d2ecfe;
--successColor: #2aac76;
--successAltColor: #d2f4e6;
--dangerColor: #e13756;
--dangerAltColor: #fcdee4;
--warningColor: #ff8e3c;
--warningAltColor: #ffe7d6;
--warningAltColor: #ffeadb;
--overlayColor: rgba(53, 71, 104, 0.25);
--tooltipColor: rgba(0, 0, 0, 0.85);
+79 -6
View File
@@ -475,11 +475,12 @@ export default class CommonHelper {
/**
* Truncates the provided text to the specified max characters length.
*
* @param {String} str
* @param {Number} length
* @param {String} str
* @param {Number} [length]
* @param {Boolean} [dots]
* @return {String}
*/
static truncate(str, length = 150, dots = false) {
static truncate(str, length = 150, dots = true) {
str = str || "";
if (str.length <= length) {
@@ -979,8 +980,8 @@ export default class CommonHelper {
switch (type?.toLowerCase()) {
case "auth":
return "ri-group-line";
case "single":
return "ri-file-list-2-line";
case "view":
return "ri-terminal-box-line";
default:
return "ri-folder-2-line";
}
@@ -1157,7 +1158,7 @@ export default class CommonHelper {
for (const collection of collections) {
if (collection.type === 'auth') {
authCollections.push(collection);
} else if (collection.type === 'single') {
} else if (collection.type === 'base') {
singleCollections.push(collection);
} else {
baseCollections.push(collection);
@@ -1318,4 +1319,76 @@ export default class CommonHelper {
return missingValue;
}
/**
* Rudimentary SELECT query columns extractor.
* Returns an array with the identifier aliases
* (expressions wrapped in parenthesis are skipped).
*
* @param {String} selectQuery
* @return {Array}
*/
static extractColumnsFromQuery(selectQuery) {
const groupReplacement = "__GROUP__";
selectQuery = (selectQuery || "").
// replace parenthesis/group expessions
replace(/\([\s\S]+?\)/gm, groupReplacement).
// replace multi-whitespace characters with single space
replace(/[\t\r\n]|(?:\s\s)+/g, " ");
const match = selectQuery.match(/select\s+([\s\S]+)\s+from/);
const expressions = match?.[1]?.split(",") || [];
const result = [];
for (let expr of expressions) {
const column = expr.trim().split(" ").pop(); // get only the alias
if (column != "" && column != groupReplacement) {
result.push(column);
}
}
return result;
}
/**
* Returns an array with all public collection identifiers (schema + type specific fields).
*
* @param {[type]} collection The collection to extract identifiers from.
* @param {String} prefix Optional prefix for each found identified.
* @return {Array}
*/
static getAllCollectionIdentifiers(collection, prefix = "") {
if (!collection) {
return;
}
let result = [prefix + "id"];
if (collection.isView) {
for (let col of CommonHelper.extractColumnsFromQuery(collection.options.query)) {
CommonHelper.pushUnique(result, prefix + col);
}
} else if (collection.isAuth) {
result.push(prefix + "username");
result.push(prefix + "email");
result.push(prefix + "emailVisibility");
result.push(prefix + "verified");
result.push(prefix + "created");
result.push(prefix + "updated");
} else {
result.push(prefix + "created");
result.push(prefix + "updated");
}
const schema = collection.schema || [];
for (const field of schema) {
CommonHelper.pushUnique(result, prefix + field.name);
}
return result;
}
}