pocketbase/ui/src/components/records/fields/RelationField.svelte

223 lines
6.5 KiB
Svelte

<script>
import { onDestroy } from "svelte";
import CommonHelper from "@/utils/CommonHelper";
import ApiClient from "@/utils/ApiClient";
import tooltip from "@/actions/tooltip";
import Field from "@/components/base/Field.svelte";
import Draggable from "@/components/base/Draggable.svelte";
import RecordsPicker from "@/components/records/RecordsPicker.svelte";
import RecordInfo from "@/components/records/RecordInfo.svelte";
const batchSize = 100;
export let field;
export let value;
export let picker;
let fieldRef;
let list = [];
let isLoading = false;
let loadTimeoutId;
let invalidIds = [];
let originalValue = value;
$: isMultiple = field.options?.maxSelect != 1;
$: if (typeof value != "undefined") {
fieldRef?.changed();
}
$: if (needLoad(list, value)) {
isLoading = true;
// Move the load function to the end of the execution queue.
//
// It helps reducing the layout shifts (the relation field has fixed height skeleton loader)
// and allows the other form fields to load sooner.
clearTimeout(loadTimeoutId);
loadTimeoutId = setTimeout(load, 0);
}
$: invalidIds = CommonHelper.toArray(originalValue).filter((id) => !list.find((item) => item.id == id));
function needLoad() {
if (isLoading) {
return false;
}
const ids = CommonHelper.toArray(value);
list = list.filter((item) => ids.includes(item.id));
return ids.length != list.length;
}
async function load() {
const ids = CommonHelper.toArray(value);
list = []; // reset
if (!field?.options?.collectionId || !ids.length) {
isLoading = false;
return;
}
isLoading = true;
// batch load all selected records to avoid parser stack overflow errors
const filterIds = ids.slice();
const loadPromises = [];
while (filterIds.length > 0) {
const filters = [];
for (const id of filterIds.splice(0, batchSize)) {
filters.push(`id="${id}"`);
}
loadPromises.push(
ApiClient.collection(field?.options?.collectionId).getFullList(batchSize, {
filter: filters.join("||"),
$autoCancel: false,
})
);
}
try {
let loadedItems = [];
await Promise.all(loadPromises).then((values) => {
loadedItems = loadedItems.concat(...values);
});
// preserve selected order
for (const id of ids) {
const rel = CommonHelper.findByKey(loadedItems, "id", id);
if (rel) {
list.push(rel);
}
}
list = list;
// ensure that any record that was deleted during the request
// is also removed from the relation value
listToValue();
} catch (err) {
ApiClient.error(err);
}
isLoading = false;
}
function remove(rel) {
CommonHelper.removeByKey(list, "id", rel.id);
list = list;
listToValue();
}
function listToValue() {
if (isMultiple) {
value = list.map((r) => r.id);
} else {
value = list[0]?.id || "";
}
}
onDestroy(() => {
clearTimeout(loadTimeoutId);
});
</script>
<Field
bind:this={fieldRef}
class="form-field form-field-list {field.required ? 'required' : ''}"
name={field.name}
let:uniqueId
>
<label for={uniqueId}>
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
<span class="txt">{field.name}</span>
{#if invalidIds.length}
<i
class="ri-error-warning-line link-hint m-l-auto flex-order-10"
use:tooltip={{
position: "left",
text:
"The following relation ids were removed from the list because they are missing or invalid: " +
invalidIds.join(", "),
}}
/>
{/if}
</label>
<div class="list">
<div class="relations-list">
{#each list as record, i (record.id)}
<Draggable
bind:list
group={field.name + "_relation"}
index={i}
disabled={!isMultiple}
let:dragging
let:dragover
on:sort={() => {
listToValue();
}}
>
<div class="list-item" class:dragging class:dragover>
<div class="content">
<RecordInfo {record} />
</div>
<div class="actions">
<button
type="button"
class="btn btn-transparent btn-hint btn-sm btn-circle btn-remove"
use:tooltip={"Remove"}
on:click={() => remove(record)}
>
<i class="ri-close-line" />
</button>
</div>
</div>
</Draggable>
{:else}
{#if isLoading}
{#each CommonHelper.toArray(value).slice(0, 10) as _}
<div class="list-item">
<div class="skeleton-loader" />
</div>
{/each}
{/if}
{/each}
</div>
<div class="list-item list-item-btn">
<button
type="button"
class="btn btn-transparent btn-sm btn-block"
on:click={() => picker?.show()}
>
<i class="ri-magic-line" />
<!-- <i class="ri-layout-line" /> -->
<span class="txt">Open picker</span>
</button>
</div>
</div>
</Field>
<RecordsPicker
bind:this={picker}
{value}
{field}
on:save={(e) => {
list = e.detail || [];
value = isMultiple ? list.map((r) => r.id) : list[0]?.id || "";
}}
/>
<style lang="scss">
.relations-list {
max-height: 300px;
overflow: auto; /* fallback */
overflow: overlay;
}
</style>