new schema and indexes ui
This commit is contained in:
@@ -1,46 +1,44 @@
|
||||
<script>
|
||||
import { Collection, SchemaField } from "pocketbase";
|
||||
import FieldAccordion from "@/components/collections/FieldAccordion.svelte";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import IndexesList from "@/components/collections/IndexesList.svelte";
|
||||
import NewField from "@/components/collections/schema/NewField.svelte";
|
||||
import SchemaFieldText from "@/components/collections/schema/SchemaFieldText.svelte";
|
||||
import SchemaFieldNumber from "@/components/collections/schema/SchemaFieldNumber.svelte";
|
||||
import SchemaFieldBool from "@/components/collections/schema/SchemaFieldBool.svelte";
|
||||
import SchemaFieldEmail from "@/components/collections/schema/SchemaFieldEmail.svelte";
|
||||
import SchemaFieldUrl from "@/components/collections/schema/SchemaFieldUrl.svelte";
|
||||
import SchemaFieldEditor from "@/components/collections/schema/SchemaFieldEditor.svelte";
|
||||
import SchemaFieldDate from "@/components/collections/schema/SchemaFieldDate.svelte";
|
||||
import SchemaFieldSelect from "@/components/collections/schema/SchemaFieldSelect.svelte";
|
||||
import SchemaFieldJson from "@/components/collections/schema/SchemaFieldJson.svelte";
|
||||
import SchemaFieldFile from "@/components/collections/schema/SchemaFieldFile.svelte";
|
||||
import SchemaFieldRelation from "@/components/collections/schema/SchemaFieldRelation.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
const baseReservedNames = [
|
||||
"id",
|
||||
"created",
|
||||
"updated",
|
||||
"collectionId",
|
||||
"collectionName",
|
||||
"expand",
|
||||
"true",
|
||||
"false",
|
||||
"null",
|
||||
];
|
||||
|
||||
let reservedNames = [];
|
||||
|
||||
$: if (collection.isAuth) {
|
||||
reservedNames = baseReservedNames.concat([
|
||||
"username",
|
||||
"email",
|
||||
"emailVisibility",
|
||||
"verified",
|
||||
"tokenKey",
|
||||
"passwordHash",
|
||||
"lastResetSentAt",
|
||||
"lastVerificationSentAt",
|
||||
"password",
|
||||
"passwordConfirm",
|
||||
"oldPassword",
|
||||
]);
|
||||
} else {
|
||||
reservedNames = baseReservedNames.slice(0);
|
||||
}
|
||||
const fieldComponents = {
|
||||
text: SchemaFieldText,
|
||||
number: SchemaFieldNumber,
|
||||
bool: SchemaFieldBool,
|
||||
email: SchemaFieldEmail,
|
||||
url: SchemaFieldUrl,
|
||||
editor: SchemaFieldEditor,
|
||||
date: SchemaFieldDate,
|
||||
select: SchemaFieldSelect,
|
||||
json: SchemaFieldJson,
|
||||
file: SchemaFieldFile,
|
||||
relation: SchemaFieldRelation,
|
||||
};
|
||||
|
||||
$: if (typeof collection.schema === "undefined") {
|
||||
collection = collection || new Collection();
|
||||
collection.schema = [];
|
||||
}
|
||||
|
||||
$: nonDeletedFields = collection.schema.filter((f) => !f.toDelete) || [];
|
||||
|
||||
function removeField(fieldIndex) {
|
||||
if (collection.schema[fieldIndex]) {
|
||||
collection.schema.splice(fieldIndex, 1);
|
||||
@@ -48,9 +46,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
function newField() {
|
||||
function newField(fieldType = "text") {
|
||||
const field = new SchemaField({
|
||||
name: getUniqueFieldName(),
|
||||
type: fieldType,
|
||||
});
|
||||
|
||||
field.onMountExpand = true;
|
||||
@@ -73,22 +72,19 @@
|
||||
return !!collection.schema.find((field) => field.name === name);
|
||||
}
|
||||
|
||||
function getSiblingsFieldNames(currentField) {
|
||||
let result = [];
|
||||
function getSchemaFieldIndex(field) {
|
||||
return nonDeletedFields.findIndex((f) => f === field);
|
||||
}
|
||||
|
||||
if (currentField.toDelete) {
|
||||
return result;
|
||||
function replaceIndexesColumn(oldName, newName) {
|
||||
if (!collection?.schema?.length || oldName === newName || !newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let field of collection.schema) {
|
||||
if (field === currentField || field.toDelete) {
|
||||
continue; // skip current and deleted fields
|
||||
}
|
||||
|
||||
result.push(field.name);
|
||||
}
|
||||
|
||||
return result;
|
||||
// update indexes on renamed fields
|
||||
collection.indexes = collection.indexes.map((idx) =>
|
||||
CommonHelper.replaceIndexColumn(idx, oldName, newName)
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
@@ -124,6 +120,9 @@
|
||||
}
|
||||
|
||||
collection.schema = newSchema;
|
||||
|
||||
// reset errors since the schema keys index has changed
|
||||
setErrors({});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -144,26 +143,22 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="accordions">
|
||||
<div class="accordions schema-fields">
|
||||
{#each collection.schema as field, i (field)}
|
||||
<FieldAccordion
|
||||
<svelte:component
|
||||
this={fieldComponents[field.type]}
|
||||
key={getSchemaFieldIndex(field)}
|
||||
bind:field
|
||||
key={i}
|
||||
excludeNames={reservedNames.concat(getSiblingsFieldNames(field))}
|
||||
on:remove={() => removeField(i)}
|
||||
on:dragstart={(e) => onFieldDrag(e?.detail, i)}
|
||||
on:drop={(e) => onFieldDrop(e?.detail, i)}
|
||||
on:rename={(e) => replaceIndexesColumn(e.detail.oldName, e.detail.newName)}
|
||||
on:dragstart={(e) => onFieldDrag(e.detail, i)}
|
||||
on:drop={(e) => onFieldDrop(e.detail, i)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="clearfix m-t-xs" />
|
||||
<NewField class="btn btn-block btn-outline" on:select={(e) => newField(e.detail)} />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-block {collection.schema.length ? 'btn-transparent' : 'btn-secondary'}"
|
||||
on:click={newField}
|
||||
>
|
||||
<i class="ri-add-line" />
|
||||
<span class="txt">New field</span>
|
||||
</button>
|
||||
<div class="clearfix m-b-base" />
|
||||
|
||||
<IndexesList bind:collection />
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
this={codeEditorComponent}
|
||||
id={uniqueId}
|
||||
placeholder="eg. SELECT id, name from posts"
|
||||
language="sql"
|
||||
language="sql-select"
|
||||
minHeight="150"
|
||||
on:change={() => {
|
||||
if (schemaErrors.length) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
let showFiltersInfo = false;
|
||||
</script>
|
||||
|
||||
<div class="block m-b-base handle" class:fade={!showFiltersInfo}>
|
||||
<div class="block m-b-sm handle" class:fade={!showFiltersInfo}>
|
||||
<div class="flex txt-sm txt-hint m-b-5">
|
||||
<p>
|
||||
All rules follow the
|
||||
@@ -77,22 +77,17 @@
|
||||
|
||||
<RuleField label="List/Search rule" formKey="listRule" {collection} bind:rule={collection.listRule} />
|
||||
|
||||
<hr />
|
||||
<RuleField label="View rule" formKey="viewRule" {collection} bind:rule={collection.viewRule} />
|
||||
|
||||
{#if !collection?.isView}
|
||||
<hr />
|
||||
<RuleField label="Create rule" formKey="createRule" {collection} bind:rule={collection.createRule} />
|
||||
|
||||
<hr />
|
||||
<RuleField label="Update rule" formKey="updateRule" {collection} bind:rule={collection.updateRule} />
|
||||
|
||||
<hr />
|
||||
<RuleField label="Delete rule" formKey="deleteRule" {collection} bind:rule={collection.deleteRule} />
|
||||
{/if}
|
||||
|
||||
{#if collection?.isAuth}
|
||||
<hr />
|
||||
<RuleField
|
||||
label="Manage rule"
|
||||
formKey="options.manageRule"
|
||||
|
||||
@@ -69,6 +69,13 @@
|
||||
collection.deleteRule = null;
|
||||
}
|
||||
|
||||
// update indexes on collection rename
|
||||
$: if (collection?.name && original?.name != collection?.name) {
|
||||
collection.indexes = collection.indexes.map((idx) =>
|
||||
CommonHelper.replaceIndexTableName(idx, collection.name)
|
||||
);
|
||||
}
|
||||
|
||||
export function changeTab(newTab) {
|
||||
activeTab = newTab;
|
||||
}
|
||||
@@ -228,6 +235,15 @@
|
||||
field.id = "";
|
||||
}
|
||||
}
|
||||
|
||||
// update indexes with the new table name
|
||||
if (!CommonHelper.isEmpty(clone.indexes)) {
|
||||
for (let i = 0; i < clone.indexes.length; i++) {
|
||||
const parsed = CommonHelper.parseIndex(clone.indexes[i]);
|
||||
parsed.tableName = clone.name;
|
||||
clone.indexes[i] = CommonHelper.buildIndex(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
show(clone);
|
||||
@@ -241,6 +257,7 @@
|
||||
<OverlayPanel
|
||||
bind:this={collectionPanel}
|
||||
class="overlay-panel-lg colored-header collection-panel"
|
||||
escClose={false}
|
||||
beforeHide={() => {
|
||||
if (hasChanges && confirmClose) {
|
||||
confirm("You have unsaved changes. Do you really want to close the panel?", () => {
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
<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 Toggler from "@/components/base/Toggler.svelte";
|
||||
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 EditorOptions from "@/components/collections/schema/EditorOptions.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";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$: 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.originalName && 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
|
||||
}
|
||||
|
||||
$: if (field.required) {
|
||||
field.nullable = false;
|
||||
}
|
||||
|
||||
$: interactive = !disabled && !field.system && !field.toDelete && canBeStored;
|
||||
|
||||
$: hasValidName = validateFieldName(field.name);
|
||||
|
||||
$: hasErrors =
|
||||
!hasValidName || !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 validateFieldName(name) {
|
||||
name = ("" + name).toLowerCase();
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const excluded of excludeNames) {
|
||||
if (excluded.toLowerCase() === name) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalizeFieldName(name) {
|
||||
return CommonHelper.slugify(name);
|
||||
}
|
||||
|
||||
function requiredLabel(field) {
|
||||
switch (field?.type) {
|
||||
case "bool":
|
||||
return "Nonfalsey";
|
||||
case "number":
|
||||
return "Nonzero";
|
||||
default:
|
||||
return "Nonempty";
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (field?.onMountExpand) {
|
||||
field.onMountExpand = false; // auto expand only the first time
|
||||
expand();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Accordion
|
||||
bind:this={accordion}
|
||||
on:expand
|
||||
on:collapse
|
||||
on:toggle
|
||||
on:dragenter
|
||||
on:dragleave
|
||||
on:dragstart
|
||||
on:drop
|
||||
draggable
|
||||
single
|
||||
{interactive}
|
||||
class={disabled || field.toDelete || field.system ? "field-accordion disabled" : "field-accordion"}
|
||||
{...$$restProps}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<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">{requiredLabel(field)}</span>
|
||||
{/if}
|
||||
{#if field.unique}
|
||||
<span class="label label-success">Unique</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex-fill" />
|
||||
|
||||
{#if hasErrors && !field.system}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={{ text: "Has errors", position: "left" }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if field.toDelete}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger btn-transparent"
|
||||
on:click|stopPropagation={() => {
|
||||
field.toDelete = false;
|
||||
}}
|
||||
>
|
||||
<span class="txt">Restore</span>
|
||||
</button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<form
|
||||
class="field-form"
|
||||
on:dragstart={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}}
|
||||
on:submit|preventDefault={() => {
|
||||
canBeStored && collapse();
|
||||
}}
|
||||
>
|
||||
<div class="grid">
|
||||
<div class="col-sm-6">
|
||||
<Field
|
||||
class="form-field required {field.id ? 'readonly' : ''}"
|
||||
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
|
||||
{!hasValidName ? 'invalid' : ''}
|
||||
{field.id && field.system ? 'disabled' : ''}
|
||||
"
|
||||
name="schema.{key}.name"
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Name</span>
|
||||
{#if !hasValidName}
|
||||
<span class="txt invalid-name-note" transition:fly={{ duration: 150, x: 5 }}>
|
||||
Duplicated or invalid name
|
||||
</span>
|
||||
{/if}
|
||||
</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 === "editor"}
|
||||
<EditorOptions {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} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4 flex">
|
||||
<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}>
|
||||
<span class="txt">{requiredLabel(field)}</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: `Requires the field value to be ${requiredLabel(
|
||||
field
|
||||
)}\n(aka. not ${CommonHelper.zeroDefaultStr(field)}).`,
|
||||
position: "right",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4 flex">
|
||||
{#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>
|
||||
|
||||
{#if !field.toDelete}
|
||||
<div class="col-sm-4 txt-right">
|
||||
<div class="flex-fill" />
|
||||
<div class="inline-flex flex-gap-sm flex-nowrap">
|
||||
<button type="button" aria-label="More" class="btn btn-circle btn-sm btn-transparent">
|
||||
<i class="ri-more-line" />
|
||||
<Toggler
|
||||
class="dropdown dropdown-sm dropdown-upside dropdown-right dropdown-nowrap no-min-width"
|
||||
>
|
||||
<button type="button" class="dropdown-item txt-right" on:click={handleDelete}>
|
||||
<span class="txt">Remove</span>
|
||||
</button>
|
||||
</Toggler>
|
||||
</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>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<input type="submit" class="hidden" tabindex="-1" />
|
||||
</form>
|
||||
</Accordion>
|
||||
|
||||
<style>
|
||||
.invalid-name-note {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
text-transform: none;
|
||||
}
|
||||
.title.field-name {
|
||||
max-width: 130px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,162 @@
|
||||
<script>
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let collection;
|
||||
|
||||
let panel;
|
||||
let original = "";
|
||||
let index = "";
|
||||
let key = "";
|
||||
let codeEditorComponent;
|
||||
let isCodeEditorComponentLoading = false;
|
||||
|
||||
$: presetColumns = collection?.schema?.map((f) => f.name) || [];
|
||||
|
||||
$: indexParts = CommonHelper.parseIndex(index);
|
||||
|
||||
$: indexColumns = indexParts.columns?.map((c) => c.column) || [];
|
||||
|
||||
export function show(showIndex, showKey) {
|
||||
key = !CommonHelper.isEmpty(showKey) ? showKey : "";
|
||||
original = showIndex || blankIndex();
|
||||
index = original;
|
||||
|
||||
return panel?.show();
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
return panel?.hide();
|
||||
}
|
||||
|
||||
function blankIndex() {
|
||||
const parsed = CommonHelper.parseIndex("");
|
||||
parsed.tableName = collection?.name || "";
|
||||
|
||||
return CommonHelper.buildIndex(parsed);
|
||||
}
|
||||
|
||||
function remove() {
|
||||
dispatch("remove", original);
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (!indexColumns.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch("submit", {
|
||||
old: original,
|
||||
new: index,
|
||||
});
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
function toggleColumn(column) {
|
||||
const clone = CommonHelper.clone(indexParts);
|
||||
|
||||
const col = clone.columns.find((c) => c.column == column);
|
||||
if (col) {
|
||||
CommonHelper.removeByValue(clone.columns, col);
|
||||
} else {
|
||||
CommonHelper.pushUnique(clone.columns, { column });
|
||||
}
|
||||
|
||||
index = CommonHelper.buildIndex(clone);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
isCodeEditorComponentLoading = true;
|
||||
|
||||
try {
|
||||
codeEditorComponent = (await import("@/components/base/CodeEditor.svelte")).default;
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
|
||||
isCodeEditorComponentLoading = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<OverlayPanel bind:this={panel} popup on:hide on:show {...$$restProps}>
|
||||
<svelte:fragment slot="header">
|
||||
<h4>{original ? "Update" : "Create"} index</h4>
|
||||
</svelte:fragment>
|
||||
|
||||
<Field class="form-field form-field-toggle m-b-sm" let:uniqueId>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={uniqueId}
|
||||
checked={indexParts.unique}
|
||||
on:change={(e) => {
|
||||
indexParts.unique = e.target.checked;
|
||||
indexParts.tableName = indexParts.tableName || collection?.name;
|
||||
index = CommonHelper.buildIndex(indexParts);
|
||||
}}
|
||||
/>
|
||||
<label for={uniqueId}>Unique</label>
|
||||
</Field>
|
||||
|
||||
<Field class="form-field required m-b-sm" name={`indexes.${key || ""}`} let:uniqueId>
|
||||
{#if isCodeEditorComponentLoading}
|
||||
<textarea disabled rows="7" placeholder="Loading..." />
|
||||
{:else}
|
||||
<svelte:component
|
||||
this={codeEditorComponent}
|
||||
id={uniqueId}
|
||||
placeholder={`eg. CREATE INDEX idx_test on ${collection?.name} (created)`}
|
||||
language="sql-create-index"
|
||||
minHeight="85"
|
||||
bind:value={index}
|
||||
/>
|
||||
{/if}
|
||||
</Field>
|
||||
|
||||
{#if presetColumns.length > 0}
|
||||
<div class="inline-flex gap-10">
|
||||
<span class="txt txt-hint">Presets</span>
|
||||
{#each presetColumns as column}
|
||||
<button
|
||||
type="button"
|
||||
class="label link-primary"
|
||||
class:label-info={indexColumns.includes(column)}
|
||||
on:click={() => toggleColumn(column)}
|
||||
>
|
||||
{column}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
{#if original != ""}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-hint btn-transparent m-r-auto"
|
||||
use:tooltip={{ text: "Delete", position: "top" }}
|
||||
on:click={() => remove()}
|
||||
>
|
||||
<i class="ri-delete-bin-7-line" />
|
||||
</button>
|
||||
{/if}
|
||||
<button type="button" class="btn btn-transparent" on:click={() => hide()}>
|
||||
<span class="txt">Cancel</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
class:btn-disabled={indexColumns.length <= 0}
|
||||
on:click={() => submit()}
|
||||
>
|
||||
<span class="txt">Set index</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</OverlayPanel>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { errors, removeError } from "@/stores/errors";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import IndexUpsertPanel from "@/components/collections/IndexUpsertPanel.svelte";
|
||||
|
||||
export let collection;
|
||||
|
||||
let upsertPanel;
|
||||
|
||||
function pushOrReplace(oldIndex, newIndex) {
|
||||
for (let i = 0; i < collection.indexes.length; i++) {
|
||||
// replace
|
||||
if (collection.indexes[i] == oldIndex) {
|
||||
collection.indexes[i] = newIndex;
|
||||
removeError("indexes." + i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// push missing
|
||||
collection.indexes.push(newIndex);
|
||||
collection.indexes = collection.indexes;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="section-title">
|
||||
Unique constraints and indexes ({collection?.indexes?.length || 0})
|
||||
</div>
|
||||
<div class="indexes-list">
|
||||
{#each collection?.indexes || [] as rawIndex, i}
|
||||
{@const parsed = CommonHelper.parseIndex(rawIndex)}
|
||||
<button
|
||||
type="button"
|
||||
class="label link-primary {$errors.indexes?.[i]?.message ? 'label-danger' : 'label-info'}"
|
||||
use:tooltip={$errors.indexes?.[i]?.message || ""}
|
||||
on:click={() => upsertPanel?.show(rawIndex, i)}
|
||||
>
|
||||
{#if parsed.unique}
|
||||
<strong>Unique:</strong>
|
||||
{/if}
|
||||
<span class="txt">
|
||||
{parsed.columns?.map((c) => c.column).join(", ")}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
<button type="button" class="label label-primary link-fade" on:click={() => upsertPanel?.show()}>
|
||||
<span class="txt">+</span>
|
||||
<span class="txt">New index</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<IndexUpsertPanel
|
||||
bind:this={upsertPanel}
|
||||
bind:collection
|
||||
on:remove={(e) => {
|
||||
for (let i = 0; i < collection.indexes.length; i++) {
|
||||
if (collection.indexes[i] == e.detail) {
|
||||
collection.indexes.splice(i, 1);
|
||||
removeError("indexes." + i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
collection.indexes = collection.indexes;
|
||||
}}
|
||||
on:submit={(e) => {
|
||||
pushOrReplace(e.detail.old, e.detail.new);
|
||||
}}
|
||||
/>
|
||||
|
||||
<style lang="scss">
|
||||
.indexes-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
.label {
|
||||
overflow: hidden;
|
||||
min-width: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +0,0 @@
|
||||
<script>
|
||||
// svelte-ignore unused-export-let
|
||||
export let key = "";
|
||||
// svelte-ignore unused-export-let
|
||||
export let options = {};
|
||||
</script>
|
||||
@@ -1,34 +0,0 @@
|
||||
<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>
|
||||
@@ -1,6 +0,0 @@
|
||||
<script>
|
||||
// svelte-ignore unused-export-let
|
||||
export let key = "";
|
||||
// svelte-ignore unused-export-let
|
||||
export let options = {};
|
||||
</script>
|
||||
@@ -1,53 +0,0 @@
|
||||
<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: 'List of domains that are NOT allowed. \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: 'List of domains that are ONLY allowed. \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>
|
||||
@@ -1,208 +0,0 @@
|
||||
<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 ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
import MimeTypeSelectOption from "@/components/base/MimeTypeSelectOption.svelte";
|
||||
import MultipleValueInput from "@/components/base/MultipleValueInput.svelte";
|
||||
import baseMimeTypesList from "@/mimes.js";
|
||||
|
||||
export let key = "";
|
||||
export let options = {};
|
||||
|
||||
let mimeTypesList = baseMimeTypesList.slice();
|
||||
|
||||
$: if (CommonHelper.isEmpty(options)) {
|
||||
// load defaults
|
||||
options = {
|
||||
maxSelect: 1,
|
||||
maxSize: 5242880,
|
||||
thumbs: [],
|
||||
mimeTypes: [],
|
||||
};
|
||||
} else {
|
||||
appendMissingMimeTypes();
|
||||
}
|
||||
|
||||
// append any previously set custom mime types to the predefined
|
||||
// list for backward compatibility
|
||||
function appendMissingMimeTypes() {
|
||||
if (CommonHelper.isEmpty(options.mimeTypes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const missing = [];
|
||||
|
||||
for (const v of options.mimeTypes) {
|
||||
if (!!mimeTypesList.find((item) => item.mimeType === v)) {
|
||||
continue; // exist
|
||||
}
|
||||
|
||||
missing.push({ mimeType: v });
|
||||
}
|
||||
|
||||
if (missing.length) {
|
||||
mimeTypesList = mimeTypesList.concat(missing);
|
||||
}
|
||||
}
|
||||
</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">Allowed mime types</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: "Allow files ONLY with the listed mime types. \n Leave empty for no restriction.",
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<ObjectSelect
|
||||
id={uniqueId}
|
||||
multiple
|
||||
searchable
|
||||
closable={false}
|
||||
selectionKey="mimeType"
|
||||
selectPlaceholder="No restriction"
|
||||
items={mimeTypesList}
|
||||
labelComponent={MimeTypeSelectOption}
|
||||
optionComponent={MimeTypeSelectOption}
|
||||
bind:keyOfSelected={options.mimeTypes}
|
||||
/>
|
||||
<div class="help-block">
|
||||
<button type="button" class="inline-flex flex-gap-0">
|
||||
<span class="txt link-primary">Choose presets</span>
|
||||
<i class="ri-arrow-drop-down-fill" />
|
||||
<Toggler class="dropdown dropdown-sm dropdown-nowrap dropdown-left">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => {
|
||||
options.mimeTypes = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
];
|
||||
}}
|
||||
>
|
||||
<span class="txt">Images (jpg, png, svg, gif, webp)</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => {
|
||||
options.mimeTypes = [
|
||||
"video/mp4",
|
||||
"video/x-ms-wmv",
|
||||
"video/quicktime",
|
||||
"video/3gpp",
|
||||
];
|
||||
}}
|
||||
>
|
||||
<span class="txt">Videos (mp4, avi, mov, 3gp)</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
</button>
|
||||
</Toggler>
|
||||
</button>
|
||||
</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 additional thumb sizes for image files, along with the default thumb size of 100x100. The thumbs are generated lazily on first access.",
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<MultipleValueInput id={uniqueId} placeholder="eg. 50x50, 480x720" bind:value={options.thumbs} />
|
||||
<div class="help-block">
|
||||
<span class="txt">Use comma as separator.</span>
|
||||
<button type="button" class="inline-flex flex-gap-0">
|
||||
<span class="txt link-primary">Supported formats</span>
|
||||
<i class="ri-arrow-drop-down-fill" />
|
||||
<Toggler class="dropdown dropdown-sm dropdown-center dropdown-nowrap p-r-10">
|
||||
<ul class="m-0">
|
||||
<li>
|
||||
<strong>WxH</strong>
|
||||
(eg. 100x50) - crop to WxH viewbox (from center)
|
||||
</li>
|
||||
<li>
|
||||
<strong>WxHt</strong>
|
||||
(eg. 100x50t) - crop to WxH viewbox (from top)
|
||||
</li>
|
||||
<li>
|
||||
<strong>WxHb</strong>
|
||||
(eg. 100x50b) - crop to WxH viewbox (from bottom)
|
||||
</li>
|
||||
<li>
|
||||
<strong>WxHf</strong>
|
||||
(eg. 100x50f) - fit inside a WxH viewbox (without cropping)
|
||||
</li>
|
||||
<li>
|
||||
<strong>0xH</strong>
|
||||
(eg. 0x50) - resize to H height preserving the aspect ratio
|
||||
</li>
|
||||
<li>
|
||||
<strong>Wx0</strong>
|
||||
(eg. 100x0) - resize to W width preserving the aspect ratio
|
||||
</li>
|
||||
</ul>
|
||||
</Toggler>
|
||||
</button>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,50 +0,0 @@
|
||||
<script>
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
// svelte-ignore unused-export-let
|
||||
export const key = "";
|
||||
// svelte-ignore unused-export-let
|
||||
export const options = {};
|
||||
|
||||
let showInfo = false;
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex txt-sm flex-gap-5 link-hint"
|
||||
on:click={() => {
|
||||
showInfo = !showInfo;
|
||||
}}
|
||||
>
|
||||
<strong class="txt">String value normalizations</strong>
|
||||
{#if showInfo}
|
||||
<i class="ri-arrow-up-s-line txt-sm" />
|
||||
{:else}
|
||||
<i class="ri-arrow-down-s-line txt-sm" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if showInfo}
|
||||
<div class="block" transition:slide|local={{ duration: 150 }}>
|
||||
<div class="alert alert-warning m-b-0 m-t-10">
|
||||
<div class="content">
|
||||
In order to support seamlessly both <code>application/json</code> and
|
||||
<code>multipart/form-data</code>
|
||||
requests, the following normalization rules are applied if the <code>json</code> field is a
|
||||
<strong>plain string</strong>:
|
||||
<ul>
|
||||
<li>"true" is converted to the json <code>true</code></li>
|
||||
<li>"false" is converted to the json <code>false</code></li>
|
||||
<li>"null" is converted to the json <code>null</code></li>
|
||||
<li>"[1,2,3]" is converted to the json <code>[1,2,3]</code></li>
|
||||
<li>{'"{"a":1,"b":2}"'} is converted to the json <code>{'{"a":1,"b":2}'}</code></li>
|
||||
<li>numeric strings are converted to json number</li>
|
||||
<li>double quoted strings are left as they are (aka. without normalizations)</li>
|
||||
<li>any other string (empty string too) is double quoted</li>
|
||||
</ul>
|
||||
Alternatively, if you want to avoid the string value normalizations, you can wrap your data inside
|
||||
an object, eg.<code>{'{"data": anything}'}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
+49
-4
@@ -1,12 +1,13 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
|
||||
export let value = "text";
|
||||
import Toggler from "@/components/base/Toggler.svelte";
|
||||
|
||||
let classes = "";
|
||||
export { classes as class }; // export reserved keyword
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const types = [
|
||||
{
|
||||
label: "Plain text",
|
||||
@@ -64,6 +65,50 @@
|
||||
icon: CommonHelper.getFieldTypeIcon("json"),
|
||||
},
|
||||
];
|
||||
|
||||
function select(fieldType) {
|
||||
dispatch("select", fieldType);
|
||||
}
|
||||
</script>
|
||||
|
||||
<ObjectSelect class="field-type-select {classes}" items={types} bind:keyOfSelected={value} {...$$restProps} />
|
||||
<button type="button" class={classes} on:click={dispatch}>
|
||||
<i class="ri-add-line" />
|
||||
<div class="txt">New field</div>
|
||||
<Toggler class="dropdown field-types-dropdown">
|
||||
{#each types as item}
|
||||
<div
|
||||
tabindex="0"
|
||||
class="dropdown-item closable"
|
||||
on:click|stopPropagation={() => {
|
||||
select(item.value);
|
||||
}}
|
||||
on:keydown|stopPropagation={(e) => {
|
||||
if (e.code === "Enter" || e.code === "Space") {
|
||||
select(item.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i class="icon {item.icon}" />
|
||||
<span class="txt">{item.label}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</Toggler>
|
||||
</button>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.field-types-dropdown) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
box-shadow: 0px 0px 0px 2px var(--primaryColor);
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
.dropdown-item {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +0,0 @@
|
||||
<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>
|
||||
@@ -1,187 +0,0 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import Select from "@/components/base/Select.svelte";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
import CollectionUpsertPanel from "@/components/collections/CollectionUpsertPanel.svelte";
|
||||
import { collections } from "@/stores/collections";
|
||||
|
||||
export let key = "";
|
||||
export let options = {};
|
||||
|
||||
const isSingleOptions = [
|
||||
{ label: "Single", value: true },
|
||||
{ label: "Multiple", value: false },
|
||||
];
|
||||
|
||||
const defaultOptions = [
|
||||
{ label: "False", value: false },
|
||||
{ label: "True", value: true },
|
||||
];
|
||||
|
||||
const baseFields = ["id", "created", "updated"];
|
||||
|
||||
const authFields = ["username", "email", "emailVisibility", "verified"];
|
||||
|
||||
let upsertPanel = null;
|
||||
let displayFieldsList = [];
|
||||
let oldCollectionId = null;
|
||||
let isSingle = options?.maxSelect == 1;
|
||||
let oldIsSingle = isSingle;
|
||||
|
||||
// load defaults
|
||||
$: if (CommonHelper.isEmpty(options)) {
|
||||
options = {
|
||||
maxSelect: 1,
|
||||
collectionId: null,
|
||||
cascadeDelete: false,
|
||||
displayFields: [],
|
||||
};
|
||||
isSingle = true;
|
||||
oldIsSingle = isSingle;
|
||||
}
|
||||
|
||||
$: if (oldIsSingle != isSingle) {
|
||||
oldIsSingle = isSingle;
|
||||
if (isSingle) {
|
||||
options.minSelect = null;
|
||||
options.maxSelect = 1;
|
||||
} else {
|
||||
options.maxSelect = null;
|
||||
}
|
||||
}
|
||||
|
||||
$: selectedColection = $collections.find((c) => c.id == options.collectionId) || null;
|
||||
|
||||
$: if (oldCollectionId != options.collectionId) {
|
||||
oldCollectionId = options.collectionId;
|
||||
refreshDisplayFieldsList();
|
||||
}
|
||||
|
||||
function refreshDisplayFieldsList() {
|
||||
displayFieldsList = baseFields.slice(0);
|
||||
if (!selectedColection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedColection.isAuth) {
|
||||
displayFieldsList = displayFieldsList.concat(authFields);
|
||||
}
|
||||
|
||||
for (const field of selectedColection.schema) {
|
||||
displayFieldsList.push(field.name);
|
||||
}
|
||||
|
||||
// deselect any missing display field
|
||||
if (options?.displayFields?.length > 0) {
|
||||
for (let i = options.displayFields.length - 1; i >= 0; i--) {
|
||||
if (!displayFieldsList.includes(options.displayFields[i])) {
|
||||
options.displayFields.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field required" name="schema.{key}.options.collectionId" let:uniqueId>
|
||||
<label for={uniqueId}>Collection</label>
|
||||
<ObjectSelect
|
||||
id={uniqueId}
|
||||
searchable={$collections.length > 5}
|
||||
selectPlaceholder={"Select collection"}
|
||||
noOptionsText="No collections found"
|
||||
selectionKey="id"
|
||||
items={$collections}
|
||||
bind:keyOfSelected={options.collectionId}
|
||||
>
|
||||
<svelte:fragment slot="afterOptions">
|
||||
<hr />
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent btn-block btn-sm"
|
||||
on:click={() => upsertPanel?.show()}
|
||||
>
|
||||
<i class="ri-add-line" />
|
||||
<span class="txt">New collection</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</ObjectSelect>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" let:uniqueId>
|
||||
<label for={uniqueId}>Relation type</label>
|
||||
<ObjectSelect id={uniqueId} items={isSingleOptions} bind:keyOfSelected={isSingle} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{#if !isSingle}
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.minSelect" let:uniqueId>
|
||||
<label for={uniqueId}>Min select</label>
|
||||
<input
|
||||
type="number"
|
||||
id={uniqueId}
|
||||
step="1"
|
||||
min="1"
|
||||
placeholder="No min limit"
|
||||
bind:value={options.minSelect}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.maxSelect" let:uniqueId>
|
||||
<label for={uniqueId}>Max select</label>
|
||||
<input
|
||||
type="number"
|
||||
id={uniqueId}
|
||||
step="1"
|
||||
placeholder="No max limit"
|
||||
min={options.minSelect || 2}
|
||||
bind:value={options.maxSelect}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.displayFields" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Display fields</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: "Optionally select the field(s) that will be used in the listings UI. Leave empty for auto.",
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<Select
|
||||
multiple
|
||||
searchable
|
||||
id={uniqueId}
|
||||
selectPlaceholder="Auto"
|
||||
items={displayFieldsList}
|
||||
bind:selected={options.displayFields}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.cascadeDelete" let:uniqueId>
|
||||
<label for={uniqueId}>Delete main record on relation delete</label>
|
||||
<ObjectSelect id={uniqueId} items={defaultOptions} bind:keyOfSelected={options.cascadeDelete} />
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CollectionUpsertPanel
|
||||
bind:this={upsertPanel}
|
||||
on:save={(e) => {
|
||||
if (e?.detail?.collection?.id) {
|
||||
options.collectionId = e.detail.collection.id;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,217 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { SchemaField } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import { errors, setErrors } from "@/stores/errors";
|
||||
import Toggler from "@/components/base/Toggler.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let key = "";
|
||||
export let field = new SchemaField();
|
||||
|
||||
let optionsTrigger;
|
||||
let isDragOver = false;
|
||||
|
||||
$: if (field.toDelete) {
|
||||
// reset the name if it was previously deleted
|
||||
if (field.originalName && 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
|
||||
}
|
||||
|
||||
$: if (field.required) {
|
||||
field.nullable = false;
|
||||
}
|
||||
|
||||
$: interactive = !field.toDelete && !(field.id && field.system);
|
||||
|
||||
$: hasErrors = !CommonHelper.isEmpty(CommonHelper.getNestedVal($errors, `schema.${key}`));
|
||||
|
||||
function remove() {
|
||||
if (!field.id) {
|
||||
dispatch("remove");
|
||||
} else {
|
||||
field.toDelete = true;
|
||||
}
|
||||
}
|
||||
|
||||
function restore() {
|
||||
field.toDelete = false;
|
||||
|
||||
// reset all errors since the error index key would have been changed
|
||||
setErrors({});
|
||||
}
|
||||
|
||||
function normalizeFieldName(name) {
|
||||
return CommonHelper.slugify(name);
|
||||
}
|
||||
|
||||
function requiredLabel(field) {
|
||||
switch (field?.type) {
|
||||
case "bool":
|
||||
return "Nonfalsey";
|
||||
case "number":
|
||||
return "Nonzero";
|
||||
default:
|
||||
return "Nonempty";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
draggable={true}
|
||||
class="schema-field"
|
||||
class:drag-over={isDragOver}
|
||||
on:dragstart={(e) => {
|
||||
if (!e.target.classList.contains("drag-handle-wrapper")) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const blank = document.createElement("div");
|
||||
e.dataTransfer.setDragImage(blank, 0, 0);
|
||||
interactive && dispatch("dragstart", e);
|
||||
}}
|
||||
on:dragenter={(e) => {
|
||||
if (interactive) {
|
||||
isDragOver = true;
|
||||
dispatch("dragenter", e);
|
||||
}
|
||||
}}
|
||||
on:drop|preventDefault={(e) => {
|
||||
if (interactive) {
|
||||
isDragOver = false;
|
||||
dispatch("drop", e);
|
||||
}
|
||||
}}
|
||||
on:dragleave={(e) => {
|
||||
if (interactive) {
|
||||
isDragOver = false;
|
||||
dispatch("dragleave", e);
|
||||
}
|
||||
}}
|
||||
on:dragover|preventDefault
|
||||
>
|
||||
<div class="schema-field-header">
|
||||
{#if interactive}
|
||||
<div class="drag-handle-wrapper" draggable="true" aria-label="Sort">
|
||||
<span class="drag-handle" />
|
||||
</div>
|
||||
{/if}
|
||||
<Field
|
||||
class="form-field required m-0 {!interactive ? 'disabled' : ''}"
|
||||
name="schema.{key}.name"
|
||||
inlineError
|
||||
>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="form-field-addon prefix no-pointer-events" class:txt-disabled={!interactive}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
</div>
|
||||
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
disabled={!interactive}
|
||||
readonly={field.id && field.system}
|
||||
spellcheck="false"
|
||||
autofocus={!field.id}
|
||||
placeholder="Field name"
|
||||
value={field.name}
|
||||
on:input={(e) => {
|
||||
const oldName = field.name;
|
||||
field.name = normalizeFieldName(e.target.value);
|
||||
e.target.value = field.name;
|
||||
|
||||
dispatch("rename", { oldName: oldName, newName: field.name });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<slot {interactive} {hasErrors} />
|
||||
|
||||
{#if field.toDelete}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-warning btn-transparent options-trigger"
|
||||
aria-label="Restore"
|
||||
use:tooltip={"Restore"}
|
||||
on:click={restore}
|
||||
>
|
||||
<i class="ri-restart-line" />
|
||||
</button>
|
||||
{:else if interactive}
|
||||
<button
|
||||
bind:this={optionsTrigger}
|
||||
type="button"
|
||||
aria-label="Field options"
|
||||
class="btn btn-sm btn-circle btn-transparent options-trigger {hasErrors
|
||||
? 'btn-danger'
|
||||
: 'btn-hint'}"
|
||||
>
|
||||
<i class="ri-settings-3-line" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if interactive}
|
||||
<Toggler class="dropdown dropdown-block schema-field-dropdown" trigger={optionsTrigger}>
|
||||
<div class="grid grid-sm">
|
||||
<div class="col-sm-12 hidden-empty">
|
||||
<slot name="options" {interactive} {hasErrors} />
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4 flex">
|
||||
<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}>
|
||||
<span class="txt">{requiredLabel(field)}</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: `Requires the field value NOT to be ${CommonHelper.zeroDefaultStr(
|
||||
field
|
||||
)}.`,
|
||||
position: "right",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{#if !field.toDelete}
|
||||
<div class="col-sm-4 m-l-auto txt-right">
|
||||
<div class="flex-fill" />
|
||||
<div class="inline-flex flex-gap-sm flex-nowrap">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="More"
|
||||
class="btn btn-circle btn-sm btn-transparent"
|
||||
>
|
||||
<i class="ri-more-line" />
|
||||
<Toggler
|
||||
class="dropdown dropdown-sm dropdown-upside dropdown-right dropdown-nowrap no-min-width"
|
||||
>
|
||||
<button type="button" class="dropdown-item txt-right" on:click={remove}>
|
||||
<span class="txt">Remove</span>
|
||||
</button>
|
||||
</Toggler>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Toggler>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import SchemaField from "@/components/collections/schema/SchemaField.svelte";
|
||||
|
||||
export let field;
|
||||
export let key = "";
|
||||
</script>
|
||||
|
||||
<SchemaField
|
||||
bind:field
|
||||
{key}
|
||||
on:rename
|
||||
on:remove
|
||||
on:drop
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragleave
|
||||
{...$$restProps}
|
||||
/>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script>
|
||||
import Flatpickr from "svelte-flatpickr";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import SchemaField from "@/components/collections/schema/SchemaField.svelte";
|
||||
|
||||
export let field;
|
||||
|
||||
export let key = "";
|
||||
</script>
|
||||
|
||||
<SchemaField
|
||||
bind:field
|
||||
{key}
|
||||
on:rename
|
||||
on:remove
|
||||
on:drop
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragleave
|
||||
{...$$restProps}
|
||||
>
|
||||
<svelte:fragment slot="options">
|
||||
<div class="grid grid-sm">
|
||||
<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={field.options.min}
|
||||
bind:formattedValue={field.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={field.options.max}
|
||||
bind:formattedValue={field.options.max}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</SchemaField>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import SchemaField from "@/components/collections/schema/SchemaField.svelte";
|
||||
|
||||
export let field;
|
||||
export let key = "";
|
||||
</script>
|
||||
|
||||
<SchemaField
|
||||
bind:field
|
||||
{key}
|
||||
on:rename
|
||||
on:remove
|
||||
on:drop
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragleave
|
||||
{...$$restProps}
|
||||
/>
|
||||
@@ -0,0 +1,68 @@
|
||||
<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";
|
||||
import SchemaField from "@/components/collections/schema/SchemaField.svelte";
|
||||
|
||||
export let field;
|
||||
export let key = "";
|
||||
</script>
|
||||
|
||||
<SchemaField
|
||||
bind:field
|
||||
{key}
|
||||
on:rename
|
||||
on:remove
|
||||
on:drop
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragleave
|
||||
{...$$restProps}
|
||||
>
|
||||
<svelte:fragment slot="options">
|
||||
<div class="grid grid-sm">
|
||||
<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: 'List of domains that are NOT allowed. \n This field is disabled if "Only domains" is set.',
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<MultipleValueInput
|
||||
id={uniqueId}
|
||||
disabled={!CommonHelper.isEmpty(field.options.onlyDomains)}
|
||||
bind:value={field.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: 'List of domains that are ONLY allowed. \n This field is disabled if "Except domains" is set.',
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<MultipleValueInput
|
||||
id="{uniqueId}.options.onlyDomains"
|
||||
disabled={!CommonHelper.isEmpty(field.options.exceptDomains)}
|
||||
bind:value={field.options.onlyDomains}
|
||||
/>
|
||||
<div class="help-block">Use comma as separator.</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</SchemaField>
|
||||
@@ -0,0 +1,280 @@
|
||||
<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 ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
import MimeTypeSelectOption from "@/components/base/MimeTypeSelectOption.svelte";
|
||||
import MultipleValueInput from "@/components/base/MultipleValueInput.svelte";
|
||||
import SchemaField from "@/components/collections/schema/SchemaField.svelte";
|
||||
import baseMimeTypesList from "@/mimes.js";
|
||||
|
||||
export let field;
|
||||
export let key = "";
|
||||
|
||||
const isSingleOptions = [
|
||||
{ label: "Single", value: true },
|
||||
{ label: "Multiple", value: false },
|
||||
];
|
||||
|
||||
let mimeTypesList = baseMimeTypesList.slice();
|
||||
let isSingle = field.options?.maxSelect <= 1;
|
||||
let oldIsSingle = isSingle;
|
||||
|
||||
$: if (CommonHelper.isEmpty(field.options)) {
|
||||
loadDefaults();
|
||||
} else {
|
||||
appendMissingMimeTypes();
|
||||
}
|
||||
|
||||
$: if (oldIsSingle != isSingle) {
|
||||
oldIsSingle = isSingle;
|
||||
if (isSingle) {
|
||||
field.options.maxSelect = 1;
|
||||
} else {
|
||||
field.options.maxSelect = field.options?.values?.length || 99;
|
||||
}
|
||||
}
|
||||
|
||||
function loadDefaults() {
|
||||
field.options = {
|
||||
maxSelect: 1,
|
||||
maxSize: 5242880,
|
||||
thumbs: [],
|
||||
mimeTypes: [],
|
||||
};
|
||||
isSingle = true;
|
||||
oldIsSingle = isSingle;
|
||||
}
|
||||
|
||||
// append any previously set custom mime types to the predefined
|
||||
// list for backward compatibility
|
||||
function appendMissingMimeTypes() {
|
||||
if (CommonHelper.isEmpty(field.options.mimeTypes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const missing = [];
|
||||
|
||||
for (const v of field.options.mimeTypes) {
|
||||
if (!!mimeTypesList.find((item) => item.mimeType === v)) {
|
||||
continue; // exist
|
||||
}
|
||||
|
||||
missing.push({ mimeType: v });
|
||||
}
|
||||
|
||||
if (missing.length) {
|
||||
mimeTypesList = mimeTypesList.concat(missing);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SchemaField
|
||||
bind:field
|
||||
{key}
|
||||
on:rename
|
||||
on:remove
|
||||
on:drop
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragleave
|
||||
{...$$restProps}
|
||||
>
|
||||
<svelte:fragment let:interactive>
|
||||
<Field
|
||||
class="form-field form-field-single-multiple-select {!interactive ? 'disabled' : ''}"
|
||||
inlineError
|
||||
let:uniqueId
|
||||
>
|
||||
<ObjectSelect
|
||||
id={uniqueId}
|
||||
items={isSingleOptions}
|
||||
disabled={!interactive}
|
||||
bind:keyOfSelected={isSingle}
|
||||
/>
|
||||
</Field>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="options">
|
||||
<div class="grid grid-sm">
|
||||
<div class="col-sm-12">
|
||||
<Field class="form-field" name="schema.{key}.options.mimeTypes" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Allowed mime types</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: "Allow files ONLY with the listed mime types. \n Leave empty for no restriction.",
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<ObjectSelect
|
||||
id={uniqueId}
|
||||
multiple
|
||||
searchable
|
||||
closable={false}
|
||||
selectionKey="mimeType"
|
||||
selectPlaceholder="No restriction"
|
||||
items={mimeTypesList}
|
||||
labelComponent={MimeTypeSelectOption}
|
||||
optionComponent={MimeTypeSelectOption}
|
||||
bind:keyOfSelected={field.options.mimeTypes}
|
||||
/>
|
||||
<div class="help-block">
|
||||
<button type="button" class="inline-flex flex-gap-0">
|
||||
<span class="txt link-primary">Choose presets</span>
|
||||
<i class="ri-arrow-drop-down-fill" />
|
||||
<Toggler class="dropdown dropdown-sm dropdown-nowrap dropdown-left">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => {
|
||||
field.options.mimeTypes = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
];
|
||||
}}
|
||||
>
|
||||
<span class="txt">Images (jpg, png, svg, gif, webp)</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => {
|
||||
field.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>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => {
|
||||
field.options.mimeTypes = [
|
||||
"video/mp4",
|
||||
"video/x-ms-wmv",
|
||||
"video/quicktime",
|
||||
"video/3gpp",
|
||||
];
|
||||
}}
|
||||
>
|
||||
<span class="txt">Videos (mp4, avi, mov, 3gp)</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => {
|
||||
field.options.mimeTypes = [
|
||||
"application/zip",
|
||||
"application/x-7z-compressed",
|
||||
"application/x-rar-compressed",
|
||||
];
|
||||
}}
|
||||
>
|
||||
<span class="txt">Archives (zip, 7zip, rar)</span>
|
||||
</button>
|
||||
</Toggler>
|
||||
</button>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class={!isSingle ? "col-sm-6" : "col-sm-8"}>
|
||||
<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 additional thumb sizes for image files, along with the default thumb size of 100x100. The thumbs are generated lazily on first access.",
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<MultipleValueInput
|
||||
id={uniqueId}
|
||||
placeholder="eg. 50x50, 480x720"
|
||||
bind:value={field.options.thumbs}
|
||||
/>
|
||||
<div class="help-block">
|
||||
<span class="txt">Use comma as separator.</span>
|
||||
<button type="button" class="inline-flex flex-gap-0">
|
||||
<span class="txt link-primary">Supported formats</span>
|
||||
<i class="ri-arrow-drop-down-fill" />
|
||||
<Toggler class="dropdown dropdown-sm dropdown-center dropdown-nowrap p-r-10">
|
||||
<ul class="m-0">
|
||||
<li>
|
||||
<strong>WxH</strong>
|
||||
(eg. 100x50) - crop to WxH viewbox (from center)
|
||||
</li>
|
||||
<li>
|
||||
<strong>WxHt</strong>
|
||||
(eg. 100x50t) - crop to WxH viewbox (from top)
|
||||
</li>
|
||||
<li>
|
||||
<strong>WxHb</strong>
|
||||
(eg. 100x50b) - crop to WxH viewbox (from bottom)
|
||||
</li>
|
||||
<li>
|
||||
<strong>WxHf</strong>
|
||||
(eg. 100x50f) - fit inside a WxH viewbox (without cropping)
|
||||
</li>
|
||||
<li>
|
||||
<strong>0xH</strong>
|
||||
(eg. 0x50) - resize to H height preserving the aspect ratio
|
||||
</li>
|
||||
<li>
|
||||
<strong>Wx0</strong>
|
||||
(eg. 100x0) - resize to W width preserving the aspect ratio
|
||||
</li>
|
||||
</ul>
|
||||
</Toggler>
|
||||
</button>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class={!isSingle ? "col-sm-3" : "col-sm-4"}>
|
||||
<Field class="form-field required" name="schema.{key}.options.maxSize" let:uniqueId>
|
||||
<label for={uniqueId}>Max file size</label>
|
||||
<input type="number" id={uniqueId} step="1" min="0" bind:value={field.options.maxSize} />
|
||||
<div class="help-block">Must be in bytes.</div>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{#if !isSingle}
|
||||
<div class="col-sm-3">
|
||||
<Field class="form-field required" name="schema.{key}.options.maxSelect" let:uniqueId>
|
||||
<label for={uniqueId}>Max select</label>
|
||||
<input
|
||||
id={uniqueId}
|
||||
type="number"
|
||||
step="1"
|
||||
min="2"
|
||||
required
|
||||
bind:value={field.options.maxSelect}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</SchemaField>
|
||||
|
||||
<style>
|
||||
:global(.form-field-file-max-select) {
|
||||
width: 100px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script>
|
||||
import { slide } from "svelte/transition";
|
||||
import SchemaField from "@/components/collections/schema/SchemaField.svelte";
|
||||
|
||||
export let field;
|
||||
export let key = "";
|
||||
|
||||
let showInfo = false;
|
||||
</script>
|
||||
|
||||
<SchemaField
|
||||
bind:field
|
||||
{key}
|
||||
on:rename
|
||||
on:remove
|
||||
on:drop
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragleave
|
||||
{...$$restProps}
|
||||
>
|
||||
<svelte:fragment slot="options">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex txt-sm flex-gap-5 link-hint"
|
||||
on:click={() => {
|
||||
showInfo = !showInfo;
|
||||
}}
|
||||
>
|
||||
<strong class="txt">String value normalizations</strong>
|
||||
{#if showInfo}
|
||||
<i class="ri-arrow-up-s-line txt-sm" />
|
||||
{:else}
|
||||
<i class="ri-arrow-down-s-line txt-sm" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if showInfo}
|
||||
<div class="block" transition:slide|local={{ duration: 150 }}>
|
||||
<div class="alert alert-warning m-b-0 m-t-10">
|
||||
<div class="content">
|
||||
In order to support seamlessly both <code>application/json</code> and
|
||||
<code>multipart/form-data</code>
|
||||
requests, the following normalization rules are applied if the <code>json</code> field
|
||||
is a
|
||||
<strong>plain string</strong>:
|
||||
<ul>
|
||||
<li>"true" is converted to the json <code>true</code></li>
|
||||
<li>"false" is converted to the json <code>false</code></li>
|
||||
<li>"null" is converted to the json <code>null</code></li>
|
||||
<li>"[1,2,3]" is converted to the json <code>[1,2,3]</code></li>
|
||||
<li>
|
||||
{'"{"a":1,"b":2}"'} is converted to the json <code>{'{"a":1,"b":2}'}</code>
|
||||
</li>
|
||||
<li>numeric strings are converted to json number</li>
|
||||
<li>double quoted strings are left as they are (aka. without normalizations)</li>
|
||||
<li>any other string (empty string too) is double quoted</li>
|
||||
</ul>
|
||||
Alternatively, if you want to avoid the string value normalizations, you can wrap your
|
||||
data inside an object, eg.<code>{'{"data": anything}'}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</SchemaField>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script>
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import SchemaField from "@/components/collections/schema/SchemaField.svelte";
|
||||
|
||||
export let field;
|
||||
export let key = "";
|
||||
</script>
|
||||
|
||||
<SchemaField
|
||||
bind:field
|
||||
{key}
|
||||
on:rename
|
||||
on:remove
|
||||
on:drop
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragleave
|
||||
{...$$restProps}
|
||||
>
|
||||
<svelte:fragment slot="options">
|
||||
<div class="grid grid-sm">
|
||||
<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={field.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={field.options.min}
|
||||
bind:value={field.options.max}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</SchemaField>
|
||||
@@ -0,0 +1,222 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import Select from "@/components/base/Select.svelte";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
import CollectionUpsertPanel from "@/components/collections/CollectionUpsertPanel.svelte";
|
||||
import SchemaField from "@/components/collections/schema/SchemaField.svelte";
|
||||
import { collections } from "@/stores/collections";
|
||||
|
||||
export let field;
|
||||
export let key = "";
|
||||
|
||||
const isSingleOptions = [
|
||||
{ label: "Single", value: true },
|
||||
{ label: "Multiple", value: false },
|
||||
];
|
||||
|
||||
const defaultOptions = [
|
||||
{ label: "False", value: false },
|
||||
{ label: "True", value: true },
|
||||
];
|
||||
|
||||
const baseFields = ["id", "created", "updated"];
|
||||
|
||||
const authFields = ["username", "email", "emailVisibility", "verified"];
|
||||
|
||||
let upsertPanel = null;
|
||||
let displayFieldsList = [];
|
||||
let oldCollectionId = null;
|
||||
let isSingle = field.options?.maxSelect == 1;
|
||||
let oldIsSingle = isSingle;
|
||||
|
||||
// load defaults
|
||||
$: if (CommonHelper.isEmpty(field.options)) {
|
||||
loadDefaults();
|
||||
}
|
||||
|
||||
$: if (oldIsSingle != isSingle) {
|
||||
oldIsSingle = isSingle;
|
||||
if (isSingle) {
|
||||
field.options.minSelect = null;
|
||||
field.options.maxSelect = 1;
|
||||
} else {
|
||||
field.options.maxSelect = null;
|
||||
}
|
||||
}
|
||||
|
||||
$: selectedColection = $collections.find((c) => c.id == field.options.collectionId) || null;
|
||||
|
||||
$: if (oldCollectionId != field.options.collectionId) {
|
||||
oldCollectionId = field.options.collectionId;
|
||||
refreshDisplayFieldsList();
|
||||
}
|
||||
|
||||
function loadDefaults() {
|
||||
field.options = {
|
||||
maxSelect: 1,
|
||||
collectionId: null,
|
||||
cascadeDelete: false,
|
||||
displayFields: [],
|
||||
};
|
||||
isSingle = true;
|
||||
oldIsSingle = isSingle;
|
||||
}
|
||||
|
||||
function refreshDisplayFieldsList() {
|
||||
displayFieldsList = baseFields.slice(0);
|
||||
if (!selectedColection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedColection.isAuth) {
|
||||
displayFieldsList = displayFieldsList.concat(authFields);
|
||||
}
|
||||
|
||||
for (const f of selectedColection.schema) {
|
||||
displayFieldsList.push(f.name);
|
||||
}
|
||||
|
||||
// deselect any missing display field
|
||||
if (field.options?.displayFields?.length > 0) {
|
||||
for (let i = field.options.displayFields.length - 1; i >= 0; i--) {
|
||||
if (!displayFieldsList.includes(field.options.displayFields[i])) {
|
||||
field.options.displayFields.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SchemaField
|
||||
bind:field
|
||||
{key}
|
||||
on:rename
|
||||
on:remove
|
||||
on:drop
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragleave
|
||||
{...$$restProps}
|
||||
>
|
||||
<svelte:fragment let:interactive>
|
||||
<Field
|
||||
class="form-field required {!interactive ? 'disabled' : ''}"
|
||||
inlineError
|
||||
name="schema.{key}.options.collectionId"
|
||||
let:uniqueId
|
||||
>
|
||||
<ObjectSelect
|
||||
id={uniqueId}
|
||||
searchable={$collections.length > 5}
|
||||
selectPlaceholder={"Select collection *"}
|
||||
noOptionsText="No collections found"
|
||||
selectionKey="id"
|
||||
items={$collections}
|
||||
disabled={!interactive}
|
||||
bind:keyOfSelected={field.options.collectionId}
|
||||
>
|
||||
<svelte:fragment slot="afterOptions">
|
||||
<hr />
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent btn-block btn-sm"
|
||||
on:click={() => upsertPanel?.show()}
|
||||
>
|
||||
<i class="ri-add-line" />
|
||||
<span class="txt">New collection</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</ObjectSelect>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
class="form-field form-field-single-multiple-select {!interactive ? 'disabled' : ''}"
|
||||
inlineError
|
||||
let:uniqueId
|
||||
>
|
||||
<ObjectSelect
|
||||
id={uniqueId}
|
||||
items={isSingleOptions}
|
||||
disabled={!interactive}
|
||||
bind:keyOfSelected={isSingle}
|
||||
/>
|
||||
</Field>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="options">
|
||||
<div class="grid grid-sm">
|
||||
{#if !isSingle}
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.minSelect" let:uniqueId>
|
||||
<label for={uniqueId}>Min select</label>
|
||||
<input
|
||||
type="number"
|
||||
id={uniqueId}
|
||||
step="1"
|
||||
min="1"
|
||||
placeholder="No min limit"
|
||||
bind:value={field.options.minSelect}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.maxSelect" let:uniqueId>
|
||||
<label for={uniqueId}>Max select</label>
|
||||
<input
|
||||
type="number"
|
||||
id={uniqueId}
|
||||
step="1"
|
||||
placeholder="No max limit"
|
||||
min={field.options.minSelect || 2}
|
||||
bind:value={field.options.maxSelect}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.displayFields" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Display fields</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: "Optionally select the field(s) that will be used in the listings UI. Leave empty for auto.",
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<Select
|
||||
multiple
|
||||
searchable
|
||||
id={uniqueId}
|
||||
selectPlaceholder="Auto"
|
||||
items={displayFieldsList}
|
||||
bind:selected={field.options.displayFields}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.cascadeDelete" let:uniqueId>
|
||||
<label for={uniqueId}>Delete main record on relation delete</label>
|
||||
<ObjectSelect
|
||||
id={uniqueId}
|
||||
items={defaultOptions}
|
||||
bind:keyOfSelected={field.options.cascadeDelete}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</SchemaField>
|
||||
|
||||
<CollectionUpsertPanel
|
||||
bind:this={upsertPanel}
|
||||
on:save={(e) => {
|
||||
if (e?.detail?.collection?.id) {
|
||||
field.options.collectionId = e.detail.collection.id;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
import SchemaField from "@/components/collections/schema/SchemaField.svelte";
|
||||
import MultipleValueInput from "@/components/base/MultipleValueInput.svelte";
|
||||
|
||||
export let field;
|
||||
export let key = "";
|
||||
|
||||
const isSingleOptions = [
|
||||
{ label: "Single", value: true },
|
||||
{ label: "Multiple", value: false },
|
||||
];
|
||||
|
||||
let isSingle = field.options?.maxSelect <= 1;
|
||||
let oldIsSingle = isSingle;
|
||||
|
||||
$: if (CommonHelper.isEmpty(field.options)) {
|
||||
loadDefaults();
|
||||
}
|
||||
|
||||
$: if (oldIsSingle != isSingle) {
|
||||
oldIsSingle = isSingle;
|
||||
if (isSingle) {
|
||||
field.options.maxSelect = 1;
|
||||
} else {
|
||||
field.options.maxSelect = field.options?.values?.length || 2;
|
||||
}
|
||||
}
|
||||
|
||||
function loadDefaults() {
|
||||
field.options = {
|
||||
maxSelect: 1,
|
||||
values: [],
|
||||
};
|
||||
isSingle = true;
|
||||
oldIsSingle = isSingle;
|
||||
}
|
||||
</script>
|
||||
|
||||
<SchemaField
|
||||
bind:field
|
||||
{key}
|
||||
on:rename
|
||||
on:remove
|
||||
on:drop
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragleave
|
||||
{...$$restProps}
|
||||
>
|
||||
<svelte:fragment let:interactive>
|
||||
<Field
|
||||
class="form-field required {!interactive ? 'disabled' : ''}"
|
||||
inlineError
|
||||
name="schema.{key}.options.values"
|
||||
let:uniqueId
|
||||
>
|
||||
<div use:tooltip={{ text: "Choices (comma separated)", position: "top-left", delay: 700 }}>
|
||||
<MultipleValueInput
|
||||
id={uniqueId}
|
||||
placeholder="Choices: eg. optionA, optionB"
|
||||
required
|
||||
disabled={!interactive}
|
||||
bind:value={field.options.values}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
<Field
|
||||
class="form-field form-field-single-multiple-select {!interactive ? 'disabled' : ''}"
|
||||
inlineError
|
||||
let:uniqueId
|
||||
>
|
||||
<ObjectSelect
|
||||
id={uniqueId}
|
||||
items={isSingleOptions}
|
||||
disabled={!interactive}
|
||||
bind:keyOfSelected={isSingle}
|
||||
/>
|
||||
</Field>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="options">
|
||||
{#if !isSingle}
|
||||
<Field class="form-field required" name="schema.{key}.options.maxSelect" let:uniqueId>
|
||||
<label for={uniqueId}>Max select</label>
|
||||
<input
|
||||
id={uniqueId}
|
||||
type="number"
|
||||
step="1"
|
||||
min="2"
|
||||
required
|
||||
bind:value={field.options.maxSelect}
|
||||
/>
|
||||
</Field>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</SchemaField>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script>
|
||||
import SchemaField from "@/components/collections/schema/SchemaField.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let field;
|
||||
export let key = "";
|
||||
</script>
|
||||
|
||||
<SchemaField
|
||||
bind:field
|
||||
{key}
|
||||
on:rename
|
||||
on:remove
|
||||
on:drop
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragleave
|
||||
{...$$restProps}
|
||||
>
|
||||
<svelte:fragment slot="options">
|
||||
<div class="grid grid-sm">
|
||||
<div class="col-sm-3">
|
||||
<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={field.options.min} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<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={field.options.min || 0}
|
||||
bind:value={field.options.max}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field" name="schema.{key}.options.pattern" let:uniqueId>
|
||||
<label for={uniqueId}>Regex pattern</label>
|
||||
<input
|
||||
type="text"
|
||||
id={uniqueId}
|
||||
placeholder={"Valid Go regular expression, eg. ^w+$"}
|
||||
bind:value={field.options.pattern}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</SchemaField>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script>
|
||||
import SchemaFieldEmail from "./SchemaFieldEmail.svelte";
|
||||
|
||||
export let field;
|
||||
export let key = "";
|
||||
</script>
|
||||
|
||||
<!-- shares the same options with the email field -->
|
||||
<SchemaFieldEmail
|
||||
bind:field
|
||||
{key}
|
||||
on:rename
|
||||
on:remove
|
||||
on:drop
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragleave
|
||||
{...$$restProps}
|
||||
/>
|
||||
@@ -1,43 +0,0 @@
|
||||
<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: [],
|
||||
};
|
||||
}
|
||||
|
||||
// note: 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>
|
||||
@@ -1,30 +0,0 @@
|
||||
<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>
|
||||
@@ -1,9 +0,0 @@
|
||||
<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 />
|
||||
Reference in New Issue
Block a user