fixed relation and file custom change event trigger
This commit is contained in:
File diff suppressed because one or more lines are too long
2
ui/dist/index.html
vendored
2
ui/dist/index.html
vendored
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<!-- prism -->
|
<!-- prism -->
|
||||||
<script src="./libs/prism/prism.js" data-manual></script>
|
<script src="./libs/prism/prism.js" data-manual></script>
|
||||||
<script type="module" crossorigin src="./assets/index-DhtJzO0I.js"></script>
|
<script type="module" crossorigin src="./assets/index-OxsdchXY.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="./assets/pocketbase.es-B_4DUNUU.js">
|
<link rel="modulepreload" crossorigin href="./assets/pocketbase.es-B_4DUNUU.js">
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-BLIFQr7L.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-BLIFQr7L.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "dprint fmt && vite build"
|
||||||
"format": "dprint fmt"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function input(props) {
|
|||||||
|
|
||||||
// trigger custom change event for clearing field errors
|
// trigger custom change event for clearing field errors
|
||||||
function triggerChangeEvent() {
|
function triggerChangeEvent() {
|
||||||
fieldEl?.dispatchEvent(
|
fieldContentEl?.dispatchEvent(
|
||||||
new CustomEvent("change", {
|
new CustomEvent("change", {
|
||||||
detail: { data: props },
|
detail: { data: props },
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
@@ -92,7 +92,6 @@ export function input(props) {
|
|||||||
const fileInput = t.input({
|
const fileInput = t.input({
|
||||||
type: "file",
|
type: "file",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
name: () => props.field.name,
|
|
||||||
multiple: () => props.field.maxSelect > 1,
|
multiple: () => props.field.maxSelect > 1,
|
||||||
accept: () => props.field.mimeTypes?.join(",") || undefined,
|
accept: () => props.field.mimeTypes?.join(",") || undefined,
|
||||||
onchange: (e) => {
|
onchange: (e) => {
|
||||||
@@ -101,7 +100,125 @@ export function input(props) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fieldEl = t.div(
|
const fieldContentEl = t.output(
|
||||||
|
{
|
||||||
|
className: "field-content",
|
||||||
|
name: () => props.field.name,
|
||||||
|
},
|
||||||
|
// @todo enable ordering new files before/inbetween existing
|
||||||
|
app.components.sortable({
|
||||||
|
className: "list",
|
||||||
|
data: () => {
|
||||||
|
const vals = app.utils.toArray(props.record[props.field.name]);
|
||||||
|
|
||||||
|
let hadInvalid = false;
|
||||||
|
// filter empty or invalid values (e.g. from old serialized draft)
|
||||||
|
for (let i = vals.length - 1; i >= 0; i--) {
|
||||||
|
if (typeof vals[i] == "string" || vals[i] instanceof Blob) {
|
||||||
|
continue; // valid
|
||||||
|
}
|
||||||
|
|
||||||
|
hadInvalid = true;
|
||||||
|
vals.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update record model to prevent conflict with required and other validators
|
||||||
|
if (hadInvalid) {
|
||||||
|
props.record[props.field.name] = vals;
|
||||||
|
}
|
||||||
|
|
||||||
|
return vals;
|
||||||
|
},
|
||||||
|
onchange: (sortedList) => {
|
||||||
|
props.record[props.field.name] = sortedList;
|
||||||
|
triggerChangeEvent();
|
||||||
|
},
|
||||||
|
dataItem: (nameOrFile, i) => {
|
||||||
|
return t.div(
|
||||||
|
{
|
||||||
|
rid: nameOrFile,
|
||||||
|
className: () => `list-item highlight ${isDeleted(nameOrFile) ? "deleted" : ""}`,
|
||||||
|
},
|
||||||
|
t.div({ className: "content gap-10" }, () => {
|
||||||
|
if (typeof nameOrFile == "string") {
|
||||||
|
return [
|
||||||
|
app.components.recordFileThumb({
|
||||||
|
record: props.record,
|
||||||
|
filename: nameOrFile,
|
||||||
|
}),
|
||||||
|
t.button(
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
ariaDescription: app.attrs.tooltip("Open in new tab"),
|
||||||
|
onclick: async () => {
|
||||||
|
const token = await app.getFileToken(props.record.collectionId);
|
||||||
|
const url = app.pb.files.getURL(props.record, nameOrFile, {
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
window.open(url, "_blank", "noreferrer,noopener");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
t.span({ className: "txt link-primary" }, nameOrFile),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
app.components.uploadedFileThumb({
|
||||||
|
file: nameOrFile,
|
||||||
|
}),
|
||||||
|
t.span({ className: "label success" }, "New"),
|
||||||
|
t.span({ className: "txt" }, nameOrFile.name),
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
t.div(
|
||||||
|
{ className: "actions" },
|
||||||
|
t.button(
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
className: "btn sm secondary transparent circle",
|
||||||
|
ariaLabel: app.attrs.tooltip("Remove file"),
|
||||||
|
hidden: () => isDeleted(nameOrFile),
|
||||||
|
onclick: () => toDelete(nameOrFile),
|
||||||
|
},
|
||||||
|
t.i({ className: "ri-close-line", ariaHidden: true }),
|
||||||
|
),
|
||||||
|
t.button(
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
className: "btn sm warning transparent",
|
||||||
|
hidden: () => !isDeleted(nameOrFile),
|
||||||
|
onclick: () => restoreDeleted(nameOrFile),
|
||||||
|
},
|
||||||
|
t.span({ className: "txt" }, "Restore"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
t.hr({
|
||||||
|
className: "m-t-5 m-b-0",
|
||||||
|
hidden: () => app.utils.toArray(props.record[props.field.name]).length > 0,
|
||||||
|
}),
|
||||||
|
t.button(
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
className: "btn sm secondary block",
|
||||||
|
title: () => local.maxReached ? "Max allowed files reached" : undefined,
|
||||||
|
disabled: () => local.maxReached,
|
||||||
|
onclick: (e) => {
|
||||||
|
if (!local.maxReached) {
|
||||||
|
fileInput?.click();
|
||||||
|
}
|
||||||
|
document.activeElement?.blur();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
t.i({ className: "ri-upload-cloud-line", ariaHidden: true }),
|
||||||
|
t.span({ className: "txt" }, "Upload or drop new file"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return t.div(
|
||||||
{
|
{
|
||||||
className: "record-field-input field-type-file",
|
className: "record-field-input field-type-file",
|
||||||
ondragover: (e) => {
|
ondragover: (e) => {
|
||||||
@@ -130,120 +247,7 @@ export function input(props) {
|
|||||||
t.span({ className: "txt" }, () => props.field.name),
|
t.span({ className: "txt" }, () => props.field.name),
|
||||||
),
|
),
|
||||||
fileInput,
|
fileInput,
|
||||||
t.div(
|
fieldContentEl,
|
||||||
{ className: "field-content" },
|
|
||||||
// @todo enable ordering new files before/inbetween existing
|
|
||||||
app.components.sortable({
|
|
||||||
className: "list",
|
|
||||||
data: () => {
|
|
||||||
const vals = app.utils.toArray(props.record[props.field.name]);
|
|
||||||
|
|
||||||
let hadInvalid = false;
|
|
||||||
// filter empty or invalid values (e.g. from old serialized draft)
|
|
||||||
for (let i = vals.length - 1; i >= 0; i--) {
|
|
||||||
if (typeof vals[i] == "string" || vals[i] instanceof Blob) {
|
|
||||||
continue; // valid
|
|
||||||
}
|
|
||||||
|
|
||||||
hadInvalid = true;
|
|
||||||
vals.splice(i, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update record model to prevent conflict with required and other validators
|
|
||||||
if (hadInvalid) {
|
|
||||||
props.record[props.field.name] = vals;
|
|
||||||
}
|
|
||||||
|
|
||||||
return vals;
|
|
||||||
},
|
|
||||||
onchange: (sortedList) => {
|
|
||||||
props.record[props.field.name] = sortedList;
|
|
||||||
triggerChangeEvent();
|
|
||||||
},
|
|
||||||
dataItem: (nameOrFile, i) => {
|
|
||||||
return t.div(
|
|
||||||
{
|
|
||||||
rid: nameOrFile,
|
|
||||||
className: () => `list-item highlight ${isDeleted(nameOrFile) ? "deleted" : ""}`,
|
|
||||||
},
|
|
||||||
t.div({ className: "content gap-10" }, () => {
|
|
||||||
if (typeof nameOrFile == "string") {
|
|
||||||
return [
|
|
||||||
app.components.recordFileThumb({
|
|
||||||
record: props.record,
|
|
||||||
filename: nameOrFile,
|
|
||||||
}),
|
|
||||||
t.button(
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
ariaDescription: app.attrs.tooltip("Open in new tab"),
|
|
||||||
onclick: async () => {
|
|
||||||
const token = await app.getFileToken(props.record.collectionId);
|
|
||||||
const url = app.pb.files.getURL(props.record, nameOrFile, {
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
window.open(url, "_blank", "noreferrer,noopener");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
t.span({ className: "txt link-primary" }, nameOrFile),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
app.components.uploadedFileThumb({
|
|
||||||
file: nameOrFile,
|
|
||||||
}),
|
|
||||||
t.span({ className: "label success" }, "New"),
|
|
||||||
t.span({ className: "txt" }, nameOrFile.name),
|
|
||||||
];
|
|
||||||
}),
|
|
||||||
t.div(
|
|
||||||
{ className: "actions" },
|
|
||||||
t.button(
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
className: "btn sm secondary transparent circle",
|
|
||||||
ariaLabel: app.attrs.tooltip("Remove file"),
|
|
||||||
hidden: () => isDeleted(nameOrFile),
|
|
||||||
onclick: () => toDelete(nameOrFile),
|
|
||||||
},
|
|
||||||
t.i({ className: "ri-close-line", ariaHidden: true }),
|
|
||||||
),
|
|
||||||
t.button(
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
className: "btn sm warning transparent",
|
|
||||||
hidden: () => !isDeleted(nameOrFile),
|
|
||||||
onclick: () => restoreDeleted(nameOrFile),
|
|
||||||
},
|
|
||||||
t.span({ className: "txt" }, "Restore"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
t.hr({
|
|
||||||
className: "m-t-5 m-b-0",
|
|
||||||
hidden: () => app.utils.toArray(props.record[props.field.name]).length > 0,
|
|
||||||
}),
|
|
||||||
t.button(
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
className: "btn sm secondary block",
|
|
||||||
title: () => local.maxReached ? "Max allowed files reached" : undefined,
|
|
||||||
disabled: () => local.maxReached,
|
|
||||||
onclick: (e) => {
|
|
||||||
if (!local.maxReached) {
|
|
||||||
fileInput.click();
|
|
||||||
}
|
|
||||||
document.activeElement?.blur();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
t.i({ className: "ri-upload-cloud-line", ariaHidden: true }),
|
|
||||||
t.span({ className: "txt" }, "Upload or drop new file"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
() => {
|
() => {
|
||||||
if (props.field.help) {
|
if (props.field.help) {
|
||||||
@@ -251,6 +255,4 @@ export function input(props) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return fieldEl;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function input(props) {
|
|||||||
|
|
||||||
// trigger custom change event for clearing field errors
|
// trigger custom change event for clearing field errors
|
||||||
function triggerChangeEvent() {
|
function triggerChangeEvent() {
|
||||||
fieldEl?.dispatchEvent(
|
fieldContentEl?.dispatchEvent(
|
||||||
new CustomEvent("change", {
|
new CustomEvent("change", {
|
||||||
detail: { data: props },
|
detail: { data: props },
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
@@ -87,6 +87,7 @@ export function input(props) {
|
|||||||
|
|
||||||
function updateRecordValue(ids = []) {
|
function updateRecordValue(ids = []) {
|
||||||
props.record[props.field.name] = props.field.maxSelect > 1 ? ids : ids?.[0] || "";
|
props.record[props.field.name] = props.field.maxSelect > 1 ? ids : ids?.[0] || "";
|
||||||
|
triggerChangeEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
const watchers = [
|
const watchers = [
|
||||||
@@ -96,7 +97,82 @@ export function input(props) {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
const fieldEl = t.div(
|
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(
|
||||||
|
{
|
||||||
|
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",
|
className: "record-field-input field-type-relation",
|
||||||
onunmount: () => {
|
onunmount: () => {
|
||||||
@@ -110,81 +186,7 @@ export function input(props) {
|
|||||||
t.i({ className: app.fieldTypes.relation.icon, ariaHidden: true }),
|
t.i({ className: app.fieldTypes.relation.icon, ariaHidden: true }),
|
||||||
t.span({ className: "txt" }, () => props.field.name),
|
t.span({ className: "txt" }, () => props.field.name),
|
||||||
),
|
),
|
||||||
t.output(
|
fieldContentEl,
|
||||||
{
|
|
||||||
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));
|
|
||||||
triggerChangeEvent();
|
|
||||||
},
|
|
||||||
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(
|
|
||||||
{
|
|
||||||
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"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
() => {
|
() => {
|
||||||
if (props.field.help) {
|
if (props.field.help) {
|
||||||
@@ -192,6 +194,4 @@ export function input(props) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return fieldEl;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user