mirror of
https://github.com/pocketbase/pocketbase.git
synced 2026-09-09 08:00:47 +02:00
216 lines
7.2 KiB
JavaScript
216 lines
7.2 KiB
JavaScript
// {
|
|
// collection: undefined,
|
|
// originalRecord: undefined,
|
|
// record: undefined,
|
|
// field: undefined,
|
|
// }
|
|
export function input(props) {
|
|
const uniqueId = "rel_" + app.utils.randomString();
|
|
|
|
// trigger custom change event for clearing field errors
|
|
function triggerChangeEvent() {
|
|
fieldContentEl?.dispatchEvent(
|
|
new CustomEvent("change", {
|
|
detail: { data: props },
|
|
bubbles: true,
|
|
}),
|
|
);
|
|
}
|
|
|
|
const local = store({
|
|
selected: [],
|
|
isLoading: true,
|
|
get maxReached() {
|
|
const maxSelect = props.field.maxSelect || 1;
|
|
return app.utils.toArray(props.record[props.field.name]).length >= maxSelect;
|
|
},
|
|
});
|
|
|
|
async function loadSelected() {
|
|
local.isLoading = true;
|
|
|
|
const ids = app.utils.toArray(props.record[props.field.name]);
|
|
if (!ids.length) {
|
|
local.selected = [];
|
|
local.isLoading = false;
|
|
return;
|
|
}
|
|
|
|
const resultRecords = [];
|
|
const idsToLoad = [];
|
|
|
|
// check for preloaded expand
|
|
const expanded = app.utils.toArray(props.record.expand?.[props.field.name]);
|
|
for (const id of ids) {
|
|
const found = expanded.find((r) => r.id == id);
|
|
if (found) {
|
|
resultRecords.push(found);
|
|
} else {
|
|
idsToLoad.push(id);
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (idsToLoad.length) {
|
|
const fieldCollection = app.store.collections.find((c) => c.id == props.field.collectionId);
|
|
|
|
// eagerly expand first level presentable relations (if any and the collections are loaded)
|
|
const relExpands = [];
|
|
const presentableRelationFields = fieldCollection?.fields?.filter(
|
|
(f) => !f.hidden && f.presentable && f.type == "relation",
|
|
) || [];
|
|
for (const field of presentableRelationFields) {
|
|
relExpands.push(field.name);
|
|
}
|
|
|
|
const records = await app.pb.collection(props.field.collectionId).getFullList({
|
|
requestKey: null,
|
|
filter: idsToLoad.map((id) => app.pb.filter("id={:id}", { id })).join("||"),
|
|
expand: relExpands.join(",") || undefined,
|
|
});
|
|
|
|
// preserve the original order
|
|
for (const id of idsToLoad) {
|
|
const found = records.find((r) => r.id == id);
|
|
if (found) {
|
|
resultRecords.push(found);
|
|
} else {
|
|
console.warn("missing relation id:", id);
|
|
}
|
|
}
|
|
}
|
|
|
|
local.selected = resultRecords;
|
|
local.isLoading = false;
|
|
} catch (err) {
|
|
if (!err.isAbort) {
|
|
app.checkApiError(err);
|
|
local.isLoading = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function remove(id) {
|
|
const ids = app.utils.toArray(props.record[props.field.name]);
|
|
const propIndex = ids.indexOf(id);
|
|
if (propIndex >= 0) {
|
|
ids.splice(propIndex, 1);
|
|
updateRecordValue(ids);
|
|
}
|
|
|
|
const selectedIndex = local.selected.findIndex((r) => r.id == id);
|
|
local.selected.splice(selectedIndex, 1);
|
|
}
|
|
|
|
function updateRecordValue(ids = []) {
|
|
props.record[props.field.name] = props.field.maxSelect > 1 ? ids : ids?.[0] || "";
|
|
triggerChangeEvent();
|
|
}
|
|
|
|
const watchers = [
|
|
watch(
|
|
() => props.record[props.field.name],
|
|
() => loadSelected(),
|
|
),
|
|
];
|
|
|
|
const fieldContentEl = t.output(
|
|
{
|
|
className: "field-content",
|
|
name: () => props.field.name,
|
|
},
|
|
// loader
|
|
t.div(
|
|
{
|
|
hidden: () => !local.isLoading,
|
|
className: "list",
|
|
},
|
|
() => {
|
|
const ids = app.utils.toArray(props.record[props.field.name]);
|
|
return ids.map(() => {
|
|
return t.div({ className: "list-item" }, t.span({ className: "skeleton-loader" }));
|
|
});
|
|
},
|
|
),
|
|
// list
|
|
app.components.sortable({
|
|
className: "list",
|
|
hidden: () => local.isLoading,
|
|
data: () => local.selected,
|
|
onchange: (sortedList) => {
|
|
local.selected = sortedList;
|
|
updateRecordValue(sortedList.map((r) => r.id));
|
|
},
|
|
dataItem: (record, relIndex) => {
|
|
return t.div(
|
|
{
|
|
rid: record,
|
|
className: "list-item highlight",
|
|
},
|
|
t.div({ className: "content" }, () => app.components.recordSummary(record)),
|
|
t.div(
|
|
{ className: "actions" },
|
|
t.button(
|
|
{
|
|
type: "button",
|
|
className: "btn sm secondary transparent circle",
|
|
ariaLabel: app.attrs.tooltip("Remove"),
|
|
onclick: () => remove(record.id),
|
|
},
|
|
t.i({ className: "ri-close-line", ariaHidden: true }),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
}),
|
|
// picker btn
|
|
t.hr({
|
|
hidden: () => !app.utils.isEmpty(props.record[props.field.name]),
|
|
className: "m-t-5 m-b-0",
|
|
}),
|
|
t.button(
|
|
{
|
|
type: "button",
|
|
className: "btn sm secondary block",
|
|
disabled: () => local.isLoading,
|
|
onclick: (e) => {
|
|
app.modals.openRecordsPicker({
|
|
collection: props.field.collectionId,
|
|
selectedIds: app.utils.toArray(props.record[props.field.name]),
|
|
maxSelect: props.field.maxSelect,
|
|
onselect: (records) => {
|
|
local.selected = records;
|
|
updateRecordValue(records.map((r) => r.id));
|
|
},
|
|
});
|
|
},
|
|
},
|
|
t.i({ className: "ri-magic-line", ariaHidden: true }),
|
|
t.span({ className: "txt" }, "Open records picker"),
|
|
),
|
|
);
|
|
|
|
return t.div(
|
|
{
|
|
className: "record-field-input field-type-relation",
|
|
onunmount: () => {
|
|
watchers.forEach((w) => w?.unwatch());
|
|
},
|
|
},
|
|
t.div(
|
|
{ className: () => `field-list ${props.field.required ? "required" : ""}` },
|
|
t.label(
|
|
{ htmlFor: uniqueId },
|
|
t.i({ className: app.fieldTypes.relation.icon, ariaHidden: true }),
|
|
t.span({ className: "txt" }, () => props.field.name),
|
|
),
|
|
fieldContentEl,
|
|
),
|
|
() => {
|
|
if (props.field.help) {
|
|
return t.div({ className: "field-help" }, props.field.help);
|
|
}
|
|
},
|
|
);
|
|
}
|