window.app = window.app || {}; window.app.modals = window.app.modals || {}; const recordsPerPage = 100; const defaultSettings = { btnText: "Insert", fileTypes: [], // "image", "document", "video", "audio", "file" onselect: function(selectedFile) {}, }; const LAST_SELECTED_STORAGE_KEY = "pbLastRecordFilePickerCollection"; const RECORDS_REQUEST_KEY = "listFilePickerRecords"; /** * Opens a new record file picker. * * @example * ```js * app.modals.openRecordFilePicker({ * onselect: (selectedFile) => { ... } * }) * ``` * * @param {Object} settings */ window.app.modals.openRecordFilePicker = function(settings = {}) { settings = Object.assign({}, defaultSettings, settings); const modal = recordFilePickerModal(settings); document.body.appendChild(modal); app.modals.open(modal); }; function recordFilePickerModal(settings = defaultSettings) { let modal; const uniqueId = "file_picker_" + app.utils.randomString(); const data = store({ selectedFile: {}, records: [], activeCollectionId: "", searchTerm: "", lastRecordsPage: 1, lastTotalRecords: 0, isLoadingRecords: false, get collections() { return app.utils.sortedCollections( app.store.collections.filter((c) => { if (c.type == "view") { return false; } // has at least one public file key return !!c.fields?.find((f) => { return f.type === "file" && !f.protected; }); }), ); }, get activeCollection() { const collection = data.collections.find((c) => c.id == data.activeCollectionId); if (collection) { return collection; } // always fallback to the first one (if available) return data.collections[0]; }, get activeCollectionFileFields() { return data.activeCollection?.fields?.filter((f) => f.type === "file" && !f.protected) || []; }, get isLoading() { return app.store.isLoadingCollections || data.isLoadingRecords; }, get canLoadMore() { return !data.isLoadingRecords && data.lastTotalRecords == recordsPerPage; }, get hasAtleastOneFile() { return !!data.records.find((r) => extractFiles(r).length > 0); }, }); const watchers = []; // load and sync activeCollectionId from/to localStorage watchers.push( watch(() => { if (!data.activeCollectionId) { data.activeCollectionId = window.localStorage.getItem(LAST_SELECTED_STORAGE_KEY); } else { window.localStorage.setItem(LAST_SELECTED_STORAGE_KEY, data.activeCollectionId); data.searchTerm = ""; // reset } }), ); // reload records on search or active collection change watchers.push( watch( () => [data.activeCollection, data.searchTerm], () => loadRecords(true), ), ); function resetList() { app.pb.cancelRequest(RECORDS_REQUEST_KEY); data.isLoadingRecords = false; data.records = []; data.lastTotalRecords = 0; data.lastRecordsPage = 1; data.selectedFile = {}; } async function loadRecords(reset = false) { if (!data.activeCollection) { resetList(); return; } if (reset) { resetList(); } data.isLoadingRecords = true; try { const page = reset ? 1 : data.lastRecordsPage + 1; const fallbackSearchFields = app.utils.getAllCollectionIdentifiers(data.activeCollection); let normalizedFilter = app.utils.normalizeSearchFilter(data.searchTerm, fallbackSearchFields) || ""; if (normalizedFilter) { normalizedFilter += " && "; } normalizedFilter += "(" + data.activeCollectionFileFields.map((f) => `${f.name}:length>0`).join("||") + ")"; const result = await app.pb.collection(data.activeCollection.id).getList(page, recordsPerPage, { requestKey: RECORDS_REQUEST_KEY, filter: normalizedFilter, skipTotal: 1, sort: data.activeCollection.type != "view" ? "-@rowid" : "", }); data.lastRecordsPage = result.page; data.lastTotalRecords = result.items.length; data.records = app.utils.filterDuplicatesByKey(data.records.concat(result.items)); data.isLoadingRecords = false; } catch (err) { if (!err.isAbort) { data.isLoadingRecords = false; app.checkApiError(err); } } } function extractFiles(record) { let result = []; for (const field of data.activeCollectionFileFields) { const names = app.utils.toArray(record[field.name]); for (const name of names) { if ( app.utils.isEmpty(settings.fileTypes) || settings.fileTypes?.includes(app.utils.getFileType(name)) ) { result.push(name); } } } return result; } function selectFile(record, name) { data.selectedFile = { record, name, thumb: "" }; } function isSelected(record, name) { return data.selectedFile?.name == name && data.selectedFile?.record?.id == record?.id; } const documentEvents = { "record:create": (e) => { if (e.detail.collectionId != data.activeCollection?.id) { return; } if (data.selectedFile?.record?.id == e.detail.id) { data.selectedFile.record = e.detail; } loadRecords(true); }, "record:delete": (e) => { if ( // check both because for delete we don't know which one was assigned to e.detail.collectionId != data.activeCollection?.id && e.detail.collectionName != data.activeCollection?.name ) { return; } if (data.selectedFile?.record?.id == e.detail.id) { data.selectedFile = {}; } loadRecords(true); }, }; modal = t.div( { className: "modal popup record-file-picker-modal", onafterclose: (el) => { el?.remove(); }, onmount: (el) => { for (let event in documentEvents) { document.addEventListener(event, documentEvents[event]); } }, onunmount: (el) => { watchers.forEach((w) => w?.unwatch()); for (let event in documentEvents) { document.removeEventListener(event, documentEvents[event]); } }, }, t.header( { className: "modal-header" }, // collections select t.button( { className: () => `btn primary outline record-file-picker-collection-select-btn ${ app.store.isLoadingCollections ? "loading" : "" }`, disabled: () => app.store.isLoadingCollections, "html-popovertarget": "collections_dropdown" + uniqueId, }, t.span( { className: "txt-lg collection-name m-r-auto" }, () => data.activeCollection?.name || "Select collection", ), t.i({ className: "ri-arrow-drop-down-line" }), ), t.div( { id: "collections_dropdown" + uniqueId, className: "dropdown", popover: "hint" }, () => { return data.collections.map((c) => { return t.button( { type: "button", className: () => `dropdown-item ${data.activeCollectionId == c.id ? "active" : ""}`, onclick: (e) => { data.activeCollectionId = c.id; e.target?.closest(".dropdown")?.hidePopover(); }, }, c.name, ); }); }, ), // search app.components.recordsSearchbar({ disabled: () => !data.activeCollection?.id, collection: () => data.activeCollection, value: () => data.searchTerm, onsubmit: (newFilter) => (data.searchTerm = newFilter), }), // new record t.button( { type: "button", className: "btn circle transparent", ariaDescription: app.attrs.tooltip("Add new record"), onclick: () => app.modals.openRecordUpsert(data.activeCollection), }, t.i({ className: "ri-add-line txt-hint" }), ), ), t.div( { className: "modal-content" }, // initial loader t.div( { className: "block txt-center", hidden: () => data.hasAtleastOneFile || !data.isLoading, }, t.span({ className: "loader" }), ), // files list t.div({ className: "record-file-picker-list" }, () => { const result = []; for (const record of data.records) { const files = extractFiles(record); for (const name of files) { result.push( t.button( { rid: record.id + ":" + name, className: () => `list-item thumb ${isSelected(record, name) ? "success" : ""}`, ariaDescription: app.attrs.tooltip(name, "bottom"), onclick: () => selectFile(record, name), }, () => { if (app.utils.hasImageExtension(name)) { return t.img({ loading: "lazy", src: app.pb.files.getURL(record, name, { thumb: "100x100" }), alt: name, }); } const ftype = app.utils.getFileType(name); return t.i({ className: app.utils.fileTypeIcons[ftype] || "ri-file-line" }); }, ), ); } } return result; }), // load more t.div( { hidden: () => !data.canLoadMore || !data.hasAtleastOneFile, className: "block txt-center", }, t.button( { className: () => `btn secondary expanded-lg m-t-base ${data.isLoadingRecords ? "loading" : ""}`, disabled: () => data.isLoadingRecords, onclick: () => loadRecords(), }, t.span({ className: "txt" }, "Load more"), ), ), // no files t.div( { className: "block txt-center txt-hint p-t-10 p-b-10", hidden: () => data.hasAtleastOneFile || data.isLoading, }, () => { if (app.utils.isEmpty(settings.fileTypes)) { return t.p(null, "No records with selectable files found."); } return t.p(null, `No "${settings.fileTypes.join("\", \"")}" files found.`); }, t.button({ type: "button", className: "btn sm secondary", textContent: "Clear search", hidden: () => !data.searchTerm?.length, onclick: () => { data.searchTerm = ""; }, }), ), ), t.footer( { className: "modal-footer" }, t.button( { type: "button", className: "btn transparent m-r-auto", onclick: () => app.modals.close(modal), }, t.span({ className: "txt" }, "Close"), ), // image thumb selector () => { if (!data.selectedFile?.name || !app.utils.hasImageExtension(data.selectedFile.name)) { return; } const options = [ { value: "", label: "Original size" }, { value: "100x100", label: "100x100 thumb" }, ]; // find the related field and its thumbs const fileField = data.activeCollectionFileFields.find((f) => { return data.selectedFile.record[f.name].includes(data.selectedFile.name); }); const thumbs = app.utils.toArray(fileField.thumbs); for (let thumb of thumbs) { options.push({ value: thumb, label: `${thumb} thumb`, }); } return t.div( { className: "record-file-picker-thumb-select" }, app.components.select({ required: true, value: data.selectedFile.thumb || "", options: options, onchange: (opts) => { data.selectedFile.thumb = opts?.[0].value; }, }), ); }, // submit selected t.button( { type: "button", className: "btn expanded", disabled: () => data.isLoading || !data.selectedFile?.name, onclick: () => { const selected = JSON.parse(JSON.stringify(data.selectedFile)); if (settings.onselect && settings.onselect(selected) === false) { return false; } app.modals.close(modal); }, }, t.span({ className: "txt" }, () => settings.btnText || defaultSettings.btnText), ), ), ); return modal; }