diff --git a/integ_tests/test_sharing.py b/integ_tests/test_sharing.py
index ecc667c9..6138514f 100644
--- a/integ_tests/test_sharing.py
+++ b/integ_tests/test_sharing.py
@@ -297,7 +297,8 @@ def test_share_by_map_validation(page: Page, radicale_server: str) -> None:
).to_have_count(1)
-def test_incoming_shares(page: Page, radicale_server: str) -> None:
+@pytest.mark.parametrize("permissions", ["ro", "rw"])
+def test_incoming_shares(page: Page, radicale_server: str, permissions: str) -> None:
# 1. Admin logs in and creates a map share for 'max'
login(page, radicale_server)
create_collection(page, radicale_server)
@@ -307,6 +308,8 @@ def test_incoming_shares(page: Page, radicale_server: str) -> None:
page.click('button[data-name="sharebymap"]')
page.locator('input[data-name="shareuser"]').fill("max")
page.locator('input[data-name="sharehref"]').fill("mapped")
+ if permissions == "rw":
+ page.check("#newshare_attr_permissions_rw")
page.click('#newshare button[data-name="submit"]')
expect(
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)")
@@ -372,12 +375,31 @@ def test_incoming_shares(page: Page, radicale_server: str) -> None:
)
).to_be_checked()
- # 6. Assert no error was shown
- expect(page.locator('#incomingsharingscene span[data-name="error"]')).to_be_hidden()
-
+ # 6. Verify "shared by admin" and button visibility in the collection article
page.click('#incomingsharingscene button[data-name="cancel"]')
expect(page.locator("#incomingsharingscene")).to_be_hidden()
+ article = page.locator("article:not(.hidden)").first
+ expect(article.locator('[data-name="shared-by"]')).to_be_visible()
+ expect(article.locator('[data-name="shared-by-owner"]')).to_have_text("admin")
+
+ # Action buttons are only visible on mouseover
+ article.hover()
+
+ # Share and delete buttons should be hidden for all incoming shares
+ expect(article.locator('a[data-name="share"]')).to_be_hidden()
+ expect(article.locator('[data-name="shareoption"]')).to_be_hidden()
+ expect(article.locator('a[data-name="delete"]')).to_be_hidden()
+
+ # Edit button depends on permissions
+ if permissions == "rw":
+ expect(article.locator('a[data-name="edit"]')).to_be_visible()
+ else:
+ expect(article.locator('a[data-name="edit"]')).to_be_hidden()
+
+ # 7. Assert no error was shown
+ expect(page.locator('#incomingsharingscene span[data-name="error"]')).to_be_hidden()
+
def test_no_incoming_shares_message(page: Page, radicale_server: str) -> None:
# 1. Max logs in
diff --git a/radicale/web/internal_data/index.html b/radicale/web/internal_data/index.html
index 02d32749..a21866b0 100644
--- a/radicale/web/internal_data/index.html
+++ b/radicale/web/internal_data/index.html
@@ -74,6 +74,7 @@
Title
+ 👥 shared by >
Address book
Calendar, journal and tasks
Calendar and journal
diff --git a/radicale/web/internal_data/js/api/sharing.js b/radicale/web/internal_data/js/api/sharing.js
index f9385b51..d145c3e1 100644
--- a/radicale/web/internal_data/js/api/sharing.js
+++ b/radicale/web/internal_data/js/api/sharing.js
@@ -34,9 +34,6 @@ import { CollectionType } from "../models/collection.js";
* @property {SharingFeatures} [sharing]
*/
-/** @type {ServerFeatures} */
-export let server_features = {};
-
/**
* @param {string} user
* @param {string} password
@@ -95,7 +92,7 @@ function call_sharing_api(
/**
* @param {string} user
* @param {string} password
- * @param {function():void} callback
+ * @param {function(import("../api/sharing.js").ServerFeatures, ?string):void} callback
*/
export function discover_server_features(user, password, callback) {
call_sharing_api(
@@ -104,16 +101,19 @@ export function discover_server_features(user, password, callback) {
"all/info",
{},
function (response) {
- server_features["sharing"] = JSON.parse(response);
- callback();
+ try {
+ let features = { "sharing": JSON.parse(response) };
+ callback(features, null);
+ } catch (e) {
+ callback({}, e.message);
+ }
},
function () {
// sharing is disabled on the server
- server_features["sharing"] = {};
- callback();
+ callback({ "sharing": {} }, null);
},
function (error) {
- console.error("Failed to discover sharing features: " + error);
+ callback({}, error);
},
);
}
@@ -147,7 +147,7 @@ export class Share {
*/
export function reload_sharing_list(user, password, collection, callback) {
let body = collection ? { PathMapped: collection.href } : {};
- call_sharing_api(
+ return call_sharing_api(
user,
password,
"all/list",
@@ -160,7 +160,7 @@ export function reload_sharing_list(user, password, collection, callback) {
null, // on_not_found
function (error) {
callback([], error);
- }
+ },
);
}
diff --git a/radicale/web/internal_data/js/scenes/CollectionsScene.js b/radicale/web/internal_data/js/scenes/CollectionsScene.js
index 1c850acd..a8467449 100644
--- a/radicale/web/internal_data/js/scenes/CollectionsScene.js
+++ b/radicale/web/internal_data/js/scenes/CollectionsScene.js
@@ -19,17 +19,14 @@
* along with this program. If not, see .
*/
-import { get_collections } from "../api/api.js";
-import { discover_server_features } from "../api/sharing.js";
-
import { SERVER } from "../constants.js";
import { Collection, CollectionType } from "../models/collection.js";
+import { collectionsCache } from "../utils/collections_cache.js";
import { bytesToHumanReadable } from "../utils/misc.js";
import { CreateEditCollectionScene } from "./CreateEditCollectionScene.js";
import { DeleteCollectionScene } from "./DeleteCollectionScene.js";
import { IncomingSharingScene } from "./IncomingSharingScene.js";
-import { LoadingScene } from "./LoadingScene.js";
-import { Scene, is_current_scene, pop_scene, push_scene } from "./scene_manager.js";
+import { Scene, push_scene } from "./scene_manager.js";
import { ShareCollectionScene, maybe_enable_sharing_options } from "./ShareCollectionScene.js";
import { UploadCollectionScene } from "./UploadCollectionScene.js";
@@ -51,8 +48,6 @@ export class CollectionsScene {
/** @type {HTMLElement} */ let upload_btn = html_scene.querySelector("[data-name=upload]");
/** @type {HTMLElement} */ let incomingshares_btn = html_scene.querySelector("[data-name=incomingshares]");
- /** @type {?XMLHttpRequest} */ let collections_req = null;
- /** @type {?Array} */ let child_collections = null;
/** @type {Array} */ let nodes = [];
function onnew() {
@@ -126,12 +121,20 @@ export class CollectionsScene {
/**
* @param {any[]} collections
+ * @param {import("../api/sharing.js").Share[]} shares
*/
- function show_collections(collections) {
+ function show_collections(collections, shares) {
/** @type {HTMLElement} */ let navBar = document.querySelector("#logoutview");
let heightOfNavBar = navBar.offsetHeight + "px";
html_scene.style.marginTop = heightOfNavBar;
html_scene.style.height = "calc(100vh - " + heightOfNavBar + ")";
+
+ // Clear old nodes
+ nodes.forEach(function (node) {
+ node.parentNode.removeChild(node);
+ });
+ nodes = [];
+
collections.forEach(function (/** @type {Collection} */ collection) {
/** @type {HTMLElement} */ let node = /** @type {HTMLElement} */(template.cloneNode(true));
node.classList.remove("hidden");
@@ -162,6 +165,23 @@ export class CollectionsScene {
node.querySelector("[data-name=" + e + "]").classList.add("hidden");
}
});
+ let share_info = node.querySelector("[data-name=shared-by]");
+ let share = (shares || []).find(s => s.ShareType === "map" && (s.PathOrToken || "").replace(/\/+$/, "") === (collection.href || "").replace(/\/+$/, ""));
+ if (share && share.Owner !== user) {
+ share_info.classList.remove("hidden");
+ node.querySelector("[data-name=shared-by-owner]").textContent = share.Owner;
+ let share_option = node.querySelector("[data-name=shareoption]");
+ if (share_option) {
+ share_option.classList.add("hidden");
+ share_option.removeAttribute("data-name");
+ }
+ delete_btn.classList.add("hidden");
+ if (!/w/i.test(share.Permissions || "")) {
+ edit_btn.classList.add("hidden");
+ } else {
+ edit_btn.classList.remove("hidden");
+ }
+ }
title_form.textContent = collection.displayname || collection.href;
if (title_form.textContent.length > 30) {
title_form.classList.add("smalltext");
@@ -192,43 +212,20 @@ export class CollectionsScene {
});
}
- function update() {
- let loading_scene = new LoadingScene();
- push_scene(loading_scene);
- collections_req = get_collections(user, password, principal_collection, function (child_collections_, error) {
- if (!is_current_scene(loading_scene)) {
- return;
- }
- collections_req = null;
- if (error) {
- onerror(error);
- pop_scene();
- } else {
- child_collections = child_collections_;
- pop_scene();
- }
- });
- }
this.show = function () {
html_scene.classList.remove("hidden");
new_btn.onclick = onnew;
upload_btn.onclick = onupload;
incomingshares_btn.onclick = onincomingshares;
- if (child_collections === null) {
- update();
- discover_server_features(user, password, maybe_enable_sharing_options);
- } else {
- // from update loading scene
- show_collections(child_collections);
- }
+ collectionsCache.getChildCollections(user, password, principal_collection, onerror, show_collections);
+ collectionsCache.getServerFeatures(user, password, null, maybe_enable_sharing_options);
};
this.hide = function () {
html_scene.classList.add("hidden");
new_btn.onclick = null;
upload_btn.onclick = null;
incomingshares_btn.onclick = null;
- child_collections = null;
// remove collection
nodes.forEach(function (node) {
node.parentNode.removeChild(node);
@@ -236,11 +233,6 @@ export class CollectionsScene {
nodes = [];
};
this.release = function () {
- if (collections_req !== null) {
- collections_req.abort();
- collections_req = null;
- }
- child_collections = null;
};
}
}
\ No newline at end of file
diff --git a/radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js b/radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js
index 21a3cdf4..2d47a1b6 100644
--- a/radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js
+++ b/radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js
@@ -22,6 +22,7 @@
import { create_collection, edit_collection } from "../api/api.js";
import { COLOR_RE } from "../constants.js";
import { Collection, CollectionType } from "../models/collection.js";
+import { collectionsCache } from "../utils/collections_cache.js";
import { ErrorHandler } from "../utils/error.js";
import { FormValidator, validate_color, validate_href } from "../utils/form_validator.js";
import { cleanHREFinput, onCleanHREFinput, random_hex, random_uuid } from "../utils/misc.js";
@@ -140,6 +141,7 @@ export class CreateEditCollectionScene {
errorHandler.setError(error1);
pop_scene();
} else {
+ collectionsCache.invalidate();
pop_to_parent();
}
};
diff --git a/radicale/web/internal_data/js/scenes/DeleteCollectionScene.js b/radicale/web/internal_data/js/scenes/DeleteCollectionScene.js
index 16546bbc..67e34ce6 100644
--- a/radicale/web/internal_data/js/scenes/DeleteCollectionScene.js
+++ b/radicale/web/internal_data/js/scenes/DeleteCollectionScene.js
@@ -22,6 +22,7 @@
import { delete_collection } from "../api/api.js";
import { DELETE_CONFIRMATION_TEXT } from "../constants.js";
import { Collection } from "../models/collection.js";
+import { collectionsCache } from "../utils/collections_cache.js";
import { ErrorHandler } from "../utils/error.js";
import { FormValidator, validate_equals } from "../utils/form_validator.js";
import { LoadingScene } from "./LoadingScene.js";
@@ -72,6 +73,7 @@ export class DeleteCollectionScene {
errorHandler.setError(error1);
pop_scene();
} else {
+ collectionsCache.invalidate();
pop_to_parent();
}
});
diff --git a/radicale/web/internal_data/js/scenes/IncomingSharingScene.js b/radicale/web/internal_data/js/scenes/IncomingSharingScene.js
index f1f30493..1cc1ac2c 100644
--- a/radicale/web/internal_data/js/scenes/IncomingSharingScene.js
+++ b/radicale/web/internal_data/js/scenes/IncomingSharingScene.js
@@ -19,11 +19,11 @@
* along with this program. If not, see .
*/
-import { Share, reload_sharing_list, update_incoming_share } from "../api/sharing.js";
+import { update_incoming_share } from "../api/sharing.js";
+import { collectionsCache } from "../utils/collections_cache.js";
import { ErrorHandler } from "../utils/error.js";
import { displayPermissions } from "../utils/permissions.js";
-import { LoadingScene } from "./LoadingScene.js";
-import { Scene, is_current_scene, pop_scene, push_scene } from "./scene_manager.js";
+import { Scene, pop_scene } from "./scene_manager.js";
/**
* @implements {Scene}
@@ -45,14 +45,13 @@ export class IncomingSharingScene {
let error_handler = new ErrorHandler(error_element);
/** @type {Array} */ let nodes = [];
- /** @type {?Array