Files
pocketbase/ui/src/components/records/RecordFilePicker.svelte

347 lines
11 KiB
Svelte

<script>
import { createEventDispatcher } from "svelte";
import CommonHelper from "@/utils/CommonHelper";
import ApiClient from "@/utils/ApiClient";
import scrollend from "@/actions/scrollend";
import tooltip from "@/actions/tooltip";
import { collections } from "@/stores/collections";
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
import Searchbar from "@/components/base/Searchbar.svelte";
import Field from "@/components/base/Field.svelte";
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
import RecordUpsertPanel from "@/components/records/RecordUpsertPanel.svelte";
const dispatch = createEventDispatcher();
const uniqueId = "file_picker_" + CommonHelper.randomString(5);
const batchSize = 50;
export let title = "Select a file";
export let submitText = "Insert";
export let fileTypes = ["image", "document", "video", "audio", "file"];
let pickerPanel;
let upsertPanel;
let filter = "";
let list = [];
let currentPage = 1;
let lastItemsCount = 0;
let isLoading = false;
let fileCollections = [];
let fileFields = [];
let sizeOptions = [];
let selectedCollection = {};
let selectedFile = {};
let selectedSize = "";
// find all collections with at least one non-protected file field
$: fileCollections = $collections.filter((c) => {
return (
c.type !== "view" &&
!!CommonHelper.toArray(c.schema).find((f) => f.type === "file" && !f.options?.protected)
);
});
// auto select the first collection from the list
$: if (!selectedCollection?.id && fileCollections.length > 0) {
selectedCollection = fileCollections[0];
}
$: fileFields = selectedCollection?.schema?.filter((f) => f.type === "file" && !f.options?.protected);
// reset filter on collection change
$: if (selectedCollection?.id) {
clearFilter();
refreshSizeOptions();
}
// refresh the size options on selected file change
$: if (selectedFile?.name) {
refreshSizeOptions();
}
// reset list on filter or collection change
$: if (typeof filter !== "undefined" && selectedCollection?.id && pickerPanel?.isActive()) {
loadList(true);
}
$: isSelected = (record, name) => {
return selectedFile?.name == name && selectedFile?.record?.id == record.id;
};
$: hasAtleastOneFile = list.find((r) => extractFiles(r).length > 0);
$: canLoadMore = !isLoading && lastItemsCount == batchSize;
$: canSubmit = !isLoading && !!selectedFile?.name;
export function show() {
loadList(true);
return pickerPanel?.show();
}
export function hide() {
return pickerPanel?.hide();
}
function clearList() {
list = [];
selectedFile = {};
selectedSize = "";
}
function clearFilter() {
filter = "";
}
async function loadList(reset = false) {
if (!selectedCollection?.id) {
return;
}
isLoading = true;
if (reset) {
clearList();
}
try {
const page = reset ? 1 : currentPage + 1;
const fallbackSearchFields = CommonHelper.getAllCollectionIdentifiers(selectedCollection);
const result = await ApiClient.collection(selectedCollection.id).getList(page, batchSize, {
filter: CommonHelper.normalizeSearchFilter(filter, fallbackSearchFields),
sort: "-created",
fields: "*:excerpt(100)",
skipTotal: 1,
requestKey: uniqueId + "loadImagePicker",
});
list = CommonHelper.filterDuplicatesByKey(list.concat(result.items));
currentPage = result.page;
lastItemsCount = result.items.length;
isLoading = false;
} catch (err) {
if (!err.isAbort) {
ApiClient.error(err);
isLoading = false;
}
}
}
function refreshSizeOptions() {
let sizes = ["100x100"]; // default Admin UI thumb
// extract the thumb sizes of the selected file field
if (selectedFile?.record?.id) {
for (const field of fileFields) {
if (CommonHelper.toArray(selectedFile.record[field.name]).includes(selectedFile.name)) {
sizes = sizes.concat(CommonHelper.toArray(field.options?.thumbs));
break;
}
}
}
// constuct the dropdown options
sizeOptions = [{ label: "Original size", value: "" }];
for (const size of sizes) {
sizeOptions.push({
label: `${size} thumb`,
value: size,
});
}
// reset selected size if missing
if (selectedSize && !sizes.includes(selectedSize)) {
selectedSize = "";
}
}
function extractFiles(record) {
let result = [];
for (const field of fileFields) {
const names = CommonHelper.toArray(record[field.name]);
for (const name of names) {
if (fileTypes.includes(CommonHelper.getFileType(name))) {
result.push(name);
}
}
}
return result;
}
function select(record, name) {
selectedFile = { record, name };
}
function submit() {
if (!canSubmit) {
return;
}
dispatch(
"submit",
Object.assign(
{
size: selectedSize,
},
selectedFile
)
);
hide();
}
</script>
<OverlayPanel bind:this={pickerPanel} popup class="file-picker-popup" on:hide on:show {...$$restProps}>
<svelte:fragment slot="header">
<h4>{title}</h4>
</svelte:fragment>
{#if !fileCollections.length}
<h6 class="txt-center txt-hint">
You currently don't have any collection with <code>file</code> field.
</h6>
{:else}
<div class="file-picker">
<aside class="file-picker-sidebar">
{#each fileCollections as collection (collection.id)}
<button
type="button"
class="sidebar-item"
class:active={selectedCollection?.id == collection.id}
on:click|preventDefault={() => {
selectedCollection = collection;
}}
>
{collection.name}
</button>
{/each}
</aside>
<div class="file-picker-content">
<div class="flex m-b-base flex-gap-10">
<Searchbar
value={filter}
placeholder="Record search term or filter..."
autocompleteCollection={selectedCollection}
on:submit={(e) => (filter = e.detail)}
/>
<button
type="button"
class="btn btn-pill btn-transparent btn-hint p-l-xs p-r-xs"
on:click={() => upsertPanel?.show()}
>
<div class="txt">New record</div>
</button>
</div>
<div
class="files-list"
use:scrollend={{
threshold: 150,
callback: () => {
if (canLoadMore) {
loadList();
}
},
}}
>
{#if hasAtleastOneFile}
{#each list as record (record.id)}
{@const names = extractFiles(record)}
{#each names as name}
<button
type="button"
class="thumb thumb-xl handle"
use:tooltip={name + "\n(record: " + record.id + ")"}
class:thumb-warning={isSelected(record, name)}
on:click|preventDefault={select(record, name)}
>
{#if CommonHelper.hasImageExtension(name)}
<img
loading="lazy"
src={ApiClient.files.getUrl(record, name, { thumb: "100x100" })}
alt={name}
/>
{:else}
<i class="ri-file-3-line" />
{/if}
</button>
{/each}
{/each}
{:else if !isLoading}
<div class="inline-flex">
<span class="txt txt-hint">No records found.</span>
{#if filter?.length}
<button
type="button"
class="btn btn-hint btn-sm"
on:click|preventDefault={clearFilter}
>
<span class="txt">Clear filter</span>
</button>
{/if}
</div>
{/if}
{#if isLoading}
<div class="block txt-center">
<span class="loader loader-sm active" />
</div>
{/if}
</div>
</div>
</div>
{/if}
<svelte:fragment slot="footer">
<button type="button" class="btn btn-transparent m-r-auto" disabled={isLoading} on:click={hide}>
<span class="txt">Cancel</span>
</button>
{#if CommonHelper.hasImageExtension(selectedFile?.name)}
<Field class="form-field file-picker-size-select" let:uniqueId>
<ObjectSelect
upside
id={uniqueId}
items={sizeOptions}
disabled={!canSubmit}
selectPlaceholder="Select size"
bind:keyOfSelected={selectedSize}
/>
</Field>
{/if}
<button type="button" class="btn btn-expanded" disabled={!canSubmit} on:click={submit}>
<span class="txt">{submitText}</span>
</button>
</svelte:fragment>
</OverlayPanel>
<RecordUpsertPanel
bind:this={upsertPanel}
collection={selectedCollection}
on:save={(e) => {
CommonHelper.removeByKey(list, "id", e.detail.record.id);
list.unshift(e.detail.record);
list = list;
const names = extractFiles(e.detail.record);
if (names.length > 0) {
select(e.detail.record, names[0]);
}
}}
on:delete={(e) => {
if (selectedFile?.record?.id == e.detail.id) {
selectedFile = {};
}
CommonHelper.removeByKey(list, "id", e.detail.id);
list = list;
}}
/>