diff --git a/src/pat/contentbrowser/src/ContentBrowser.svelte b/src/pat/contentbrowser/src/ContentBrowser.svelte index e44a3c9d7..79d6d531f 100644 --- a/src/pat/contentbrowser/src/ContentBrowser.svelte +++ b/src/pat/contentbrowser/src/ContentBrowser.svelte @@ -8,9 +8,7 @@ import { resolveIcon } from "./resolveIcon.js"; import Upload from "../../upload/upload"; - import { - currentPath, - } from "./stores.js"; + import { currentPath } from "./stores.js"; // import Keydown from "svelte-keydown"; animateScroll.setGlobalOptions({ @@ -21,7 +19,7 @@ // get context stores const config = getContext("config"); - const pathCache = getContext('pathCache'); + const pathCache = getContext("pathCache"); const showContentBrowser = getContext("showContentBrowser"); const selectedItems = getContext("selectedItems"); const selectedUids = getContext("selectedUids"); @@ -54,7 +52,7 @@ allowPathSelection: false, hiddenInputContainer: ".upload-wrapper", success: (fileUpload, obj) => { - contentItems.get($currentPath, null, true); + contentItems.get({ path: $currentPath, updateCache: true }); }, }); } @@ -110,9 +108,15 @@ } } + function loadMore(entries, observer) { + entries.forEach(async (entry) => { + if (entry.intersectionRatio === 0 || contentItems.loading) return; + await contentItems.loadMore(entry.target.dataset.path, $currentPath); + }); + } + function itemInPath(item) { - const inPath = $currentPath.indexOf(item.getPath) != -1; - return inPath; + return $currentPath.indexOf(item.getPath) != -1; } function filterItems() { @@ -121,19 +125,26 @@ clearTimeout(timeoutId); } timeoutId = setTimeout(() => { - contentItems.get($currentPath, this.value); + contentItems.get({ path: $currentPath, searchTerm: this.value }); }, 300); } + function initIntersectionObserver() { + for (const el of document.querySelectorAll(".load-more")) { + const observer = new IntersectionObserver(loadMore); + observer.observe(el); + } + } + $: { - contentItems.get($currentPath, null, true); + contentItems.get({ path: $currentPath }); } $: { $contentItems; scrollToRight(); + initIntersectionObserver(); } - @@ -155,14 +166,14 @@ > {#if $config.uploadEnabled} - + {/if} {/if} {#if i > 0 && level.selectable} @@ -202,21 +215,27 @@ {/if}
{#if !level.gridView} - + {:else} - + {/if}
- {#each (level.items || []) as item, n} + {#each level.items || [] as item, n}
- {#if item.getIcon} - {item.Title} - {:else} - - {/if} - {item.Title} -
+
+ {#if item.getIcon} + {item.Title} + {:else} + + {/if} + {item.Title} +
{:else} -
- - {item.Title} -
+
+ + {item.Title} +
{/if} {#if item.is_folderish} {/each} - {#if level.items_total == 0} -
-

no items found.

-
+ {#if level.batching} +
+
+ Loading... +
+
+ {/if} + {#if level.items.length == 0} +
+

no items found.

+
{/if} {/each} @@ -275,8 +309,11 @@ + selectItem(previewItem)} + >select "{previewItem.getPath + .split("/") + .pop()}"
@@ -344,7 +381,7 @@ display: flex; flex-wrap: nowrap; width: 100%; - overflow:hidden; + overflow: hidden; flex-grow: 3; border-left: var(--bs-border-style) var(--bs-border-color) var(--bs-border-width); } @@ -408,9 +445,9 @@ } .contentItem .grid-preview > img { - width:95px; - height:95px; - object-fit:cover; + width: 95px; + height: 95px; + object-fit: cover; float: left; margin-right: 1rem; } @@ -440,4 +477,9 @@ width: 590px; overflow-x: auto; } + + .load-more { + text-align: center; + min-height: 1rem; + } diff --git a/src/pat/contentbrowser/src/ContentStore.js b/src/pat/contentbrowser/src/ContentStore.js index 7de443c66..5a01b365f 100644 --- a/src/pat/contentbrowser/src/ContentStore.js +++ b/src/pat/contentbrowser/src/ContentStore.js @@ -4,7 +4,13 @@ import { request } from "./api.js"; export default function (config, pathCache) { const store = writable([]); - store.get = async (path, searchTerm, updateCache) => { + store.loading = false; + + store.get = async ({ + path = "", + searchTerm = "", + updateCache = false, + }) => { const base_url = new URL(config.base_url); const portalPath = base_url.pathname; path = path.replace(new RegExp(`^${portalPath}`), ""); @@ -20,7 +26,7 @@ export default function (config, pathCache) { while (partsToShow.length > 0) { let sub_path = partsToShow.join("/").replace(/^\//, ""); const poped = partsToShow.pop(); - sub_path = pathPrefix + ((poped != "") ? `/${sub_path}`: ""); + sub_path = pathPrefix + ((poped != "") ? `/${sub_path}` : ""); if (paths.indexOf(sub_path) === -1) paths.push(sub_path); } @@ -34,22 +40,23 @@ export default function (config, pathCache) { let level = {}; const c = get(pathCache); if (Object.keys(c).indexOf(p) === -1 || skipCache) { + console.log(`uncached lookup of ${p} (${isFirstPath}, ${searchTerm}, ${updateCache})`); let query = { base_url: config.base_url, path: p, }; - if(isFirstPath && searchTerm){ + if (isFirstPath && searchTerm) { query["searchTerm"] = "*" + searchTerm + "*"; } - if(config.selectableTypes.length) { + if (config.selectableTypes.length) { query["selectableTypes"] = config.selectableTypes; } level = await request(query); // do not update cache when searching - if(!searchTerm) { + if (!searchTerm) { const levelInfo = await request({ base_url: config.base_url, levelInfoPath: p, @@ -68,6 +75,7 @@ export default function (config, pathCache) { }); } } else { + console.log(`get path ${p} from cache`); level = c[p]; } levels = [level, ...levels]; @@ -75,5 +83,52 @@ export default function (config, pathCache) { store.set(levels); }; + store.loadMore = async (path, current_path) => { + store.loading = true; + + const c = get(pathCache); + if (Object.keys(c).indexOf(path) === -1) { + console.log(`path not found in cache ${path}`); + return; + }; + const level = c[path]; + + if (!level.batching) { + console.log("nothing to load"); + } + + const url = level.batching.next; + + const response = await fetch(url, { + headers: { + "Accept": "application/json" + } + }); + const json = await response.json(); + + if (!response.ok) { + console.log(`could not load url ${url}`); + return; + } + + console.log(`loading ${url} for ${path}`); + + const batch_url = new URL(url); + const b_start = parseInt(batch_url.searchParams.get("b_start")); + + for (const [idx, item] of Object.entries(json.items)) { + level.items[parseInt(idx) + b_start] = item; + } + level.batching = json.batching; + + pathCache.update((n) => { + n[path] = level; + return n; + }); + + // use store.get to update levels + store.get({path: current_path}); + }; + return store; } diff --git a/src/pat/contentbrowser/src/api.js b/src/pat/contentbrowser/src/api.js index a8efa53de..6bc884415 100644 --- a/src/pat/contentbrowser/src/api.js +++ b/src/pat/contentbrowser/src/api.js @@ -31,7 +31,6 @@ export async function request({ query.set("metadata_fields", "_all"); const url = `${base_url}/@search?${query.toString()}`; - console.log(url); let headers = new Headers(); headers.set("Accept", "application/json"); @@ -51,6 +50,9 @@ export async function request({ items: [], items_total: json.items_total, } + if(json.batching) { + filtered_response.batching = json.batching; + } for (const it of json.items) { if (selectableTypes.indexOf(it.portal_type) != -1 || it.is_folderish) { filtered_response.items.push(it); diff --git a/webpack.config.js b/webpack.config.js index 7a6dd3278..7555942f7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -116,7 +116,7 @@ module.exports = () => { config.resolve.extensions = [".js", ".json", ".wasm", ".svelte"]; config.resolve.mainFields = ["browser", "module", "main", "svelte"]; // config.resolve.conditionNames = ["svelte", "browser", "import"]; - //config.resolve.conditionNames = ["svelte", "module", "browser"]; + // config.resolve.conditionNames = ["svelte", "module", "browser", "import"]; config.plugins.push( mf_config({ diff --git a/yarn.lock b/yarn.lock index 8dfc6690c..40bf596f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9577,7 +9577,7 @@ string-length@^5.0.1: char-regex "^2.0.0" strip-ansi "^7.0.1" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9595,6 +9595,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -9663,7 +9672,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -9677,6 +9686,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -10620,7 +10636,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -10647,6 +10663,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"