initial public commit
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import FieldAccordion from "@/components/collections/FieldAccordion.svelte";
|
||||
|
||||
const reservedNames = ["id", "created", "updated"];
|
||||
|
||||
export let collection = {};
|
||||
|
||||
$: if (typeof collection?.schema === "undefined") {
|
||||
collection = collection || {};
|
||||
collection.schema = [];
|
||||
}
|
||||
|
||||
function removeField(fieldIndex) {
|
||||
if (collection.schema[fieldIndex]) {
|
||||
collection.schema.splice(fieldIndex, 1);
|
||||
collection.schema = collection.schema;
|
||||
}
|
||||
}
|
||||
|
||||
function newField() {
|
||||
const field = new SchemaField({
|
||||
name: getUniqueFieldName(),
|
||||
});
|
||||
|
||||
collection.schema.push(field);
|
||||
collection.schema = collection.schema;
|
||||
}
|
||||
|
||||
function getUniqueFieldName(base = "field") {
|
||||
let counter = "";
|
||||
|
||||
while (hasFieldWithName(base + counter)) {
|
||||
++counter;
|
||||
}
|
||||
|
||||
return base + counter;
|
||||
}
|
||||
|
||||
function hasFieldWithName(name) {
|
||||
return !!collection.schema.find((field) => field.name === name);
|
||||
}
|
||||
|
||||
function getSiblingsFieldNames(currentField) {
|
||||
let result = [];
|
||||
|
||||
for (let field of collection.schema) {
|
||||
if (field === currentField) {
|
||||
continue; // skip current
|
||||
}
|
||||
|
||||
result.push(field.name);
|
||||
|
||||
if (field.id && field.originalName !== "" && field.originalName !== field.name) {
|
||||
result.push(field.originalName);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="accordions">
|
||||
{#each collection.schema as field, i (i)}
|
||||
<FieldAccordion
|
||||
bind:field
|
||||
key={i}
|
||||
excludeNames={reservedNames.concat(getSiblingsFieldNames(field))}
|
||||
on:remove={() => removeField(i)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="clearfix m-t-xs" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-block {collection.schema?.length ? 'btn-secondary' : 'btn-success'}"
|
||||
on:click={newField}
|
||||
>
|
||||
<i class="ri-add-line" />
|
||||
<span class="txt">New field</span>
|
||||
</button>
|
||||
@@ -0,0 +1,188 @@
|
||||
<script>
|
||||
import { onMount, tick } from "svelte";
|
||||
import { slide } from "svelte/transition";
|
||||
import { Collection } from "pocketbase";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let tempValues = {};
|
||||
let showFiltersInfo = false;
|
||||
let editorRefs = {};
|
||||
let ruleInputComponent;
|
||||
let isRuleComponentLoading = false;
|
||||
|
||||
// all supported collection rules in "collection_rule_prop: label" format
|
||||
const ruleProps = {
|
||||
listRule: "List Action",
|
||||
viewRule: "View Action",
|
||||
createRule: "Create Action",
|
||||
updateRule: "Update Action",
|
||||
deleteRule: "Delete Action",
|
||||
};
|
||||
|
||||
function isAdminOnly(propVal) {
|
||||
return propVal === null;
|
||||
}
|
||||
|
||||
async function loadEditorComponent() {
|
||||
isRuleComponentLoading = true;
|
||||
try {
|
||||
ruleInputComponent = (await import("@/components/base/FilterAutocompleteInput.svelte")).default;
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
ruleInputComponent = null;
|
||||
}
|
||||
isRuleComponentLoading = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadEditorComponent();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="block m-b-base">
|
||||
<div class="flex">
|
||||
<p>
|
||||
All rules follow the
|
||||
<a href={import.meta.env.PB_RULES_SYNTAX_DOCS} target="_blank" rel="noopener">
|
||||
PocketBase filter syntax and operators
|
||||
</a>.
|
||||
</p>
|
||||
<span
|
||||
class="expand-handle txt-sm txt-bold txt-nowrap link-hint"
|
||||
on:click={() => (showFiltersInfo = !showFiltersInfo)}
|
||||
>
|
||||
{showFiltersInfo ? "Hide available fields" : "Show available fields"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if showFiltersInfo}
|
||||
<div transition:slide|local={{ duration: 150 }}>
|
||||
<div class="alert alert-warning m-0">
|
||||
<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}
|
||||
</div>
|
||||
|
||||
<hr class="m-t-10 m-b-5" />
|
||||
|
||||
<p class="m-b-0">
|
||||
The request fields could be accessed with the special <em>@request</em> filter:
|
||||
</p>
|
||||
<div class="inline-flex flex-gap-5">
|
||||
<code>@request.method</code>
|
||||
<code>@request.query.*</code>
|
||||
<code>@request.data.*</code>
|
||||
<code>@request.user.*</code>
|
||||
</div>
|
||||
|
||||
<hr class="m-t-10 m-b-5" />
|
||||
|
||||
<p class="m-b-0">
|
||||
You could also add constraints and query other collections using the <em
|
||||
>@collection</em
|
||||
> filter:
|
||||
</p>
|
||||
<div class="inline-flex flex-gap-5">
|
||||
<code>@collection.ANY_COLLECTION_NAME.*</code>
|
||||
</div>
|
||||
|
||||
<hr class="m-t-10 m-b-5" />
|
||||
|
||||
<p>
|
||||
Example rule:
|
||||
<br />
|
||||
<code>@request.user.id!=null && created>"2022-01-01 00:00:00"</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isRuleComponentLoading}
|
||||
<div class="txt-center">
|
||||
<span class="loader" />
|
||||
</div>
|
||||
{:else}
|
||||
{#each Object.entries(ruleProps) as [prop, label] (prop)}
|
||||
<hr class="m-t-sm m-b-sm" />
|
||||
<div class="rule-block">
|
||||
{#if isAdminOnly(collection[prop])}
|
||||
<button
|
||||
type="button"
|
||||
class="rule-toggle-btn btn btn-circle btn-outline btn-success"
|
||||
use:tooltip={"Unlock and set custom rule"}
|
||||
on:click={async () => {
|
||||
collection[prop] = tempValues[prop] || "";
|
||||
await tick();
|
||||
editorRefs[prop]?.focus();
|
||||
}}
|
||||
>
|
||||
<i class="ri-lock-unlock-line" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="rule-toggle-btn btn btn-circle btn-outline"
|
||||
use:tooltip={"Lock and set to Admins only"}
|
||||
on:click={() => {
|
||||
tempValues[prop] = collection[prop];
|
||||
collection[prop] = null;
|
||||
}}
|
||||
>
|
||||
<i class="ri-lock-line" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<Field
|
||||
class="form-field rule-field m-0 {isAdminOnly(collection[prop]) ? 'disabled' : ''}"
|
||||
name={prop}
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId} on:click={() => editorRefs[prop]?.focus()}>
|
||||
{label} - {isAdminOnly(collection[prop]) ? "Admins only" : "Custom rule"}
|
||||
</label>
|
||||
|
||||
<svelte:component
|
||||
this={ruleInputComponent}
|
||||
bind:this={editorRefs[prop]}
|
||||
bind:value={collection[prop]}
|
||||
baseCollection={collection}
|
||||
disabled={isAdminOnly(collection[prop])}
|
||||
/>
|
||||
|
||||
<div class="help-block">
|
||||
{#if isAdminOnly(collection[prop])}
|
||||
Only admins will be able to access (unlock to change)
|
||||
{:else}
|
||||
Leave empty to grant everyone access
|
||||
{/if}
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.rule-block {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--xsSpacing);
|
||||
}
|
||||
.rule-toggle-btn {
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<script>
|
||||
import { tick, createEventDispatcher } from "svelte";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let panel;
|
||||
let collection;
|
||||
|
||||
$: isCollectionRenamed = collection?.originalName != collection?.name;
|
||||
|
||||
$: renamedFields =
|
||||
collection?.schema.filter(
|
||||
(field) => field.id && !field.toDelete && field.originalName != field.name
|
||||
) || [];
|
||||
|
||||
$: deletedFields = collection?.schema.filter((field) => field.id && field.toDelete) || [];
|
||||
|
||||
export async function show(collectionToCheck) {
|
||||
collection = collectionToCheck;
|
||||
|
||||
await tick();
|
||||
|
||||
if (!isCollectionRenamed && !renamedFields.length && !deletedFields.length) {
|
||||
// no confirm required changes
|
||||
confirm();
|
||||
} else {
|
||||
panel?.show();
|
||||
}
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
panel?.hide();
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
hide();
|
||||
dispatch("confirm");
|
||||
}
|
||||
</script>
|
||||
|
||||
<OverlayPanel bind:this={panel} class="confirm-changes-panel" popup on:hide on:show>
|
||||
<svelte:fragment slot="header">
|
||||
<h4>Confirm collection changes</h4>
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<div class="icon">
|
||||
<i class="ri-error-warning-line" />
|
||||
</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!
|
||||
</p>
|
||||
{#if deletedFields.length}
|
||||
<p>All data associated with the removed fields will be permanently deleted!</p>
|
||||
{/if}
|
||||
</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}
|
||||
|
||||
{#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>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<button autofocus type="button" class="btn btn-secondary" on:click={() => hide()}>
|
||||
<span class="txt">Cancel</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-expanded" on:click={() => confirm()}>
|
||||
<span class="txt">Confirm</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</OverlayPanel>
|
||||
|
||||
<style>
|
||||
.changes-list {
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,306 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import { scale } from "svelte/transition";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import { errors, setErrors } from "@/stores/errors";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import { addCollection, removeCollection, activeCollection } from "@/stores/collections";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import Toggler from "@/components/base/Toggler.svelte";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
import CollectionFieldsTab from "@/components/collections/CollectionFieldsTab.svelte";
|
||||
import CollectionRulesTab from "@/components/collections/CollectionRulesTab.svelte";
|
||||
import CollectionUpdateConfirm from "@/components/collections/CollectionUpdateConfirm.svelte";
|
||||
|
||||
const TAB_FIELDS = "fields";
|
||||
const TAB_RULES = "api_rules";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let collectionPanel;
|
||||
let confirmChangesPanel;
|
||||
|
||||
let original = null;
|
||||
let collection = new Collection();
|
||||
let isSaving = false;
|
||||
let confirmClose = false; // prevent close recursion
|
||||
let activeTab = TAB_FIELDS;
|
||||
let initialFormHash = calculateFormHash(collection);
|
||||
|
||||
$: schemaTabError =
|
||||
// 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";
|
||||
|
||||
$: isSystemUpdate = !collection.isNew && collection.system;
|
||||
|
||||
$: hasChanges = initialFormHash != calculateFormHash(collection);
|
||||
|
||||
$: canSave = collection.isNew || hasChanges;
|
||||
|
||||
export function changeTab(newTab) {
|
||||
activeTab = newTab;
|
||||
}
|
||||
|
||||
export function show(model) {
|
||||
load(model);
|
||||
|
||||
confirmClose = true;
|
||||
|
||||
changeTab(TAB_FIELDS);
|
||||
|
||||
return collectionPanel?.show();
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
return collectionPanel?.hide();
|
||||
}
|
||||
|
||||
async function load(model) {
|
||||
setErrors({}); // reset errors
|
||||
if (typeof model !== "undefined") {
|
||||
original = model;
|
||||
collection = model?.clone();
|
||||
} else {
|
||||
original = null;
|
||||
collection = new Collection();
|
||||
}
|
||||
// normalize
|
||||
collection.schema = collection.schema || [];
|
||||
collection.originalName = collection.name || "";
|
||||
|
||||
await tick();
|
||||
|
||||
initialFormHash = calculateFormHash(collection);
|
||||
}
|
||||
|
||||
function saveWithConfirm() {
|
||||
if (collection.isNew) {
|
||||
return save();
|
||||
} else {
|
||||
confirmChangesPanel?.show(collection);
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (isSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSaving = true;
|
||||
|
||||
const data = exportFormData();
|
||||
|
||||
let request;
|
||||
if (collection.isNew) {
|
||||
request = ApiClient.Collections.create(data);
|
||||
} else {
|
||||
request = ApiClient.Collections.update(collection.id, data);
|
||||
}
|
||||
|
||||
request
|
||||
.then((result) => {
|
||||
confirmClose = false;
|
||||
hide();
|
||||
addSuccessToast(
|
||||
collection.isNew ? "Successfully created collection." : "Successfully updated collection."
|
||||
);
|
||||
addCollection(result);
|
||||
|
||||
if (collection.isNew) {
|
||||
$activeCollection = result;
|
||||
}
|
||||
|
||||
dispatch("save", result);
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
})
|
||||
.finally(() => {
|
||||
isSaving = false;
|
||||
});
|
||||
}
|
||||
|
||||
function exportFormData() {
|
||||
const data = collection.export();
|
||||
data.schema = data.schema.slice(0);
|
||||
|
||||
// remove deleted fields
|
||||
for (let i = data.schema.length - 1; i >= 0; i--) {
|
||||
const field = data.schema[i];
|
||||
if (field.toDelete) {
|
||||
data.schema.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function deleteConfirm() {
|
||||
if (!original?.id) {
|
||||
return; // nothing to delete
|
||||
}
|
||||
|
||||
confirm(`Do you really want to delete collection "${original?.name}" and all its records?`, () => {
|
||||
return ApiClient.Collections.delete(original?.id)
|
||||
.then(() => {
|
||||
hide();
|
||||
addSuccessToast(`Successfully deleted collection "${original?.name}".`);
|
||||
dispatch("delete", original);
|
||||
removeCollection(original);
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function calculateFormHash(m) {
|
||||
return JSON.stringify(m);
|
||||
}
|
||||
</script>
|
||||
|
||||
<OverlayPanel
|
||||
bind:this={collectionPanel}
|
||||
class="overlay-panel-lg colored-header collection-panel"
|
||||
beforeHide={() => {
|
||||
if (hasChanges && confirmClose) {
|
||||
confirm("You have unsaved changes. Do you really want to close the panel?", () => {
|
||||
confirmClose = false;
|
||||
hide();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
on:hide
|
||||
on:show
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<h4>
|
||||
{collection.isNew ? "New collection" : "Edit collection"}
|
||||
</h4>
|
||||
|
||||
{#if !collection.isNew && !collection.system}
|
||||
<div class="flex-fill" />
|
||||
<button type="button" class="btn btn-sm btn-circle btn-secondary flex-gap-0">
|
||||
<i class="ri-more-line" />
|
||||
<Toggler class="dropdown dropdown-right m-t-5">
|
||||
<button type="button" class="dropdown-item closable" on:click={() => deleteConfirm()}>
|
||||
<i class="ri-delete-bin-7-line" />
|
||||
<span class="txt">Delete</span>
|
||||
</button>
|
||||
</Toggler>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<form
|
||||
class="block"
|
||||
on:submit|preventDefault={() => {
|
||||
canSave && saveWithConfirm();
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
class="form-field required m-b-0 {isSystemUpdate ? 'disabled' : ''}"
|
||||
name="name"
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>Name</label>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
type="text"
|
||||
id={uniqueId}
|
||||
required
|
||||
disabled={isSystemUpdate}
|
||||
spellcheck="false"
|
||||
autofocus={collection.isNew}
|
||||
placeholder={`eg. "posts"`}
|
||||
value={collection.name}
|
||||
on:input={(e) => {
|
||||
collection.name = CommonHelper.slugify(e.target.value);
|
||||
e.target.value = collection.name;
|
||||
}}
|
||||
/>
|
||||
{#if collection.system}
|
||||
<div class="help-block">System collection</div>
|
||||
{/if}
|
||||
</Field>
|
||||
|
||||
<input type="submit" class="hidden" tabindex="-1" />
|
||||
</form>
|
||||
|
||||
<div class="tabs-header stretched">
|
||||
<button
|
||||
type="button"
|
||||
class="tab-item"
|
||||
class:active={activeTab === TAB_FIELDS}
|
||||
on:click={() => changeTab(TAB_FIELDS)}
|
||||
>
|
||||
<span class="txt">Fields</span>
|
||||
{#if !CommonHelper.isEmpty($errors?.schema)}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale|local={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={schemaTabError}
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="tab-item"
|
||||
class:active={activeTab === TAB_RULES}
|
||||
on:click={() => changeTab(TAB_RULES)}
|
||||
>
|
||||
<span class="txt">API Rules</span>
|
||||
{#if !CommonHelper.isEmpty($errors?.listRule) || !CommonHelper.isEmpty($errors?.viewRule) || !CommonHelper.isEmpty($errors?.createRule) || !CommonHelper.isEmpty($errors?.updateRule) || !CommonHelper.isEmpty($errors?.deleteRule)}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale|local={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={"Has errors"}
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="tabs-content">
|
||||
<!-- avoid rerendering the fields tab -->
|
||||
<div class="tab-item" class:active={activeTab === TAB_FIELDS}>
|
||||
<CollectionFieldsTab bind:collection />
|
||||
</div>
|
||||
|
||||
{#if activeTab === TAB_RULES}
|
||||
<div class="tab-item active">
|
||||
<CollectionRulesTab bind:collection />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button type="button" class="btn btn-secondary" disabled={isSaving} on:click={() => hide()}>
|
||||
<span class="txt">Cancel</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-expanded"
|
||||
class:btn-loading={isSaving}
|
||||
disabled={!canSave || isSaving}
|
||||
on:click={() => saveWithConfirm()}
|
||||
>
|
||||
<span class="txt">{collection.isNew ? "Create" : "Save changes"}</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</OverlayPanel>
|
||||
|
||||
<CollectionUpdateConfirm bind:this={confirmChangesPanel} on:confirm={() => save()} />
|
||||
|
||||
<style>
|
||||
.tabs-content {
|
||||
z-index: 3; /* autocomplete dropdown overlay fix */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script>
|
||||
import { collections, activeCollection } from "@/stores/collections";
|
||||
import CollectionUpsertPanel from "@/components/collections/CollectionUpsertPanel.svelte";
|
||||
|
||||
let collectionPanel;
|
||||
let searchTerm = "";
|
||||
|
||||
$: normalizedSearch = searchTerm.replace(/\s+/g, "").toLowerCase();
|
||||
|
||||
$: hasSearch = searchTerm !== "";
|
||||
|
||||
$: filteredCollections = $collections.filter((collection) => {
|
||||
return (
|
||||
collection.name != import.meta.env.PB_PROFILE_COLLECTION &&
|
||||
(collection.id == searchTerm ||
|
||||
collection.name.replace(/\s+/g, "").toLowerCase().includes(normalizedSearch))
|
||||
);
|
||||
});
|
||||
|
||||
function selectCollection(collection) {
|
||||
$activeCollection = collection;
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside class="page-sidebar collection-sidebar">
|
||||
<header class="sidebar-header">
|
||||
<div class="form-field search" class:active={hasSearch}>
|
||||
<div class="form-field-addon">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-secondary btn-circle btn-clear"
|
||||
class:hidden={!hasSearch}
|
||||
on:click={() => (searchTerm = "")}
|
||||
>
|
||||
<i class="ri-close-line" />
|
||||
</button>
|
||||
</div>
|
||||
<input type="text" placeholder="Search collections..." bind:value={searchTerm} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<hr class="m-t-5 m-b-xs" />
|
||||
|
||||
<div class="sidebar-content">
|
||||
{#each filteredCollections as collection (collection.id)}
|
||||
<div
|
||||
tabindex="0"
|
||||
class="sidebar-list-item"
|
||||
class:active={$activeCollection?.id === collection.id}
|
||||
on:click={() => selectCollection(collection)}
|
||||
>
|
||||
{#if $activeCollection?.id === collection.id}
|
||||
<i class="ri-folder-open-line" />
|
||||
{:else}
|
||||
<i class="ri-folder-2-line" />
|
||||
{/if}
|
||||
<span class="txt">{collection.name}</span>
|
||||
</div>
|
||||
{:else}
|
||||
{#if normalizedSearch.length}
|
||||
<p class="txt-hint m-t-10 m-b-10 txt-center">No collections found.</p>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<footer class="sidebar-footer">
|
||||
<button type="button" class="btn btn-block btn-outline" on:click={() => collectionPanel?.show()}>
|
||||
<i class="ri-add-line" />
|
||||
<span class="txt">New collection</span>
|
||||
</button>
|
||||
</footer>
|
||||
</aside>
|
||||
|
||||
<CollectionUpsertPanel bind:this={collectionPanel} />
|
||||
@@ -0,0 +1,286 @@
|
||||
<script>
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { scale, fly } from "svelte/transition";
|
||||
import { SchemaField } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import { errors } from "@/stores/errors";
|
||||
import Accordion from "@/components/base/Accordion.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import FieldTypeSelect from "@/components/collections/schema/FieldTypeSelect.svelte";
|
||||
import TextOptions from "@/components/collections/schema/TextOptions.svelte";
|
||||
import NumberOptions from "@/components/collections/schema/NumberOptions.svelte";
|
||||
import BoolOptions from "@/components/collections/schema/BoolOptions.svelte";
|
||||
import EmailOptions from "@/components/collections/schema/EmailOptions.svelte";
|
||||
import UrlOptions from "@/components/collections/schema/UrlOptions.svelte";
|
||||
import DateOptions from "@/components/collections/schema/DateOptions.svelte";
|
||||
import SelectOptions from "@/components/collections/schema/SelectOptions.svelte";
|
||||
import JsonOptions from "@/components/collections/schema/JsonOptions.svelte";
|
||||
import FileOptions from "@/components/collections/schema/FileOptions.svelte";
|
||||
import RelationOptions from "@/components/collections/schema/RelationOptions.svelte";
|
||||
import UserOptions from "@/components/collections/schema/UserOptions.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let key = "0";
|
||||
export let field = new SchemaField();
|
||||
export let disabled = false;
|
||||
export let excludeNames = [];
|
||||
|
||||
let accordion;
|
||||
let initialType = field.type;
|
||||
|
||||
$: if (initialType != field.type) {
|
||||
initialType = field.type;
|
||||
// reset common options
|
||||
field.options = {};
|
||||
field.unique = false;
|
||||
}
|
||||
|
||||
$: if (excludeNames.length) {
|
||||
const normalizedName = normalizeFieldName(field.name);
|
||||
if (field.name !== normalizedName) {
|
||||
field.name = normalizedName;
|
||||
}
|
||||
}
|
||||
|
||||
$: canBeStored = !CommonHelper.isEmpty(field.name) && field.type;
|
||||
|
||||
$: if (!canBeStored) {
|
||||
accordion && expand();
|
||||
}
|
||||
|
||||
$: if (field.toDelete) {
|
||||
accordion && collapse();
|
||||
|
||||
// reset the name if it was previously deleted
|
||||
if (!field.name && field.originalName) {
|
||||
field.name = field.originalName;
|
||||
}
|
||||
}
|
||||
|
||||
$: if (!field.originalName && field.name) {
|
||||
field.originalName = field.name;
|
||||
}
|
||||
|
||||
$: if (typeof field.toDelete === "undefined") {
|
||||
field.toDelete = false; // normalize
|
||||
}
|
||||
|
||||
$: interactive = !disabled && !field.system && !field.toDelete && canBeStored;
|
||||
|
||||
$: hasErrors = !CommonHelper.isEmpty(CommonHelper.getNestedVal($errors, `schema.${key}`));
|
||||
|
||||
export function expand() {
|
||||
accordion?.expand();
|
||||
}
|
||||
|
||||
export function collapse() {
|
||||
accordion?.collapse();
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
if (!field.id) {
|
||||
collapse();
|
||||
dispatch("remove");
|
||||
} else {
|
||||
field.toDelete = true;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeFieldName(name) {
|
||||
name = CommonHelper.slugify(name);
|
||||
|
||||
let counter = "";
|
||||
while (excludeNames.includes(name + counter)) {
|
||||
++counter;
|
||||
}
|
||||
|
||||
return name + counter;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// auto expand new fields
|
||||
if (!field.id) {
|
||||
expand();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Accordion
|
||||
bind:this={accordion}
|
||||
on:expand
|
||||
on:collapse
|
||||
on:toggle
|
||||
single
|
||||
{interactive}
|
||||
class={disabled || field.toDelete || field.system ? "field-accordion disabled" : "field-accordion"}
|
||||
>
|
||||
<svelte:fragment slot="header" let:active={expanded}>
|
||||
<div class="inline-flex">
|
||||
<span class="icon field-type">
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
</span>
|
||||
<strong class="title field-name" class:txt-strikethrough={field.toDelete} title={field.name}>
|
||||
{field.name || "-"}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
{#if !field.toDelete}
|
||||
<div class="inline-flex">
|
||||
{#if field.system}
|
||||
<span class="label label-danger">System</span>
|
||||
{/if}
|
||||
{#if !field.id}
|
||||
<span class="label" class:label-warning={interactive && !field.toDelete}>New</span>
|
||||
{/if}
|
||||
{#if field.required}
|
||||
<span class="label label-success">Required</span>
|
||||
{/if}
|
||||
{#if field.unique}
|
||||
<span class="label label-success">Unique</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex-fill" />
|
||||
|
||||
{#if hasErrors}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={{ text: "Has errors", position: "left" }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if expanded && !field.toDelete}
|
||||
<div class="inline-flex flex-gap-sm flex-nowrap" in:fly={{ duration: 200, x: 20, opacity: 0 }}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm fade p-l-0 p-r-0"
|
||||
on:click|stopPropagation={handleDelete}
|
||||
>
|
||||
<span class="txt">Remove</span>
|
||||
</button>
|
||||
|
||||
{#if interactive}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline btn-expanded-sm"
|
||||
on:click|stopPropagation={collapse}
|
||||
>
|
||||
<span class="txt">Done</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if field.toDelete}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger btn-secondary"
|
||||
on:click|stopPropagation={() => {
|
||||
field.toDelete = false;
|
||||
}}
|
||||
>
|
||||
<span class="txt">Restore</span>
|
||||
</button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<form
|
||||
class="field-form"
|
||||
on:submit|preventDefault={() => {
|
||||
canBeStored && collapse();
|
||||
}}
|
||||
>
|
||||
<div class="grid">
|
||||
<div class="col-sm-6">
|
||||
<Field
|
||||
class="form-field required {field.id ? 'disabled' : ''}"
|
||||
name="schema.{key}.type"
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>Type</label>
|
||||
<FieldTypeSelect id={uniqueId} disabled={field.id} bind:value={field.type} />
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<Field
|
||||
class="form-field required {field.id && field.system ? 'disabled' : ''}"
|
||||
name="schema.{key}.name"
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>Name</label>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
type="text"
|
||||
id={uniqueId}
|
||||
required
|
||||
disabled={field.id && field.system}
|
||||
spellcheck="false"
|
||||
autofocus={!field.id}
|
||||
value={field.name}
|
||||
on:input={(e) => {
|
||||
field.name = normalizeFieldName(e.target.value);
|
||||
e.target.value = field.name;
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 hidden-empty">
|
||||
{#if field.type === "text"}
|
||||
<TextOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "number"}
|
||||
<NumberOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "bool"}
|
||||
<BoolOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "email"}
|
||||
<EmailOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "url"}
|
||||
<UrlOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "date"}
|
||||
<DateOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "select"}
|
||||
<SelectOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "json"}
|
||||
<JsonOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "file"}
|
||||
<FileOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "relation"}
|
||||
<RelationOptions {key} bind:options={field.options} />
|
||||
{:else if field.type === "user"}
|
||||
<UserOptions {key} bind:options={field.options} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<Field class="form-field form-field-toggle m-0" name="requried" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={field.required} />
|
||||
<label for={uniqueId}>Required</label>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
{#if field.type !== "file"}
|
||||
<Field class="form-field form-field-toggle m-0" name="unique" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={field.unique} />
|
||||
<label for={uniqueId}>Unique</label>
|
||||
</Field>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" class="hidden" tabindex="-1" />
|
||||
</form>
|
||||
</Accordion>
|
||||
|
||||
<style>
|
||||
.title.field-name {
|
||||
max-width: 130px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,111 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
import ListApiDocs from "@/components/collections/docs/ListApiDocs.svelte";
|
||||
import ViewApiDocs from "@/components/collections/docs/ViewApiDocs.svelte";
|
||||
import CreateApiDocs from "@/components/collections/docs/CreateApiDocs.svelte";
|
||||
import UpdateApiDocs from "@/components/collections/docs/UpdateApiDocs.svelte";
|
||||
import DeleteApiDocs from "@/components/collections/docs/DeleteApiDocs.svelte";
|
||||
import RealtimeApiDocs from "@/components/collections/docs/RealtimeApiDocs.svelte";
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: "list",
|
||||
label: "List",
|
||||
component: ListApiDocs,
|
||||
},
|
||||
{
|
||||
id: "view",
|
||||
label: "View",
|
||||
component: ViewApiDocs,
|
||||
},
|
||||
{
|
||||
id: "create",
|
||||
label: "Create",
|
||||
component: CreateApiDocs,
|
||||
},
|
||||
{
|
||||
id: "update",
|
||||
label: "Update",
|
||||
component: UpdateApiDocs,
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
label: "Delete",
|
||||
component: DeleteApiDocs,
|
||||
},
|
||||
{
|
||||
id: "realtime",
|
||||
label: "Realtime",
|
||||
component: RealtimeApiDocs,
|
||||
},
|
||||
];
|
||||
|
||||
let collectionPanel;
|
||||
let collection = new Collection();
|
||||
let activeTab = tabs[0].id;
|
||||
|
||||
export function show(model) {
|
||||
collection = model;
|
||||
|
||||
changeTab(tabs[0].id);
|
||||
|
||||
return collectionPanel?.show();
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
return collectionPanel?.hide();
|
||||
}
|
||||
|
||||
export function changeTab(newTab) {
|
||||
activeTab = newTab;
|
||||
}
|
||||
|
||||
function changeTabViaKey(e, newTab) {
|
||||
if (e.code === "Enter" || e.code === "Space") {
|
||||
e.preventDefault();
|
||||
changeTab(newTab);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<OverlayPanel
|
||||
bind:this={collectionPanel}
|
||||
on:hide
|
||||
on:show
|
||||
class="overlay-panel-xl colored-header collection-panel"
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<h4><strong>{collection.name}</strong> records API</h4>
|
||||
|
||||
<div class="tabs-header stretched">
|
||||
{#each tabs as tab (tab.id)}
|
||||
<button
|
||||
tabindex="0"
|
||||
class="tab-item"
|
||||
class:active={activeTab === tab.id}
|
||||
on:click={() => changeTab(tab.id)}
|
||||
on:keydown|self={(e) => changeTabViaKey(e, tab.id)}
|
||||
>
|
||||
<span class="txt">{tab.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="tabs-content">
|
||||
{#each tabs as tab (tab.id)}
|
||||
{#if activeTab === tab.id}
|
||||
<div class="tab-item active">
|
||||
<svelte:component this={tab.component} {collection} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button type="button" class="btn btn-secondary" on:click={() => hide()}>
|
||||
<span class="txt">Close</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</OverlayPanel>
|
||||
@@ -0,0 +1,184 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 200;
|
||||
let sdkTab = "JavaScript";
|
||||
let responses = [];
|
||||
let sdkExamples = [];
|
||||
|
||||
$: adminsOnly = collection?.createRule === null;
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 200,
|
||||
body: JSON.stringify(CommonHelper.dummyCollectionRecord(collection), null, 2),
|
||||
},
|
||||
{
|
||||
code: 400,
|
||||
body: `
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to create record.",
|
||||
"data": {
|
||||
"${collection?.schema?.[0]?.name}": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: 403,
|
||||
body: `
|
||||
{
|
||||
"code": 403,
|
||||
"message": "You are not allowed to perform this request.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
$: sdkExamples = [
|
||||
{
|
||||
lang: "JavaScript",
|
||||
code: `
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const client = new PocketBase("${ApiClient.baseUrl}");
|
||||
|
||||
const data = { ... };
|
||||
|
||||
client.Records.create("${collection?.name}", data)
|
||||
.then(function (record) {
|
||||
// success...
|
||||
}).catch(function (error) {
|
||||
// error...
|
||||
});
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="alert alert-success">
|
||||
<strong class="label label-primary">POST</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/records
|
||||
</p>
|
||||
</div>
|
||||
{#if adminsOnly}
|
||||
<p class="txt-hint txt-sm txt-right">Requires <code>Authorization: Admin TOKEN</code> header</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="content m-b-base">
|
||||
<p>Create a new <strong>{collection.name}</strong> record.</p>
|
||||
<p>
|
||||
Body parameters could be sent as <code>application/json</code> or
|
||||
<code>multipart/form-data</code>.
|
||||
</p>
|
||||
<p>
|
||||
File upload is supported only via <code>multipart/form-data</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Client SDKs example</div>
|
||||
<div class="tabs m-b-lg">
|
||||
<div class="tabs-header compact left">
|
||||
{#each sdkExamples as example (example.lang)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={sdkTab === example.lang}
|
||||
on:click={() => (sdkTab = example.lang)}
|
||||
>
|
||||
{example.lang}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each sdkExamples as example (example.lang)}
|
||||
<div class="tab-item" class:active={sdkTab === example.lang}>
|
||||
<CodeBlock content={example.code} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Body Parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each collection?.schema as field (field.name)}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
{#if field.required}
|
||||
<span class="label label-success">Required</span>
|
||||
{:else}
|
||||
<span class="label label-warning">Optional</span>
|
||||
{/if}
|
||||
<span>{field.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">{CommonHelper.getFieldValueType(field)}</span>
|
||||
</td>
|
||||
<td>
|
||||
{#if field.type === "text"}
|
||||
Plain text value.
|
||||
{:else if field.type === "number"}
|
||||
Number value.
|
||||
{:else if field.type === "json"}
|
||||
JSON array or object.
|
||||
{:else if field.type === "email"}
|
||||
Email address.
|
||||
{:else if field.type === "url"}
|
||||
URL address.
|
||||
{:else if field.type === "file"}
|
||||
FormData object.<br />
|
||||
Set to <code>null</code> to delete already uploaded file(s).
|
||||
{:else if field.type === "relation"}
|
||||
Relation record {field.options?.maxSelect > 1 ? "ids" : "id"}.
|
||||
{:else if field.type === "user"}
|
||||
User {field.options?.maxSelect > 1 ? "ids" : "id"}.
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,156 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 204;
|
||||
let sdkTab = "JavaScript";
|
||||
let responses = [];
|
||||
let sdkExamples = [];
|
||||
|
||||
$: adminsOnly = collection?.deleteRule === null;
|
||||
|
||||
$: if (collection?.id) {
|
||||
responses.push({
|
||||
code: 204,
|
||||
body: `
|
||||
null
|
||||
`,
|
||||
});
|
||||
|
||||
responses.push({
|
||||
code: 400,
|
||||
body: `
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to delete record. Make sure that the record is not part of a required relation reference.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
if (adminsOnly) {
|
||||
responses.push({
|
||||
code: 403,
|
||||
body: `
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Only admins can access this action.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
responses.push({
|
||||
code: 404,
|
||||
body: `
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested resource wasn't found.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
$: sdkExamples = [
|
||||
{
|
||||
lang: "JavaScript",
|
||||
code: `
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const client = new PocketBase("${ApiClient.baseUrl}");
|
||||
|
||||
client.Records.delete("${collection?.name}", "RECORD_ID")
|
||||
.then(function () {
|
||||
// success...
|
||||
}).catch(function (error) {
|
||||
// error...
|
||||
});
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="alert alert-danger">
|
||||
<strong class="label label-primary">DELETE</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/records/<strong>:id</strong>
|
||||
</p>
|
||||
</div>
|
||||
{#if adminsOnly}
|
||||
<p class="txt-hint txt-sm txt-right">Requires <code>Authorization: Admin TOKEN</code> header</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="content m-b-base">
|
||||
<p>Delete a single <strong>{collection.name}</strong> record.</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Client SDKs example</div>
|
||||
<div class="tabs m-b-lg">
|
||||
<div class="tabs-header compact left">
|
||||
{#each sdkExamples as example (example.lang)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={sdkTab === example.lang}
|
||||
on:click={() => (sdkTab = example.lang)}
|
||||
>
|
||||
{example.lang}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each sdkExamples as example (example.lang)}
|
||||
<div class="tab-item" class:active={sdkTab === example.lang}>
|
||||
<CodeBlock content={example.code} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Path parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>ID of the record to delete.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
<p>
|
||||
The syntax basically follows the format
|
||||
<code>
|
||||
<span class="txt-success">OPERAND</span>
|
||||
<span class="txt-danger">OPERATOR</span>
|
||||
<span class="txt-success">OPERAND</span></code
|
||||
>, where:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code class="txt-success">OPERAND</code> - could be any of the above field literal, string (single or double
|
||||
quoted), number, null, true, false
|
||||
</li>
|
||||
<li>
|
||||
<code class="txt-danger">OPERATOR</code> - is one of:
|
||||
<br />
|
||||
<ul>
|
||||
<li>
|
||||
<code class="filter-op">{"="}</code>
|
||||
<span class="txt-hint">Equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"!="}</code>
|
||||
<span class="txt-hint">NOT equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{">"}</code>
|
||||
<span class="txt-hint">Greater than</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{">="}</code>
|
||||
<span class="txt-hint">Greater than or equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"<"}</code>
|
||||
<span class="txt-hint">Less than or equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"<="}</code>
|
||||
<span class="txt-hint">Less than or equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"~"}</code>
|
||||
<span class="txt-hint">
|
||||
Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for wildcard
|
||||
match)
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"!~"}</code>
|
||||
<span class="txt-hint">
|
||||
NOT Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
|
||||
wildcard match)
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
To group and combine several expressions you could use brackets
|
||||
<code>(...)</code>, <code>&&</code> (AND) and <code>||</code> (OR) tokens.
|
||||
</p>
|
||||
|
||||
<style>
|
||||
.filter-op {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 5px;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,232 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import FilterSyntax from "@/components/collections/docs/FilterSyntax.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 200;
|
||||
let sdkTab = "JavaScript";
|
||||
let responses = [];
|
||||
let sdkExamples = [];
|
||||
|
||||
$: adminsOnly = collection?.listRule === null;
|
||||
|
||||
$: if (collection?.id) {
|
||||
responses.push({
|
||||
code: 200,
|
||||
body: JSON.stringify(
|
||||
{
|
||||
page: 1,
|
||||
perPage: 30,
|
||||
totalItems: 2,
|
||||
items: [
|
||||
CommonHelper.dummyCollectionRecord(collection),
|
||||
CommonHelper.dummyCollectionRecord(collection),
|
||||
],
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
});
|
||||
|
||||
responses.push({
|
||||
code: 400,
|
||||
body: `
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Something went wrong while processing your request. Invalid filter.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
if (adminsOnly) {
|
||||
responses.push({
|
||||
code: 403,
|
||||
body: `
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Only admins can access this action.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
responses.push({
|
||||
code: 404,
|
||||
body: `
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested resource wasn't found.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
$: sdkExamples = [
|
||||
{
|
||||
lang: "JavaScript",
|
||||
code: `
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const client = new PocketBase("${ApiClient.baseUrl}");
|
||||
|
||||
client.Records.getList("${collection?.name}", { page: 2 })
|
||||
.then(function (list) {
|
||||
// success...
|
||||
}).catch(function (error) {
|
||||
// error...
|
||||
});
|
||||
|
||||
// alternatively you can also fetch all records at once via getFullList:
|
||||
client.Records.getFullList("${collection?.name}", 200 /* batch size */);
|
||||
.then(function (records) {
|
||||
// success...
|
||||
}).catch(function (error) {
|
||||
// error...
|
||||
});
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong class="label label-primary">GET</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/records
|
||||
</p>
|
||||
</div>
|
||||
{#if adminsOnly}
|
||||
<p class="txt-hint txt-sm txt-right">Requires <code>Authorization: Admin TOKEN</code> header</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="content m-b-base">
|
||||
<p>Fetch a paginated <strong>{collection.name}</strong> records list.</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Client SDKs example</div>
|
||||
<div class="tabs m-b-lg">
|
||||
<div class="tabs-header compact left">
|
||||
{#each sdkExamples as example (example.lang)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={sdkTab === example.lang}
|
||||
on:click={() => (sdkTab = example.lang)}
|
||||
>
|
||||
{example.lang}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each sdkExamples as example (example.lang)}
|
||||
<div class="tab-item" class:active={sdkTab === example.lang}>
|
||||
<CodeBlock content={example.code} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Query parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>page</td>
|
||||
<td>
|
||||
<span class="label">Number</span>
|
||||
</td>
|
||||
<td>The page (aka. offset) of the paginated list (default to 1).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>perPage</td>
|
||||
<td>
|
||||
<span class="label">Number</span>
|
||||
</td>
|
||||
<td>Specify the max returned records per page (default to 30).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>sort</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Specify the records order attribute(s). <br />
|
||||
Add <code>-</code> / <code>+</code> (default) in front of the attribute for DESC / ASC order.
|
||||
Ex.:
|
||||
<CodeBlock
|
||||
content={`
|
||||
// DESC by created and ASC by id
|
||||
?sort=-created,id
|
||||
`}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>filter</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Filter the returned records. Ex.:
|
||||
<CodeBlock
|
||||
content={`
|
||||
?filter=(id='abc' && created>'2022-01-01')
|
||||
`}
|
||||
/>
|
||||
<FilterSyntax />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>expand</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Auto expand nested record relations. Ex.:
|
||||
<CodeBlock
|
||||
content={`
|
||||
?expand=rel1,rel2.subrel21.subrel22
|
||||
`}
|
||||
/>
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to each individual record under the
|
||||
<code>@expand</code> property (eg. <code>{`"@expand": {"rel1": {...}, ...}`}</code>).
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<div
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,109 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let sdkTab = "JavaScript";
|
||||
let sdkExamples = [];
|
||||
|
||||
$: sdkExamples = [
|
||||
{
|
||||
lang: "JavaScript",
|
||||
code: `
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const client = new PocketBase("${ApiClient.baseUrl}");
|
||||
|
||||
// (Optionally) authenticate
|
||||
client.Users.authViaEmail("test@example.com", "123456");
|
||||
|
||||
// Subscribe to changes in any record from the collection
|
||||
client.Realtime.subscribe("${collection?.name}", function (e) {
|
||||
console.log(e.data);
|
||||
});
|
||||
|
||||
// Subscribe to changes in a single record
|
||||
client.Realtime.subscribe("${collection?.name}/RECORD_ID", function (e) {
|
||||
console.log(e.data);
|
||||
});
|
||||
|
||||
// Unsubscribe
|
||||
client.Realtime.unsubscribe() // remove all subscriptions
|
||||
client.Realtime.unsubscribe("${collection?.name}") // remove the collection subscription
|
||||
client.Realtime.unsubscribe("${collection?.name}/RECORD_ID") // remove the record subscription
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="alert">
|
||||
<strong class="label label-primary">SSE</strong>
|
||||
<div class="content">
|
||||
<p>/api/realtime</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content m-b-base">
|
||||
<p>Subscribe to realtime changes via Server-Sent Events (SSE).</p>
|
||||
<p>
|
||||
Events are send for <strong>create</strong>, <strong>update</strong>
|
||||
and <strong>delete</strong> record operations (see "Event data format" section below).
|
||||
</p>
|
||||
<div class="alert alert-info m-t-10">
|
||||
<div class="icon">
|
||||
<i class="ri-information-line" />
|
||||
</div>
|
||||
<div class="contet">
|
||||
<p>
|
||||
<strong>You could subscribe to a single record or to an entire collection.</strong>
|
||||
</p>
|
||||
<p>
|
||||
When you subscribe to a <strong>single record</strong>, the collection's
|
||||
<strong>ViewRule</strong> will be used to determine whether the subscriber has access to receive
|
||||
the event message.
|
||||
</p>
|
||||
<p>
|
||||
When you subscribe to an <strong>entire collection</strong>, the collection's
|
||||
<strong>ListRule</strong> will be used to determine whether the subscriber has access to receive
|
||||
the event message.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Client SDKs example</div>
|
||||
<div class="tabs m-b-base">
|
||||
<div class="tabs-header compact left">
|
||||
{#each sdkExamples as example (example.lang)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={sdkTab === example.lang}
|
||||
on:click={() => (sdkTab = example.lang)}
|
||||
>
|
||||
{example.lang}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each sdkExamples as example (example.lang)}
|
||||
<div class="tab-item" class:active={sdkTab === example.lang}>
|
||||
<CodeBlock content={example.code} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Event data format</div>
|
||||
<CodeBlock
|
||||
content={JSON.stringify(
|
||||
{
|
||||
action: "create",
|
||||
record: CommonHelper.dummyCollectionRecord(collection),
|
||||
},
|
||||
null,
|
||||
2
|
||||
).replace('"action": "create"', '"action": "create" // create, update or delete')}
|
||||
/>
|
||||
@@ -0,0 +1,214 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 200;
|
||||
let sdkTab = "JavaScript";
|
||||
let responses = [];
|
||||
let sdkExamples = [];
|
||||
|
||||
$: adminsOnly = collection?.updateRule === null;
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 200,
|
||||
body: JSON.stringify(CommonHelper.dummyCollectionRecord(collection), null, 2),
|
||||
},
|
||||
{
|
||||
code: 400,
|
||||
body: `
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to update record.",
|
||||
"data": {
|
||||
"${collection?.schema?.[0]?.name}": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: 403,
|
||||
body: `
|
||||
{
|
||||
"code": 403,
|
||||
"message": "You are not allowed to perform this request.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: 404,
|
||||
body: `
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested resource wasn't found.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
$: sdkExamples = [
|
||||
{
|
||||
lang: "JavaScript",
|
||||
code: `
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const client = new PocketBase("${ApiClient.baseUrl}");
|
||||
|
||||
const data = { ... };
|
||||
|
||||
client.Records.update("${collection?.name}", "RECORD_ID", data)
|
||||
.then(function (record) {
|
||||
// success...
|
||||
}).catch(function (error) {
|
||||
// error...
|
||||
});
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<strong class="label label-primary">PATCH</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/records/<strong>:id</strong>
|
||||
</p>
|
||||
</div>
|
||||
{#if adminsOnly}
|
||||
<p class="txt-hint txt-sm txt-right">Requires <code>Authorization: Admin TOKEN</code> header</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="content m-b-base">
|
||||
<p>Update a single <strong>{collection.name}</strong> record.</p>
|
||||
<p>
|
||||
Body parameters could be sent as <code>application/json</code> or
|
||||
<code>multipart/form-data</code>.
|
||||
</p>
|
||||
<p>
|
||||
File upload is supported only via <code>multipart/form-data</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Client SDKs example</div>
|
||||
<div class="tabs m-b-lg">
|
||||
<div class="tabs-header compact left">
|
||||
{#each sdkExamples as example (example.lang)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={sdkTab === example.lang}
|
||||
on:click={() => (sdkTab = example.lang)}
|
||||
>
|
||||
{example.lang}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each sdkExamples as example (example.lang)}
|
||||
<div class="tab-item" class:active={sdkTab === example.lang}>
|
||||
<CodeBlock content={example.code} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Path parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>ID of the record to update.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Body Parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th width="60%">Type</th>
|
||||
<th width="50%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each collection?.schema as field (field.name)}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
{#if field.required}
|
||||
<span class="label label-success">Required</span>
|
||||
{:else}
|
||||
<span class="label label-warning">Optional</span>
|
||||
{/if}
|
||||
<span>{field.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">{CommonHelper.getFieldValueType(field)}</span>
|
||||
</td>
|
||||
<td>
|
||||
{#if field.type === "text"}
|
||||
Plain text value.
|
||||
{:else if field.type === "number"}
|
||||
Number value.
|
||||
{:else if field.type === "json"}
|
||||
JSON array or object.
|
||||
{:else if field.type === "email"}
|
||||
Email address.
|
||||
{:else if field.type === "url"}
|
||||
URL address.
|
||||
{:else if field.type === "file"}
|
||||
FormData object.<br />
|
||||
Set to <code>null</code> to delete already uploaded file(s).
|
||||
{:else if field.type === "relation"}
|
||||
Relation record {field.options?.maxSelect > 1 ? "ids" : "id"}.
|
||||
{:else if field.type === "user"}
|
||||
User {field.options?.maxSelect > 1 ? "ids" : "id"}.
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,174 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 200;
|
||||
let sdkTab = "JavaScript";
|
||||
let responses = [];
|
||||
let sdkExamples = [];
|
||||
|
||||
$: adminsOnly = collection?.viewRule === null;
|
||||
|
||||
$: if (collection?.id) {
|
||||
responses.push({
|
||||
code: 200,
|
||||
body: JSON.stringify(CommonHelper.dummyCollectionRecord(collection), null, 2),
|
||||
});
|
||||
|
||||
if (adminsOnly) {
|
||||
responses.push({
|
||||
code: 403,
|
||||
body: `
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Only admins can access this action.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
responses.push({
|
||||
code: 404,
|
||||
body: `
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested resource wasn't found.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
$: sdkExamples = [
|
||||
{
|
||||
lang: "JavaScript",
|
||||
code: `
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const client = new PocketBase("${ApiClient.baseUrl}");
|
||||
|
||||
client.Records.getOne("${collection?.name}", "RECORD_ID")
|
||||
.then(function (record) {
|
||||
// success...
|
||||
}).catch(function (error) {
|
||||
// error...
|
||||
});
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong class="label label-primary">GET</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/records/<strong>:id</strong>
|
||||
</p>
|
||||
</div>
|
||||
{#if adminsOnly}
|
||||
<p class="txt-hint txt-sm txt-right">Requires <code>Authorization: Admin TOKEN</code> header</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="content m-b-base">
|
||||
<p>Fetch a single <strong>{collection.name}</strong> record.</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Client SDKs example</div>
|
||||
<div class="tabs m-b-lg">
|
||||
<div class="tabs-header compact left">
|
||||
{#each sdkExamples as example (example.lang)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={sdkTab === example.lang}
|
||||
on:click={() => (sdkTab = example.lang)}
|
||||
>
|
||||
{example.lang}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each sdkExamples as example (example.lang)}
|
||||
<div class="tab-item" class:active={sdkTab === example.lang}>
|
||||
<CodeBlock content={example.code} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Path Parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>ID of the record to view.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Query parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>expand</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Auto expand nested record relations. Ex.:
|
||||
<CodeBlock
|
||||
content={`
|
||||
?expand=rel1,rel2.subrel21.subrel22
|
||||
`}
|
||||
/>
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to the record under the
|
||||
<code>@expand</code> property (eg. <code>{`"@expand": {"rel1": {...}, ...}`}</code>).
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
// svelte-ignore unused-export-let
|
||||
export let key = "";
|
||||
// svelte-ignore unused-export-let
|
||||
export let options = {};
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
import Flatpickr from "svelte-flatpickr";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let key = "";
|
||||
export let options = {};
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.min" let:uniqueId>
|
||||
<label for={uniqueId}>Min date (UTC)</label>
|
||||
<Flatpickr
|
||||
id={uniqueId}
|
||||
options={CommonHelper.defaultFlatpickrOptions()}
|
||||
value={options.min}
|
||||
bind:formattedValue={options.min}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.max" let:uniqueId>
|
||||
<label for={uniqueId}>Max date (UTC)</label>
|
||||
<Flatpickr
|
||||
id={uniqueId}
|
||||
options={CommonHelper.defaultFlatpickrOptions()}
|
||||
value={options.max}
|
||||
bind:formattedValue={options.max}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import MultipleValueInput from "@/components/base/MultipleValueInput.svelte";
|
||||
|
||||
export let key = "";
|
||||
export let options = {};
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.exceptDomains" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Except domains</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: 'Domains that are NOT allowed as value. \n This field is disabled if "Only domains" is set.',
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<MultipleValueInput
|
||||
id={uniqueId}
|
||||
disabled={!CommonHelper.isEmpty(options.onlyDomains)}
|
||||
bind:value={options.exceptDomains}
|
||||
/>
|
||||
<div class="help-block">Use comma as separator.</div>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.onlyDomains" let:uniqueId>
|
||||
<label for="{uniqueId}.options.onlyDomains">
|
||||
<span class="txt">Only domains</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: 'Domains that are ONLY allowed as value. \n This field is disabled if "Except domains" is set.',
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<MultipleValueInput
|
||||
id="{uniqueId}.options.onlyDomains"
|
||||
disabled={!CommonHelper.isEmpty(options.exceptDomains)}
|
||||
bind:value={options.onlyDomains}
|
||||
/>
|
||||
<div class="help-block">Use comma as separator.</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
|
||||
export let value = "text";
|
||||
|
||||
let classes = "";
|
||||
export { classes as class }; // export reserved keyword
|
||||
|
||||
const types = [
|
||||
{
|
||||
label: "Text",
|
||||
value: "text",
|
||||
icon: CommonHelper.getFieldTypeIcon("text"),
|
||||
},
|
||||
{
|
||||
label: "Number",
|
||||
value: "number",
|
||||
icon: CommonHelper.getFieldTypeIcon("number"),
|
||||
},
|
||||
{
|
||||
label: "Bool",
|
||||
value: "bool",
|
||||
icon: CommonHelper.getFieldTypeIcon("bool"),
|
||||
},
|
||||
{
|
||||
label: "Email",
|
||||
value: "email",
|
||||
icon: CommonHelper.getFieldTypeIcon("email"),
|
||||
},
|
||||
{
|
||||
label: "Url",
|
||||
value: "url",
|
||||
icon: CommonHelper.getFieldTypeIcon("url"),
|
||||
},
|
||||
{
|
||||
label: "DateTime",
|
||||
value: "date",
|
||||
icon: CommonHelper.getFieldTypeIcon("date"),
|
||||
},
|
||||
{
|
||||
label: "Multiple choices",
|
||||
value: "select",
|
||||
icon: CommonHelper.getFieldTypeIcon("select"),
|
||||
},
|
||||
{
|
||||
label: "JSON",
|
||||
value: "json",
|
||||
icon: CommonHelper.getFieldTypeIcon("json"),
|
||||
},
|
||||
{
|
||||
label: "File",
|
||||
value: "file",
|
||||
icon: CommonHelper.getFieldTypeIcon("file"),
|
||||
},
|
||||
{
|
||||
label: "Relation",
|
||||
value: "relation",
|
||||
icon: CommonHelper.getFieldTypeIcon("relation"),
|
||||
},
|
||||
{
|
||||
label: "User",
|
||||
value: "user",
|
||||
icon: CommonHelper.getFieldTypeIcon("user"),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<ObjectSelect
|
||||
class="field-type-select {classes}"
|
||||
searchable
|
||||
items={types}
|
||||
bind:keyOfSelected={value}
|
||||
{...$$restProps}
|
||||
/>
|
||||
@@ -0,0 +1,124 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import Toggler from "@/components/base/Toggler.svelte";
|
||||
import MultipleValueInput from "@/components/base/MultipleValueInput.svelte";
|
||||
|
||||
export let key = "";
|
||||
export let options = {};
|
||||
|
||||
$: if (CommonHelper.isEmpty(options)) {
|
||||
// load defaults
|
||||
options = {
|
||||
maxSelect: 1,
|
||||
maxSize: 5242880,
|
||||
thumbs: [],
|
||||
mimeTypes: [],
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field required" name="schema.{key}.options.maxSize" let:uniqueId>
|
||||
<label for={uniqueId}>Max file size (bytes)</label>
|
||||
<input type="number" id={uniqueId} step="1" min="0" bind:value={options.maxSize} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field required" name="schema.{key}.options.maxSelect" let:uniqueId>
|
||||
<label for={uniqueId}>Max files</label>
|
||||
<input type="number" id={uniqueId} step="1" min="" required bind:value={options.maxSelect} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<Field class="form-field" name="schema.{key}.options.mimeTypes" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Mime types</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: "Allow uploading files ONLY with the listed mime types. \n Leave empty for no restriction.",
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<MultipleValueInput
|
||||
id={uniqueId}
|
||||
placeholder="eg. image/png, application/pdf..."
|
||||
bind:value={options.mimeTypes}
|
||||
/>
|
||||
<div class="help-block">
|
||||
Use comma as separator.
|
||||
<span class="inline-flex">
|
||||
<span class="txt link-primary">Choose presets</span>
|
||||
<Toggler class="dropdown dropdown-sm dropdown-nowrap">
|
||||
<div
|
||||
tabindex="0"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => {
|
||||
options.mimeTypes = [
|
||||
"image/jpg",
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
"image/gif",
|
||||
];
|
||||
}}
|
||||
>
|
||||
<span class="txt">Images (jpg, png, svg, gif)</span>
|
||||
</div>
|
||||
<div
|
||||
tabindex="0"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => {
|
||||
options.mimeTypes = [
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
];
|
||||
}}
|
||||
>
|
||||
<span class="txt">Documents (pdf, doc/docx, xls/xlsx)</span>
|
||||
</div>
|
||||
<div
|
||||
tabindex="0"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => {
|
||||
options.mimeTypes = [
|
||||
"application/zip",
|
||||
"application/x-7z-compressed",
|
||||
"application/x-rar-compressed",
|
||||
];
|
||||
}}
|
||||
>
|
||||
<span class="txt">Archives (zip, 7zip, rar)</span>
|
||||
</div>
|
||||
</Toggler>
|
||||
</span>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<Field class="form-field" name="schema.{key}.options.thumbs" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Thumb sizes</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: "List of thumb sizes for image files. The thumbs will be generated lazily on first access.",
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<MultipleValueInput id={uniqueId} placeholder="eg. 50x50, 480x720" bind:value={options.thumbs} />
|
||||
<div class="help-block">Use comma as separator.</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
// svelte-ignore unused-export-let
|
||||
export const key = "";
|
||||
// svelte-ignore unused-export-let
|
||||
export const options = {};
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let key = "";
|
||||
export let options = {};
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.min" let:uniqueId>
|
||||
<label for={uniqueId}>Min</label>
|
||||
<input type="number" id={uniqueId} bind:value={options.min} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.max" let:uniqueId>
|
||||
<label for={uniqueId}>Max</label>
|
||||
<input type="number" id={uniqueId} min={options.min} bind:value={options.max} />
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script>
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
|
||||
export let key = "";
|
||||
export let options = {};
|
||||
|
||||
const defaultOptions = [
|
||||
{ label: "False", value: false },
|
||||
{ label: "True", value: true },
|
||||
];
|
||||
|
||||
let isLoading = false;
|
||||
let collections = [];
|
||||
|
||||
// load defaults
|
||||
$: if (CommonHelper.isEmpty(options)) {
|
||||
options = {
|
||||
maxSelect: 1,
|
||||
collectionId: null,
|
||||
cascadeDelete: false,
|
||||
};
|
||||
}
|
||||
|
||||
loadCollections();
|
||||
|
||||
function loadCollections() {
|
||||
isLoading = true;
|
||||
|
||||
ApiClient.Collections.getFullList(200, { sort: "-created" })
|
||||
.then((items) => {
|
||||
collections = items;
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-sm-9">
|
||||
<Field class="form-field required" name="schema.{key}.options.collectionId" let:uniqueId>
|
||||
<label for={uniqueId}>Collection</label>
|
||||
<ObjectSelect
|
||||
searchable={collections.length > 5}
|
||||
selectPlaceholder={isLoading ? "Loading..." : "Select collection"}
|
||||
noOptionsText="No collections found"
|
||||
selectionKey="id"
|
||||
items={collections}
|
||||
bind:keyOfSelected={options.collectionId}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<Field class="form-field required" name="schema.{key}.options.maxSelect" let:uniqueId>
|
||||
<label for={uniqueId}>Max select</label>
|
||||
<input type="number" id={uniqueId} step="1" min="1" required bind:value={options.maxSelect} />
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<Field class="form-field" name="schema.{key}.options.cascadeDelete" let:uniqueId>
|
||||
<label for={uniqueId}>Delete record on relation delete</label>
|
||||
<ObjectSelect id={uniqueId} items={defaultOptions} bind:keyOfSelected={options.cascadeDelete} />
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import MultipleValueInput from "@/components/base/MultipleValueInput.svelte";
|
||||
|
||||
export let key = "";
|
||||
export let options = {};
|
||||
|
||||
$: if (CommonHelper.isEmpty(options)) {
|
||||
// load defaults
|
||||
options = {
|
||||
maxSelect: 1,
|
||||
values: [],
|
||||
};
|
||||
}
|
||||
|
||||
// leave the validation to the api
|
||||
// $: if (!CommonHelper.isEmpty(options.values) && options.maxSelect > options.values.length) {
|
||||
// options.maxSelect = options.values.length;
|
||||
// }
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-sm-9">
|
||||
<Field class="form-field required" name="schema.{key}.options.values" let:uniqueId>
|
||||
<label for={uniqueId}>Choices</label>
|
||||
<MultipleValueInput
|
||||
id={uniqueId}
|
||||
placeholder="eg. optionA, optionB"
|
||||
required
|
||||
bind:value={options.values}
|
||||
/>
|
||||
<div class="help-block">Use comma as separator.</div>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<Field class="form-field required" name="schema.{key}.options.maxSelect" let:uniqueId>
|
||||
<label for={uniqueId}>Max select</label>
|
||||
<input type="number" id={uniqueId} step="1" min="1" required bind:value={options.maxSelect} />
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script>
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let key = "";
|
||||
export let options = {};
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.min" let:uniqueId>
|
||||
<label for={uniqueId}>Min length</label>
|
||||
<input type="number" id={uniqueId} step="1" min="0" bind:value={options.min} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.max" let:uniqueId>
|
||||
<label for={uniqueId}>Max length</label>
|
||||
<input type="number" id={uniqueId} step="1" min={options.min || 0} bind:value={options.max} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<Field class="form-field" name="schema.{key}.options.pattern" let:uniqueId>
|
||||
<label for={uniqueId}>Regex pattern</label>
|
||||
<input type="text" id={uniqueId} bind:value={options.pattern} />
|
||||
<div class="help-block">Valid Go regular expression, eg. <code>^\w+$</code>.</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script>
|
||||
import EmailOptions from "./EmailOptions.svelte";
|
||||
|
||||
export let key = "";
|
||||
export let options = {};
|
||||
</script>
|
||||
|
||||
<!-- shares the same options with the email field -->
|
||||
<EmailOptions bind:key bind:options />
|
||||
@@ -0,0 +1,36 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
|
||||
const defaultOptions = [
|
||||
{ label: "False", value: false },
|
||||
{ label: "True", value: true },
|
||||
];
|
||||
|
||||
export let key = "";
|
||||
export let options = {};
|
||||
|
||||
// load defaults
|
||||
$: if (CommonHelper.isEmpty(options)) {
|
||||
options = {
|
||||
maxSelect: 1,
|
||||
cascadeDelete: false,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field required" name="schema.{key}.options.maxSelect" let:uniqueId>
|
||||
<label for={uniqueId}>Max select</label>
|
||||
<input type="number" id={uniqueId} step="1" min="1" required bind:value={options.maxSelect} />
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.cascadeDelete" let:uniqueId>
|
||||
<label for={uniqueId}>Delete record on user delete</label>
|
||||
<ObjectSelect id={uniqueId} items={defaultOptions} bind:keyOfSelected={options.cascadeDelete} />
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user