initial public commit
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
<script>
|
||||
import {
|
||||
collections,
|
||||
activeCollection,
|
||||
isCollectionsLoading,
|
||||
loadCollections,
|
||||
} from "@/stores/collections";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Searchbar from "@/components/base/Searchbar.svelte";
|
||||
import CollectionsSidebar from "@/components/collections/CollectionsSidebar.svelte";
|
||||
import CollectionUpsertPanel from "@/components/collections/CollectionUpsertPanel.svelte";
|
||||
import CollectionDocsPanel from "@/components/collections/docs/CollectionDocsPanel.svelte";
|
||||
import RecordUpsertPanel from "@/components/records/RecordUpsertPanel.svelte";
|
||||
import RecordsList from "@/components/records/RecordsList.svelte";
|
||||
|
||||
const queryParams = CommonHelper.getQueryParams(window.location?.href);
|
||||
|
||||
let collectionUpsertPanel;
|
||||
let collectionDocsPanel;
|
||||
let recordPanel;
|
||||
let recordsList;
|
||||
let filter = queryParams.filter || "";
|
||||
let sort = queryParams.sort || "-created";
|
||||
let selectedCollectionId = queryParams.collectionId;
|
||||
|
||||
$: viewableCollections = $collections.filter((c) => c.name != import.meta.env.PB_PROFILE_COLLECTION);
|
||||
|
||||
// reset filter and sort on collection change
|
||||
$: if ($activeCollection?.id && selectedCollectionId != $activeCollection.id) {
|
||||
selectedCollectionId = $activeCollection.id;
|
||||
sort = "-created";
|
||||
filter = "";
|
||||
}
|
||||
|
||||
// keep the url params in sync
|
||||
$: if (sort || filter || $activeCollection?.id) {
|
||||
CommonHelper.replaceClientQueryParams({
|
||||
collectionId: $activeCollection?.id,
|
||||
filter: filter,
|
||||
sort: sort,
|
||||
});
|
||||
}
|
||||
|
||||
CommonHelper.setDocumentTitle("Collections");
|
||||
|
||||
loadCollections(selectedCollectionId);
|
||||
</script>
|
||||
|
||||
{#if $isCollectionsLoading}
|
||||
<div class="placeholder-section m-b-base">
|
||||
<span class="loader loader-lg" />
|
||||
<h1>Loading collections...</h1>
|
||||
</div>
|
||||
{:else if !viewableCollections.length}
|
||||
<div class="placeholder-section m-b-base">
|
||||
<div class="icon">
|
||||
<i class="ri-database-2-line" />
|
||||
</div>
|
||||
<h1 class="m-b-10">Create your first collection to add records!</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-expanded-lg btn-lg"
|
||||
on:click={() => collectionUpsertPanel?.show()}
|
||||
>
|
||||
<i class="ri-add-line" />
|
||||
<span class="txt">Create new collection</span>
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<CollectionsSidebar />
|
||||
|
||||
<main class="page-wrapper">
|
||||
<header class="page-header">
|
||||
<nav class="breadcrumbs">
|
||||
<div class="breadcrumb-item">Collections</div>
|
||||
<div class="breadcrumb-item">{$activeCollection.name}</div>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-circle"
|
||||
use:tooltip={{ text: "Edit collection", position: "right" }}
|
||||
on:click={() => collectionUpsertPanel?.show($activeCollection)}
|
||||
>
|
||||
<i class="ri-settings-4-line" />
|
||||
</button>
|
||||
|
||||
<div class="btns-group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline"
|
||||
on:click={() => collectionDocsPanel?.show($activeCollection)}
|
||||
>
|
||||
<i class="ri-code-s-slash-line" />
|
||||
<span class="txt">API Preview</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-expanded" on:click={() => recordPanel?.show()}>
|
||||
<i class="ri-add-line" />
|
||||
<span class="txt">New record</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Searchbar
|
||||
value={filter}
|
||||
autocompleteCollection={$activeCollection}
|
||||
on:submit={(e) => (filter = e.detail)}
|
||||
/>
|
||||
|
||||
<RecordsList
|
||||
bind:this={recordsList}
|
||||
collection={$activeCollection}
|
||||
bind:filter
|
||||
bind:sort
|
||||
on:select={(e) => recordPanel?.show(e?.detail)}
|
||||
/>
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
<CollectionUpsertPanel bind:this={collectionUpsertPanel} />
|
||||
<CollectionDocsPanel bind:this={collectionDocsPanel} />
|
||||
|
||||
<RecordUpsertPanel
|
||||
bind:this={recordPanel}
|
||||
collection={$activeCollection}
|
||||
on:save={() => recordsList?.load()}
|
||||
on:delete={() => recordsList?.load()}
|
||||
/>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import IdLabel from "@/components/base/IdLabel.svelte";
|
||||
import FormattedDate from "@/components/base/FormattedDate.svelte";
|
||||
import RecordFilePreview from "@/components/records/RecordFilePreview.svelte";
|
||||
|
||||
export let record;
|
||||
export let field;
|
||||
</script>
|
||||
|
||||
<td class="col-type-{field.type} col-field-{field.name}">
|
||||
{#if CommonHelper.isEmpty(record[field.name])}
|
||||
<span class="txt-hint">N/A</span>
|
||||
{:else if field.type === "bool"}
|
||||
<span class="txt">{record[field.name] ? "True" : "False"}</span>
|
||||
{:else if field.type === "url"}
|
||||
<a
|
||||
class="txt-ellipsis"
|
||||
href={record[field.name]}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
use:tooltip={"Open in new tab"}
|
||||
on:click|stopPropagation
|
||||
>
|
||||
{record[field.name]}
|
||||
</a>
|
||||
{:else if field.type === "date"}
|
||||
<FormattedDate date={record[field.name]} />
|
||||
{:else if field.type === "json"}
|
||||
<span class="txt txt-ellipsis">{JSON.stringify(record[field.name])}</span>
|
||||
{:else if field.type === "select"}
|
||||
<div class="inline-flex">
|
||||
{#each CommonHelper.toArray(record[field.name]) as item}
|
||||
<span class="label">{item}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if field.type === "relation" || field.type === "user"}
|
||||
<div class="inline-flex">
|
||||
{#each CommonHelper.toArray(record[field.name]) as item}
|
||||
<IdLabel id={item} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if field.type === "file"}
|
||||
<div class="inline-flex">
|
||||
{#each CommonHelper.toArray(record[field.name]) as filename}
|
||||
<figure class="thumb thumb-sm" use:tooltip={filename}>
|
||||
<RecordFilePreview {record} {filename} />
|
||||
</figure>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="txt txt-ellipsis" title={record[field.name]}>
|
||||
{record[field.name]}
|
||||
</span>
|
||||
{/if}
|
||||
</td>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script>
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
|
||||
export let record;
|
||||
export let filename;
|
||||
|
||||
let previewUrl = "";
|
||||
|
||||
$: if (CommonHelper.hasImageExtension(filename)) {
|
||||
previewUrl = ApiClient.Records.getFileUrl(record, `${filename}?thumb=100x100`);
|
||||
}
|
||||
|
||||
function onError() {
|
||||
previewUrl = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if previewUrl}
|
||||
<img src={previewUrl} alt={filename} on:error={onError} />
|
||||
{:else}
|
||||
<i class="ri-file-line" />
|
||||
{/if}
|
||||
@@ -0,0 +1,123 @@
|
||||
<script>
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
import RecordSelectOption from "./RecordSelectOption.svelte";
|
||||
|
||||
const uniqueId = "select_" + CommonHelper.randomString(5);
|
||||
|
||||
// original select props
|
||||
export let multiple = false;
|
||||
export let selected = multiple ? [] : undefined;
|
||||
export let keyOfSelected = multiple ? [] : undefined;
|
||||
export let selectPlaceholder = "- Select -";
|
||||
export let optionComponent = RecordSelectOption; // custom component to use for each dropdown option item
|
||||
|
||||
// custom props
|
||||
export let collectionId;
|
||||
|
||||
let list = [];
|
||||
let currentPage = 1;
|
||||
let totalItems = 0;
|
||||
let isLoadingList = false;
|
||||
let isLoadingSelected = false;
|
||||
|
||||
$: if (collectionId) {
|
||||
loadList();
|
||||
}
|
||||
|
||||
$: isLoading = isLoadingList || isLoadingSelected;
|
||||
|
||||
$: canLoadMore = totalItems > list.length;
|
||||
|
||||
loadSelected();
|
||||
|
||||
async function loadSelected() {
|
||||
const selectedIds = CommonHelper.toArray(keyOfSelected);
|
||||
|
||||
if (!collectionId || !selectedIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingSelected = true;
|
||||
|
||||
try {
|
||||
const filters = [];
|
||||
for (const id of selectedIds) {
|
||||
filters.push(`id="${id}"`);
|
||||
}
|
||||
|
||||
selected = await ApiClient.Records.getFullList(collectionId, 200, {
|
||||
sort: "-created",
|
||||
filter: filters.join("||"),
|
||||
$cancelKey: uniqueId + "loadSelected",
|
||||
});
|
||||
|
||||
// add the selected models to the list (if not already)
|
||||
list = CommonHelper.filterDuplicatesByKey(list.concat(selected));
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
}
|
||||
|
||||
isLoadingSelected = false;
|
||||
}
|
||||
|
||||
async function loadList(reset = false) {
|
||||
if (!collectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingList = true;
|
||||
|
||||
try {
|
||||
const page = reset ? 1 : currentPage + 1;
|
||||
|
||||
const result = await ApiClient.Records.getList(collectionId, page, 200, {
|
||||
sort: "-created",
|
||||
$cancelKey: uniqueId + "loadList",
|
||||
});
|
||||
|
||||
if (reset) {
|
||||
list = [];
|
||||
}
|
||||
|
||||
list = CommonHelper.filterDuplicatesByKey(list.concat(result.items));
|
||||
currentPage = result.page;
|
||||
totalItems = result.totalItems;
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
}
|
||||
|
||||
isLoadingList = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ObjectSelect
|
||||
selectPlaceholder={isLoading ? "Loading..." : selectPlaceholder}
|
||||
items={list}
|
||||
searchable={list.length > 5}
|
||||
selectionKey="id"
|
||||
labelComponent={optionComponent}
|
||||
{optionComponent}
|
||||
{multiple}
|
||||
bind:keyOfSelected
|
||||
bind:selected
|
||||
on:show
|
||||
on:hide
|
||||
class="records-select block-options"
|
||||
{...$$restProps}
|
||||
>
|
||||
<svelte:fragment slot="afterOptions">
|
||||
{#if canLoadMore}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-block btn-sm"
|
||||
class:btn-loading={isLoadingList}
|
||||
class:btn-disabled={isLoadingList}
|
||||
on:click|stopPropagation={() => loadList()}
|
||||
>
|
||||
<span class="txt">Load more</span>
|
||||
</button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ObjectSelect>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
|
||||
const excludedMetaProps = ["id", "created", "updated", "@collectionId", "@collectionName"];
|
||||
|
||||
export let item = {}; // model
|
||||
|
||||
$: meta = extractMeta(item);
|
||||
|
||||
function extractMeta(model) {
|
||||
model = model || {};
|
||||
|
||||
const props = [
|
||||
// prioritized common displayable props
|
||||
"name",
|
||||
"title",
|
||||
"label",
|
||||
"key",
|
||||
"email",
|
||||
"heading",
|
||||
"content",
|
||||
// fallback to the available props
|
||||
...Object.keys(model),
|
||||
];
|
||||
|
||||
for (const prop of props) {
|
||||
if (
|
||||
typeof model[prop] === "string" &&
|
||||
!CommonHelper.isEmpty(model[prop]) &&
|
||||
!excludedMetaProps.includes(prop)
|
||||
) {
|
||||
return prop + ": " + model[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{ text: JSON.stringify(item, null, 2), position: "left", class: "code" }}
|
||||
/>
|
||||
|
||||
<div class="content">
|
||||
<div class="block txt-ellipsis">{item.id}</div>
|
||||
{#if meta !== "" && meta !== item.id}
|
||||
<small class="block txt-hint txt-ellipsis">{meta}</small>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,269 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { Record } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import Toggler from "@/components/base/Toggler.svelte";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
import TextField from "@/components/records/fields/TextField.svelte";
|
||||
import NumberField from "@/components/records/fields/NumberField.svelte";
|
||||
import BoolField from "@/components/records/fields/BoolField.svelte";
|
||||
import EmailField from "@/components/records/fields/EmailField.svelte";
|
||||
import UrlField from "@/components/records/fields/UrlField.svelte";
|
||||
import DateField from "@/components/records/fields/DateField.svelte";
|
||||
import SelectField from "@/components/records/fields/SelectField.svelte";
|
||||
import JsonField from "@/components/records/fields/JsonField.svelte";
|
||||
import FileField from "@/components/records/fields/FileField.svelte";
|
||||
import RelationField from "@/components/records/fields/RelationField.svelte";
|
||||
import UserField from "@/components/records/fields/UserField.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const formId = "record_" + CommonHelper.randomString(5);
|
||||
|
||||
export let collection;
|
||||
|
||||
let recordPanel;
|
||||
let original = null;
|
||||
let record = new Record();
|
||||
let isSaving = false;
|
||||
let confirmClose = false; // prevent close recursion
|
||||
let uploadedFilesMap = {}; // eg.: {"field1":[File1, File2], ...}
|
||||
let deletedFileIndexesMap = {}; // eg.: {"field1":[0, 1], ...}
|
||||
let initialFormHash = "";
|
||||
|
||||
$: hasFileChanges =
|
||||
CommonHelper.hasNonEmptyProps(uploadedFilesMap) ||
|
||||
CommonHelper.hasNonEmptyProps(deletedFileIndexesMap);
|
||||
|
||||
$: hasChanges = hasFileChanges || initialFormHash != calculateFormHash(record);
|
||||
|
||||
$: canSave = record.isNew || hasChanges;
|
||||
|
||||
$: isProfileCollection = collection?.name !== import.meta.env.PB_PROFILE_COLLECTION;
|
||||
|
||||
export function show(model) {
|
||||
load(model);
|
||||
|
||||
confirmClose = true;
|
||||
|
||||
return recordPanel?.show();
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
return recordPanel?.hide();
|
||||
}
|
||||
|
||||
function load(model) {
|
||||
setErrors({}); // reset errors
|
||||
original = model || {};
|
||||
record = model?.clone ? model.clone() : new Record();
|
||||
uploadedFilesMap = {};
|
||||
deletedFileIndexesMap = {};
|
||||
initialFormHash = calculateFormHash(record);
|
||||
}
|
||||
|
||||
function calculateFormHash(m) {
|
||||
return JSON.stringify(m);
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (isSaving || !hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSaving = true;
|
||||
|
||||
const data = exportFormData();
|
||||
|
||||
let request;
|
||||
if (record.isNew) {
|
||||
request = ApiClient.Records.create(collection?.id, data);
|
||||
} else {
|
||||
request = ApiClient.Records.update(collection?.id, record.id, data);
|
||||
}
|
||||
|
||||
request
|
||||
.then(async (result) => {
|
||||
addSuccessToast(
|
||||
record.isNew ? "Successfully created record." : "Successfully updated record."
|
||||
);
|
||||
confirmClose = false;
|
||||
hide();
|
||||
dispatch("save", result);
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
})
|
||||
.finally(() => {
|
||||
isSaving = false;
|
||||
});
|
||||
}
|
||||
|
||||
function deleteConfirm() {
|
||||
if (!original?.id) {
|
||||
return; // nothing to delete
|
||||
}
|
||||
|
||||
confirm(`Do you really want to delete the selected record?`, () => {
|
||||
return ApiClient.Records.delete(original["@collectionId"], original.id)
|
||||
.then(() => {
|
||||
hide();
|
||||
addSuccessToast("Successfully deleted record.");
|
||||
dispatch("delete", original);
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function exportFormData() {
|
||||
const data = record?.export() || {};
|
||||
const formData = new FormData();
|
||||
|
||||
const schemaMap = {};
|
||||
for (const field of collection?.schema || []) {
|
||||
schemaMap[field.name] = field;
|
||||
}
|
||||
|
||||
// export base fields
|
||||
for (const key in data) {
|
||||
// skip non-schema fields
|
||||
if (!schemaMap[key]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// normalize nullable values
|
||||
if (typeof data[key] === "undefined") {
|
||||
data[key] = null;
|
||||
}
|
||||
|
||||
CommonHelper.addValueToFormData(formData, key, data[key]);
|
||||
}
|
||||
|
||||
// add uploaded files (if any)
|
||||
for (const key in uploadedFilesMap) {
|
||||
const files = CommonHelper.toArray(uploadedFilesMap[key]);
|
||||
for (const file of files) {
|
||||
formData.append(key, file);
|
||||
}
|
||||
}
|
||||
|
||||
// unset deleted files (if any)
|
||||
for (const key in deletedFileIndexesMap) {
|
||||
const indexes = CommonHelper.toArray(deletedFileIndexesMap[key]);
|
||||
for (const index of indexes) {
|
||||
formData.append(key + "." + index, "");
|
||||
}
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
</script>
|
||||
|
||||
<OverlayPanel
|
||||
bind:this={recordPanel}
|
||||
class="overlay-panel-lg record-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>
|
||||
{record.isNew ? "New" : "Edit"}
|
||||
{collection.name} record
|
||||
</h4>
|
||||
|
||||
{#if !record.isNew && isProfileCollection}
|
||||
<div class="flex-fill" />
|
||||
<button type="button" class="btn btn-sm btn-circle btn-secondary">
|
||||
<div class="content">
|
||||
<i class="ri-more-line" />
|
||||
<Toggler class="dropdown dropdown-right m-t-5">
|
||||
<div tabindex="0" class="dropdown-item closable" on:click={() => deleteConfirm()}>
|
||||
<i class="ri-delete-bin-7-line" />
|
||||
<span class="txt">Delete</span>
|
||||
</div>
|
||||
</Toggler>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<form id={formId} class="block" on:submit|preventDefault={save}>
|
||||
{#if !record.isNew}
|
||||
<Field class="form-field disabled" name="id" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon("primary")} />
|
||||
<span class="txt">id</span>
|
||||
<span class="flex-fill" />
|
||||
</label>
|
||||
<input type="text" id={uniqueId} value={record.id} disabled />
|
||||
</Field>
|
||||
{/if}
|
||||
|
||||
{#each collection?.schema || [] as field (field.name)}
|
||||
{#if field.type === "text"}
|
||||
<TextField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "number"}
|
||||
<NumberField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "bool"}
|
||||
<BoolField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "email"}
|
||||
<EmailField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "url"}
|
||||
<UrlField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "date"}
|
||||
<DateField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "select"}
|
||||
<SelectField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "json"}
|
||||
<JsonField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "file"}
|
||||
<FileField
|
||||
{field}
|
||||
{record}
|
||||
bind:value={record[field.name]}
|
||||
bind:uploadedFiles={uploadedFilesMap[field.name]}
|
||||
bind:deletedFileIndexes={deletedFileIndexesMap[field.name]}
|
||||
/>
|
||||
{:else if field.type === "relation"}
|
||||
<RelationField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "user"}
|
||||
<UserField {field} bind:value={record[field.name]} />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="block txt-center txt-disabled">
|
||||
<h5>No custom fields to be set</h5>
|
||||
</div>
|
||||
{/each}
|
||||
</form>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button type="button" class="btn btn-secondary" disabled={isSaving} on:click={() => hide()}>
|
||||
<span class="txt">Cancel</span>
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form={formId}
|
||||
class="btn btn-expanded"
|
||||
class:btn-loading={isSaving}
|
||||
disabled={!canSave || isSaving}
|
||||
>
|
||||
<span class="txt">{record.isNew ? "Create" : "Save changes"}</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</OverlayPanel>
|
||||
@@ -0,0 +1,312 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { fly } from "svelte/transition";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import SortHeader from "@/components/base/SortHeader.svelte";
|
||||
import FormattedDate from "@/components/base/FormattedDate.svelte";
|
||||
import IdLabel from "@/components/base/IdLabel.svelte";
|
||||
import RecordFieldCell from "@/components/records/RecordFieldCell.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let collection;
|
||||
export let sort = "";
|
||||
export let filter = "";
|
||||
|
||||
let records = [];
|
||||
let currentPage = 1;
|
||||
let totalRecords = 0;
|
||||
let bulkSelected = {};
|
||||
let isLoading = true;
|
||||
let isDeleting = false;
|
||||
|
||||
$: if (collection && collection.id && sort !== -1 && filter !== -1) {
|
||||
clearList();
|
||||
load(1);
|
||||
}
|
||||
|
||||
$: canLoadMore = totalRecords > records.length;
|
||||
|
||||
$: fields = collection?.schema || [];
|
||||
|
||||
$: totalBulkSelected = Object.keys(bulkSelected).length;
|
||||
|
||||
$: areAllRecordsSelected = records.length && totalBulkSelected === records.length;
|
||||
|
||||
export async function load(page = 1) {
|
||||
if (!collection?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
||||
return ApiClient.Records.getList(collection.id, page, 50, {
|
||||
sort: sort,
|
||||
filter: filter,
|
||||
})
|
||||
.then((result) => {
|
||||
if (page <= 1) {
|
||||
clearList();
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
records = records.concat(result.items);
|
||||
currentPage = result.page;
|
||||
totalRecords = result.totalItems;
|
||||
|
||||
dispatch("load", records);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err !== null) {
|
||||
isLoading = false;
|
||||
console.warn(err);
|
||||
clearList();
|
||||
ApiClient.errorResponseHandler(err, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearList() {
|
||||
records = [];
|
||||
currentPage = 1;
|
||||
totalRecords = 0;
|
||||
bulkSelected = {};
|
||||
}
|
||||
|
||||
function toggleSelectAllRecords() {
|
||||
if (areAllRecordsSelected) {
|
||||
deselectAllRecords();
|
||||
} else {
|
||||
selectAllRecords();
|
||||
}
|
||||
}
|
||||
|
||||
function deselectAllRecords() {
|
||||
bulkSelected = {};
|
||||
}
|
||||
|
||||
function selectAllRecords() {
|
||||
for (const record of records) {
|
||||
bulkSelected[record.id] = record;
|
||||
}
|
||||
bulkSelected = bulkSelected;
|
||||
}
|
||||
|
||||
function toggleSelectRecord(record) {
|
||||
if (!bulkSelected[record.id]) {
|
||||
bulkSelected[record.id] = record;
|
||||
} else {
|
||||
delete bulkSelected[record.id];
|
||||
}
|
||||
|
||||
bulkSelected = bulkSelected; // trigger reactivity
|
||||
}
|
||||
|
||||
function deleteSelectedConfirm() {
|
||||
const msg = `Do you really want to delete the selected ${
|
||||
totalBulkSelected === 1 ? "record" : "records"
|
||||
}?`;
|
||||
|
||||
confirm(msg, deleteSelected);
|
||||
}
|
||||
|
||||
async function deleteSelected() {
|
||||
if (isDeleting || !totalBulkSelected) {
|
||||
return;
|
||||
}
|
||||
|
||||
let promises = [];
|
||||
for (const recordId of Object.keys(bulkSelected)) {
|
||||
promises.push(ApiClient.Records.delete(collection?.id, recordId));
|
||||
}
|
||||
|
||||
isDeleting = true;
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(() => {
|
||||
addSuccessToast(
|
||||
`Successfully deleted the selected ${totalBulkSelected === 1 ? "record" : "records"}.`
|
||||
);
|
||||
deselectAllRecords();
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
})
|
||||
.finally(() => {
|
||||
isDeleting = false;
|
||||
|
||||
// always reload because some of the records may not be deletable
|
||||
return load();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="table" class:table-loading={isLoading}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="bulk-select-col min-width">
|
||||
<div class="form-field">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkbox_0"
|
||||
disabled={!records.length}
|
||||
checked={areAllRecordsSelected}
|
||||
on:change={() => toggleSelectAllRecords()}
|
||||
/>
|
||||
<label for="checkbox_0" />
|
||||
</div>
|
||||
</th>
|
||||
<SortHeader class="col-type-text col-field-id" name="id" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("primary")} />
|
||||
<span class="txt">id</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
{#each fields as field (field.name)}
|
||||
<SortHeader
|
||||
class="col-type-{field.type} col-field-{field.name}"
|
||||
name={field.name}
|
||||
bind:sort
|
||||
>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name}</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
{/each}
|
||||
<SortHeader class="col-type-date col-field-created" name="created" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("date")} />
|
||||
<span class="txt">created</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
<SortHeader class="col-type-date col-field-updated" name="updated" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("date")} />
|
||||
<span class="txt">updated</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
<th class="col-type-action min-width" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each records as record (record.id)}
|
||||
<tr
|
||||
tabindex="0"
|
||||
class="row-handle"
|
||||
on:click={() => dispatch("select", record)}
|
||||
on:keydown={(e) => {
|
||||
if (e.code === "Enter") {
|
||||
e.preventDefault();
|
||||
dispatch("select", record);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td class="bulk-select-col min-width">
|
||||
<div class="form-field" on:click|stopPropagation>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkbox_{record.id}"
|
||||
checked={bulkSelected[record.id]}
|
||||
on:change={() => toggleSelectRecord(record)}
|
||||
/>
|
||||
<label for="checkbox_{record.id}" />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="col-type-text col-field-id">
|
||||
<IdLabel id={record.id} />
|
||||
</td>
|
||||
|
||||
{#each fields as field (field.name)}
|
||||
<RecordFieldCell {record} {field} />
|
||||
{/each}
|
||||
|
||||
<td class="col-type-date col-field-created">
|
||||
<FormattedDate date={record.created} />
|
||||
</td>
|
||||
|
||||
<td class="col-type-date col-field-updated">
|
||||
<FormattedDate date={record.updated} />
|
||||
</td>
|
||||
|
||||
<td class="col-type-action min-width">
|
||||
<i class="ri-arrow-right-line" />
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#if isLoading}
|
||||
<tr>
|
||||
<td colspan="99" class="p-xs">
|
||||
<span class="skeleton-loader" />
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="99" class="txt-center txt-hint p-xs">
|
||||
<h6>No records found.</h6>
|
||||
{#if filter?.length}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-hint btn-expanded m-t-sm"
|
||||
on:click={() => (filter = "")}
|
||||
>
|
||||
<span class="txt">Clear filters</span>
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{#if records.length}
|
||||
<small class="block txt-hint txt-right m-t-sm">Showing {records.length} of {totalRecords}</small>
|
||||
{/if}
|
||||
|
||||
{#if records.length && canLoadMore}
|
||||
<div class="block txt-center m-t-xs">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-lg btn-secondary btn-expanded"
|
||||
class:btn-loading={isLoading}
|
||||
class:btn-disabled={isLoading}
|
||||
on:click={() => load(currentPage + 1)}
|
||||
>
|
||||
<span class="txt">Load more ({totalRecords - records.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if totalBulkSelected}
|
||||
<div class="bulkbar" transition:fly|local={{ duration: 150, y: 5 }}>
|
||||
<div class="txt">
|
||||
Selected <strong>{totalBulkSelected}</strong>
|
||||
{totalBulkSelected === 1 ? "record" : "records"}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-secondary btn-outline p-l-5 p-r-5"
|
||||
class:btn-disabled={isDeleting}
|
||||
on:click={() => deselectAllRecords()}
|
||||
>
|
||||
<span class="txt">Reset</span>
|
||||
</button>
|
||||
<div class="flex-fill" />
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary btn-danger"
|
||||
class:btn-loading={isDeleting}
|
||||
class:btn-disabled={isDeleting}
|
||||
on:click={() => deleteSelectedConfirm()}
|
||||
>
|
||||
<span class="txt">Delete selected</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let field = new SchemaField();
|
||||
export let value = false;
|
||||
</script>
|
||||
|
||||
<Field class="form-field form-field-toggle {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={value} />
|
||||
<label for={uniqueId}>{field.name}</label>
|
||||
</Field>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import Flatpickr from "svelte-flatpickr";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let field = new SchemaField();
|
||||
export let value = undefined;
|
||||
</script>
|
||||
|
||||
<Field class="form-field {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name} (UTC)</span>
|
||||
</label>
|
||||
<Flatpickr
|
||||
id={uniqueId}
|
||||
options={CommonHelper.defaultFlatpickrOptions()}
|
||||
{value}
|
||||
bind:formattedValue={value}
|
||||
/>
|
||||
</Field>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let field = new SchemaField();
|
||||
export let value = undefined;
|
||||
</script>
|
||||
|
||||
<Field class="form-field {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name}</span>
|
||||
</label>
|
||||
<input type="email" id={uniqueId} required={field.required} bind:value />
|
||||
</Field>
|
||||
@@ -0,0 +1,177 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import UploadedFilePreview from "@/components/base/UploadedFilePreview.svelte";
|
||||
import PreviewPopup from "@/components/base/PreviewPopup.svelte";
|
||||
import RecordFilePreview from "@/components/records/RecordFilePreview.svelte";
|
||||
|
||||
export let record;
|
||||
export let value = null;
|
||||
export let uploadedFiles = []; // Array<File> array
|
||||
export let deletedFileIndexes = []; // Array<int> array
|
||||
export let field = new SchemaField();
|
||||
|
||||
let fileInput;
|
||||
let previewPopup;
|
||||
let filesListElem;
|
||||
|
||||
// normalize uploadedFiles type
|
||||
$: if (!Array.isArray(uploadedFiles)) {
|
||||
uploadedFiles = CommonHelper.toArray(uploadedFiles);
|
||||
}
|
||||
|
||||
// normalize delited file indexes
|
||||
$: if (!Array.isArray(deletedFileIndexes)) {
|
||||
deletedFileIndexes = CommonHelper.toArray(deletedFileIndexes);
|
||||
}
|
||||
|
||||
$: isMultiple = field.options?.maxSelect > 1;
|
||||
|
||||
$: if (typeof value === "undefined" || value === null) {
|
||||
value = isMultiple ? [] : null;
|
||||
}
|
||||
|
||||
$: valueAsArray = CommonHelper.toArray(value);
|
||||
|
||||
$: maxReached =
|
||||
(valueAsArray.length || uploadedFiles.length) &&
|
||||
field.options?.maxSelect <= valueAsArray.length + uploadedFiles.length - deletedFileIndexes.length;
|
||||
|
||||
$: if (uploadedFiles !== -1 || deletedFileIndexes !== -1) {
|
||||
triggerListChange();
|
||||
}
|
||||
|
||||
function restoreExistingFile(valueIndex) {
|
||||
CommonHelper.removeByValue(deletedFileIndexes, valueIndex);
|
||||
deletedFileIndexes = deletedFileIndexes;
|
||||
}
|
||||
|
||||
function removeExistingFile(valueIndex) {
|
||||
CommonHelper.pushUnique(deletedFileIndexes, valueIndex);
|
||||
deletedFileIndexes = deletedFileIndexes;
|
||||
}
|
||||
|
||||
function removeNewFile(index) {
|
||||
if (!CommonHelper.isEmpty(uploadedFiles[index])) {
|
||||
uploadedFiles.splice(index, 1);
|
||||
}
|
||||
uploadedFiles = uploadedFiles;
|
||||
}
|
||||
|
||||
// emulate native change event
|
||||
function triggerListChange() {
|
||||
filesListElem?.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: { value, uploadedFiles, deletedFileIndexes },
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field class="form-field form-field-file {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name}</span>
|
||||
</label>
|
||||
|
||||
<div bind:this={filesListElem} class="files-list">
|
||||
{#each valueAsArray as filename, i (filename)}
|
||||
<div class="list-item">
|
||||
<figute
|
||||
class="thumb"
|
||||
class:fade={deletedFileIndexes.includes(i)}
|
||||
class:link-fade={CommonHelper.hasImageExtension(filename)}
|
||||
title={CommonHelper.hasImageExtension(filename) ? "Preview" : ""}
|
||||
on:click={() =>
|
||||
CommonHelper.hasImageExtension(filename)
|
||||
? previewPopup?.show(ApiClient.Records.getFileUrl(record, filename))
|
||||
: false}
|
||||
>
|
||||
<RecordFilePreview {record} {filename} />
|
||||
</figute>
|
||||
<a
|
||||
href={ApiClient.Records.getFileUrl(record, filename)}
|
||||
class="filename"
|
||||
class:txt-strikethrough={deletedFileIndexes.includes(i)}
|
||||
title={"Download " + filename}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
download
|
||||
>
|
||||
/.../{filename}
|
||||
</a>
|
||||
|
||||
{#if deletedFileIndexes.includes(i)}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger btn-secondary"
|
||||
on:click={() => restoreExistingFile(i)}
|
||||
>
|
||||
<span class="txt">Restore</span>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm btn-circle btn-remove txt-hint"
|
||||
use:tooltip={"Remove file"}
|
||||
on:click={() => removeExistingFile(i)}
|
||||
>
|
||||
<i class="ri-close-line" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#each uploadedFiles as file, i}
|
||||
<div class="list-item">
|
||||
<figute class="thumb">
|
||||
<UploadedFilePreview {file} />
|
||||
</figute>
|
||||
<div class="filename" title={file.name}>
|
||||
<small class="label label-success m-r-5">New</small>
|
||||
<span class="txt">{file.name}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm btn-circle btn-remove"
|
||||
use:tooltip={"Remove file"}
|
||||
on:click={() => removeNewFile(i)}
|
||||
>
|
||||
<i class="ri-close-line" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if !maxReached}
|
||||
<div class="list-item btn-list-item">
|
||||
<input
|
||||
bind:this={fileInput}
|
||||
type="file"
|
||||
class="hidden"
|
||||
multiple={isMultiple}
|
||||
on:change={() => {
|
||||
for (let file of fileInput.files) {
|
||||
uploadedFiles.push(file);
|
||||
}
|
||||
uploadedFiles = uploadedFiles;
|
||||
fileInput.value = null; // reset
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm btn-block"
|
||||
on:click={() => fileInput?.click()}
|
||||
>
|
||||
<i class="ri-upload-cloud-line" />
|
||||
<span class="txt">Upload new file</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<PreviewPopup bind:this={previewPopup} />
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let field = new SchemaField();
|
||||
export let value = undefined;
|
||||
|
||||
$: if (typeof value !== "undefined" && typeof value !== "string" && value !== null) {
|
||||
// the JSON field support both js primitives and encoded JSON string
|
||||
// so we are normalizing the value to only a string
|
||||
value = JSON.stringify(value, null, 2);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field class="form-field {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name}</span>
|
||||
</label>
|
||||
<textarea id={uniqueId} required={field.required} class="txt-mono txt-sm" bind:value />
|
||||
</Field>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let field = new SchemaField();
|
||||
export let value = undefined;
|
||||
</script>
|
||||
|
||||
<Field class="form-field {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name}</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id={uniqueId}
|
||||
required={field.required}
|
||||
min={field.options?.min}
|
||||
max={field.options?.max}
|
||||
bind:value
|
||||
/>
|
||||
</Field>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import RecordSelect from "@/components/records/RecordSelect.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let field = new SchemaField();
|
||||
export let value = undefined;
|
||||
|
||||
$: isMultiple = field.options?.maxSelect > 1;
|
||||
|
||||
$: if (isMultiple && Array.isArray(value) && value.length > field.options.maxSelect) {
|
||||
value = value.slice(field.options.maxSelect - 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field class="form-field {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name}</span>
|
||||
</label>
|
||||
<RecordSelect
|
||||
toggle
|
||||
id={uniqueId}
|
||||
multiple={isMultiple}
|
||||
collectionId={field.options?.collectionId}
|
||||
bind:keyOfSelected={value}
|
||||
/>
|
||||
{#if field.options?.maxSelect > 1}
|
||||
<div class="help-block">Select up to {field.options.maxSelect} items.</div>
|
||||
{/if}
|
||||
</Field>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Select from "@/components/base/Select.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let field = new SchemaField();
|
||||
export let value = undefined;
|
||||
|
||||
$: isMultiple = field.options?.maxSelect > 1;
|
||||
|
||||
$: if (typeof value === "undefined") {
|
||||
value = isMultiple ? [] : null;
|
||||
}
|
||||
|
||||
$: if (isMultiple && Array.isArray(value) && value.length > field.options.maxSelect) {
|
||||
value = value.slice(value.length - field.options.maxSelect);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field class="form-field {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name}</span>
|
||||
</label>
|
||||
<Select
|
||||
id={uniqueId}
|
||||
toggle={!field.required || isMultiple}
|
||||
multiple={isMultiple}
|
||||
items={field.options?.values}
|
||||
searchable={field.options?.values > 5}
|
||||
bind:selected={value}
|
||||
/>
|
||||
{#if field.options?.maxSelect > 1}
|
||||
<div class="help-block">Select up to {field.options.maxSelect} items.</div>
|
||||
{/if}
|
||||
</Field>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import AutoExpandTextarea from "@/components/base/AutoExpandTextarea.svelte";
|
||||
|
||||
export let field = new SchemaField();
|
||||
export let value = undefined;
|
||||
</script>
|
||||
|
||||
<Field class="form-field {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name}</span>
|
||||
</label>
|
||||
<AutoExpandTextarea id={uniqueId} required={field.required} bind:value />
|
||||
</Field>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let field = new SchemaField();
|
||||
export let value = undefined;
|
||||
</script>
|
||||
|
||||
<Field class="form-field {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name}</span>
|
||||
</label>
|
||||
<input type="url" id={uniqueId} required={field.required} bind:value />
|
||||
</Field>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import UserSelect from "@/components/users/UserSelect.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let field = new SchemaField();
|
||||
export let value = undefined;
|
||||
|
||||
// to prevent accidental changes, disable editing system user field values from the UI
|
||||
$: isDisabled = !CommonHelper.isEmpty(value) && field.system;
|
||||
|
||||
$: isMultiple = field.options?.maxSelect > 1;
|
||||
|
||||
$: if (isMultiple && Array.isArray(value) && value.length > field.options.maxSelect) {
|
||||
value = value.slice(field.options.maxSelect - 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field
|
||||
class="form-field {field.required ? 'required' : ''} {isDisabled ? 'disabled' : ''}"
|
||||
name={field.name}
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name}</span>
|
||||
</label>
|
||||
<UserSelect toggle id={uniqueId} multiple={isMultiple} disabled={isDisabled} bind:keyOfSelected={value} />
|
||||
{#if field.options?.maxSelect > 1}
|
||||
<div class="help-block">Select up to {field.options.maxSelect} users.</div>
|
||||
{/if}
|
||||
</Field>
|
||||
Reference in New Issue
Block a user