Skip to content

Commit

Permalink
feat(pat contentbrowser): Implement rootPath option to restrict acces…
Browse files Browse the repository at this point in the history
…s to a subfolder of a site structure
  • Loading branch information
petschki committed Oct 7, 2024
1 parent 87272a8 commit debebdd
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 70 deletions.
24 changes: 13 additions & 11 deletions src/pat/contentbrowser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@ Show a widget to select items in an offcanvas miller-column browser.

## Configuration

| Option | Type | Default | Description |
| :------------------------: | :-----: | :-------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| attributes | array | ['UID', 'Title', 'portal_type', 'path'] | This list is passed to the server during an AJAX request to specify the attributes which should be included on each item. |
| basePath | string | set to rootPath. | Start browse/search in this path. |
| contextPath | string | | Path of the object, which is currently edited. If this path is given, this object will not be selectable. |
| favorites | array | [] | Array of objects. These are favorites, which can be used to quickly jump to different locations. Objects have the attributes "title" and "path". |
| maximumSelectionSize | integer | -1 | The maximum number of items that can be selected in a multi-select control. If this number is less than 1 selection is not limited. |
| bSize | integer | 10 | Batch size to break down big result sets into multiple pages. |
| separator | string | ',' | Select2 option. String which separates multiple items. |
| upload | boolean | | Allow file and image uploads from within the related items widget. |
| vocabularyUrl | string | null | This is a URL to a JSON-formatted file used to populate the list |
| Option | Type | Default | Description |
| :------------------------: | :-----: | :-------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------: |
| attributes | array | ['UID', 'Title', 'portal_type', 'path'] | This list is passed to the server during an AJAX request to specify the attributes which should be included on each item. |
| rootPath | string | "/" | Browsing/searching root path. You will not get beneath this path |
| basePath | string | set to rootPath. | Start browse/search in this path. |
| contextPath | string | | Path of the object, which is currently edited. If this path is given, this object will not be selectable. |
| favorites | array | [] | Array of objects. These are favorites, which can be used to quickly jump to different locations. Objects have the attributes "title" and "path". |
| maximumSelectionSize | integer | -1 | The maximum number of items that can be selected in a multi-select control. If this number is less than 1 selection is not limited. |
| mode | string | "browse" | The maximum number of items that can be selected in a multi-select control. If this number is less than 1 selection is not limited. |
| bSize | integer | 10 | Toggle between "browse" and "search" |
| separator | string | ',' | Select2 option. String which separates multiple items. |
| upload | boolean | | Allow file and image uploads from within the related items widget. |
| vocabularyUrl | string | null | This is a URL to a JSON-formatted file used to populate the list |


## Default
Expand Down
7 changes: 5 additions & 2 deletions src/pat/contentbrowser/contentbrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ parser.addArgument(
], null, true
);
parser.addArgument("width");
parser.addArgument("mode");
parser.addArgument("max-depth");
parser.addArgument("root-path");
parser.addArgument("root-url");
parser.addArgument("base-path");
parser.addArgument("context-path");
parser.addArgument("maximum-selection-size");
Expand All @@ -34,7 +37,7 @@ parser.addArgument("selection-template");
parser.addArgument("favorites");
parser.addArgument("recently-used");
parser.addArgument("recently-used-key");
parser.addArgument("recently-used-max-items", 20);
parser.addArgument("recently-used-max-items");
parser.addArgument("b-size");

class Pattern extends BasePattern {
Expand All @@ -43,7 +46,7 @@ class Pattern extends BasePattern {
static parser = parser;

async init() {
this.el.setAttribute('style', 'display: none');
this.el.style.display = "none";

// ensure an id on our element (TinyMCE doesn't have one)
let nodeId = this.el.getAttribute("id");
Expand Down
11 changes: 6 additions & 5 deletions src/pat/contentbrowser/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
export let contextPath;
export let vocabularyUrl;
export let mode = "browse";
export let rootPath = "";
export let rootUrl = "";
export let basePath = "";
export let selectableTypes = [];
export let maximumSelectionSize = -1;
Expand Down Expand Up @@ -48,12 +50,10 @@
const currentPath = getContext("currentPath");
if (!$currentPath) {
$currentPath = basePath || "/";
// if root path is not above base path we start at rootPath
$currentPath = basePath.indexOf(rootPath) != 0 ? rootPath : basePath;
}
// base_url information
const base_url = document.body.getAttribute("data-portal-url");
let config = getContext("config");
$config = {
mode: mode,
Expand All @@ -62,6 +62,8 @@
vocabularyUrl: vocabularyUrl,
width: width,
maxDepth: maxDepth,
rootPath: rootPath,
rootUrl: rootUrl,
basePath: basePath,
selectableTypes: selectableTypes,
maximumSelectionSize: maximumSelectionSize,
Expand All @@ -74,7 +76,6 @@
recentlyUsed: recentlyUsed,
recentlyUsedKey: recentlyUsedKey,
recentlyUsedMaxItems: recentlyUsedMaxItems,
base_url: base_url,
pageSize: bSize,
};
Expand Down
21 changes: 11 additions & 10 deletions src/pat/contentbrowser/src/ContentBrowser.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
const uploadEl = document.querySelector(".upload-wrapper");
uploadEl.classList.add("pat-upload");
const patUpload = new Upload(uploadEl, {
baseUrl: $config.base_url,
baseUrl: $config.rootUrl,
currentPath: $currentPath,
relativePath: "@@fileUpload",
allowPathSelection: false,
Expand All @@ -92,21 +92,22 @@
} else {
const pathParts = item.path.split("/");
const folderPath = pathParts.slice(0, pathParts.length - 1).join("/");
currentPath.set(folderPath || "/");
currentPath.set(folderPath || $config.rootPath);
updatePreview({ data: item });
}
scrollToRight();
}
function changePath(item, e) {
// always hide upload when changing path
showUpload = false;
// clear previous selection
updatePreview({ action: "clear" });
if (item === "/") {
if (item === "/" || item === $config.rootPath) {
// clicked "home" button
currentPath.set(item);
currentPath.set($config.rootPath);
return;
}
Expand Down Expand Up @@ -282,8 +283,8 @@
}
const item = response.results[0];
if (!item.path) {
// fix for Plone Site
item.path = "/";
// fix for root
item.path = $config.rootPath;
}
changePath(item);
}
Expand Down Expand Up @@ -413,13 +414,13 @@
in:fly|local={{ duration: 300 }}
>
<div class="levelToolbar">
{#if i == 0}
{#if i == 0 && $config.mode == "browse"}
<button
type="button"
class="btn btn-link btn-xs ps-0"
tabindex="0"
on:keydown={() => changePath("/")}
on:click={() => changePath("/")}
on:keydown={() => changePath($config.rootPath)}
on:click={() => changePath($config.rootPath)}
><svg
use:resolveIcon={{ iconName: "house" }}
/></button
Expand All @@ -432,7 +433,7 @@
on:click|preventDefault={() => addItem(level)}
>
{_t("select ${level_path}", {
level_path: level.absPath || "/",
level_path: level.displayPath,
})}
</button>
{/if}
Expand Down
67 changes: 50 additions & 17 deletions src/pat/contentbrowser/src/ContentStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,56 @@ export default function (config, pathCache) {
return await request(query);
}

const browse = async (portalPath, path, searchTerm, updateCache) => {
const browse = async (path, searchTerm, updateCache) => {

let rootPath = config.rootPath;
let rootPathParts = rootPath.replace(/^\/+/, '').split("/");
let physicalPath = path;
let hideRootPath = rootPath;

if (!physicalPath.startsWith(rootPath)) {
// The path from the returned items from "vocabularyUrl" are starting
// relative from the Plone Site. So we need to generate the phyiscalPath here.
if (rootPathParts.length === 1) {
physicalPath = rootPath + physicalPath;
} else {
// We also have merge the rootPath and the clicked path correctly for example:
// rootPath: /Plone/media, clicked path: /media/subfolder
// has to become:
// /Plone/media/subfolder
let pathParts = physicalPath.replace(/^\/+/, '').split("/");
let overlapIdx = rootPathParts.length;
for (let idx = 0; idx < rootPathParts.length; idx++) {
if (rootPathParts[idx] === pathParts[0]) {
overlapIdx = idx;
break;
}
}
hideRootPath = "/" + (rootPathParts.filter(it => pathParts.includes(it))).join("/");
physicalPath = "/" + (rootPathParts.slice(1, overlapIdx).concat(pathParts)).join("/");
}
}

let paths = [];
let parts = path.split("/") || [];
const depth = parts.length >= config.maxDepth ? config.maxDepth : parts.length;
let parts = physicalPath.split("/") || [];
const maxDepth = Math.min(parts.length, config.maxDepth || 999);

let partsToShow = parts.slice(parts.length - depth, parts.length);
let partsToHide = parts.slice(0, parts.length - depth);
const pathPrefix = portalPath + partsToHide.join("/");
const pC = get(pathCache);
let partsToShow = parts.slice(parts.length - maxDepth, parts.length);
let partsToHide = parts.slice(0, parts.length - maxDepth);
const pathPrefix = partsToHide.join("/");

while (partsToShow.length > 0) {
let sub_path = partsToShow.join("/").replace(/^\//, "");
const poped = partsToShow.pop();
sub_path = pathPrefix + ((poped != "") ? `/${sub_path}` : "");
if (paths.indexOf(sub_path) === -1) paths.push(sub_path);
if (sub_path && paths.indexOf(sub_path) === -1) paths.push(sub_path);
if (sub_path == rootPath) {
// respect rootPath
break;
}
}

const pC = get(pathCache);
let levels = [];
let pathCounter = 0;

Expand Down Expand Up @@ -65,7 +98,7 @@ export default function (config, pathCache) {
level.searchTerm = searchTerm;
level.page = 1;
level.path = p;
level.absPath = p.replace(portalPath, "");
level.displayPath = p.replace(new RegExp(`^(${hideRootPath}|${rootPath})`), "") || "/"

// do not update cache when searching
if (!searchTerm) {
Expand Down Expand Up @@ -94,12 +127,16 @@ export default function (config, pathCache) {
store.set(levels);
}

const search = async (portalPath, searchTerm, page) => {
const search = async (searchTerm, page) => {
let query = {
searchPath: portalPath,
searchPath: config.rootPath,
page: page,
};
if (searchTerm) {
if (searchTerm.length < 3) {
// minimum length of search term
return;
}
query["searchTerm"] = "*" + searchTerm + "*";
}
let level = await load(query);
Expand Down Expand Up @@ -167,11 +204,8 @@ export default function (config, pathCache) {
loadMorePath = "",
page = 1,
}) => {
const base_url = new URL(config.base_url);
const portalPath = base_url.pathname;

if (config.mode === "search") {
await search(portalPath, searchTerm, page)
await search(searchTerm, page)
} else if (loadMorePath) {
const pC = get(pathCache);
if (!(loadMorePath in pC)) {
Expand All @@ -182,8 +216,7 @@ export default function (config, pathCache) {
await nextBatch(loadMorePath, page, level.searchTerm);
}
} else if (path) {
path = path.replace(new RegExp(`^${portalPath}`), "");
await browse(portalPath, path, searchTerm, updateCache);
await browse(path, searchTerm, updateCache);
}

};
Expand Down
1 change: 0 additions & 1 deletion src/pat/contentbrowser/src/RecentlyUsed.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
class="dropdown-item"
>
<svg
on:error={console.log(recentlyUsed)}
use:resolveIcon={{
iconName: `contenttype/${recentlyUsed?.portal_type.toLowerCase().replace(/\.| /g, "-")}`,
}}
Expand Down
5 changes: 2 additions & 3 deletions src/pat/contentbrowser/src/SelectedItems.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,8 @@
return;
}
const selectedItemsUids = await get_items_from_uids(initialValue, $config);
$selectedItems = selectedItemsUids;
selectedUids.update(() => selectedItemsUids.map((x) => x.UID));
$selectedItems = await get_items_from_uids(initialValue, $config);
selectedUids.update(() => $selectedItems.map((x) => x.UID));
}
function initializeSorting() {
Expand Down
Loading

0 comments on commit debebdd

Please sign in to comment.