added view collection type
This commit is contained in:
@@ -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}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user