diff --git a/integ_tests/common.py b/integ_tests/common.py index 53b8ae38..8a7b503b 100644 --- a/integ_tests/common.py +++ b/integ_tests/common.py @@ -59,6 +59,7 @@ collection_by_map = true collection_by_token = true permit_create_token = true permit_create_map = true +permit_properties_overlay = true """ ) diff --git a/integ_tests/test_sharing.py b/integ_tests/test_sharing.py index fa87a491..ec3b6398 100644 --- a/integ_tests/test_sharing.py +++ b/integ_tests/test_sharing.py @@ -112,3 +112,61 @@ def test_create_and_delete_share_by_map(page: Page, radicale_server: str) -> Non expect( page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)") ).to_have_count(0) + + +def test_share_with_property_overrides(page: Page, radicale_server: str) -> None: + login(page, radicale_server) + # Create a collection with specific details + page.click('a[data-name="new"]') + page.locator('#createcollectionscene input[data-name="displayname"]').fill("Test Collection") + page.locator('#createcollectionscene input[data-name="description"]').fill("Original Description") + page.locator('#createcollectionscene input[data-name="color"]').fill("#ff0000") + page.click('#createcollectionscene button[data-name="submit"]') + + page.hover("article:not(.hidden)") + page.click('article:not(.hidden) a[data-name="share"]', force=True, strict=True) + page.click('button[data-name="sharebytoken"]') + + # Verify defaults + expect(page.locator('input[data-name="description_override"]')).to_have_value("Original Description") + expect(page.locator('input[data-name="color_override"]')).to_have_value("#ff0000") + expect(page.locator('input[data-name="description_override"]')).to_be_disabled() + expect(page.locator('input[data-name="color_override"]')).to_be_disabled() + + # Set overrides + page.click('label[for="newshare_attr_description_enabled"]') + page.locator('input[data-name="description_override"]').fill("Overridden Description") + page.click('label[for="newshare_attr_color_enabled"]') + page.locator('input[data-name="color_override"]').fill("#00ff00") + + page.click('#newshare button[data-name="submit"]') + + # Verify the share was created + expect( + page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)") + ).to_have_count(1) + + +def test_share_journal_no_overrides(page: Page, radicale_server: str) -> None: + login(page, radicale_server) + # Create a collection of type JOURNAL + page.click('a[data-name="new"]') + page.locator('#createcollectionscene select[data-name="type"]').select_option("JOURNAL") + page.locator('#createcollectionscene input[data-name="displayname"]').fill("Test Journal") + page.locator('#createcollectionscene input[data-name="description"]').fill("Journal Description") + page.click('#createcollectionscene button[data-name="submit"]') + + page.hover("article:not(.hidden)") + page.click('article:not(.hidden) a[data-name="share"]', force=True, strict=True) + page.click('button[data-name="sharebytoken"]') + + # Verify property override fieldset is hidden + expect(page.locator('fieldset[data-name="properties_override"]')).to_be_hidden() + + # Create the share + page.click('#newshare button[data-name="submit"]') + + # Verify the share was created + expect( + page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)") + ).to_have_count(1) diff --git a/radicale/web/internal_data/index.html b/radicale/web/internal_data/index.html index 3afc37de..119fe783 100644 --- a/radicale/web/internal_data/index.html +++ b/radicale/web/internal_data/index.html @@ -227,9 +227,18 @@ -
+
Properties override - +
+ + + +
+
+ + + +
diff --git a/radicale/web/internal_data/js/api/api.js b/radicale/web/internal_data/js/api/api.js index e493eedd..fa6f2035 100644 --- a/radicale/web/internal_data/js/api/api.js +++ b/radicale/web/internal_data/js/api/api.js @@ -483,6 +483,36 @@ export function reload_sharing_list(user, password, collection, callback) { ); } + +/** + * Property keys for different collection types. + * Map to OVERLAY_PROPERTIES_WHITELIST in radicale/sharing/__init__.py + */ +export const OVERLAY_PROPERTIES = { + CALENDAR: { + DESCRIPTION: "C:calendar-description", + COLOR: "ICAL:calendar-color", + }, + ADDRESSBOOK: { + DESCRIPTION: "CR:addressbook-description", + COLOR: "INF:addressbook-color", + } +}; + +/** + * Returns the correct internal property key for a given collection type and property name. + * @param {string} type Collection type (ADDRESSBOOK, CALENDAR, etc.) + * @param {"DESCRIPTION" | "COLOR"} property Property name + * @returns {string | null} Internal property key or null if not supported + */ +export function get_property_key(type, property) { + if (type === CollectionType.ADDRESSBOOK) { + return OVERLAY_PROPERTIES.ADDRESSBOOK[property]; + } else if (CollectionType.is_subset(CollectionType.CALENDAR, type)) { + return OVERLAY_PROPERTIES.CALENDAR[property]; + } + return null; +} /** * @param {string} user * @param {string} password @@ -490,7 +520,7 @@ export function reload_sharing_list(user, password, collection, callback) { * @param {string} permissions * @param {boolean} enabled * @param {boolean} hidden - * @param {string} properties + * @param {Object} properties * @param {function(?string):void} callback */ export function add_share_by_token( @@ -536,7 +566,7 @@ export function add_share_by_token( * @param {string} permissions * @param {boolean} enabled * @param {boolean} hidden - * @param {string} properties + * @param {Object} properties * @param {string} share_user * @param {string} href * @param {function(?string):void} callback diff --git a/radicale/web/internal_data/js/scenes/NewShareScene.js b/radicale/web/internal_data/js/scenes/NewShareScene.js index 9f9aedaa..4d9e746f 100644 --- a/radicale/web/internal_data/js/scenes/NewShareScene.js +++ b/radicale/web/internal_data/js/scenes/NewShareScene.js @@ -19,7 +19,8 @@ * along with this program. If not, see . */ -import { add_share_by_map, add_share_by_token } from "../api/api.js"; +import { add_share_by_map, add_share_by_token, get_property_key } from "../api/api.js"; +import { CollectionType } from "../models/collection.js"; import { ErrorHandler } from "../utils/error.js"; import { FormValidator, validate_href, validate_non_empty } from "../utils/form_validator.js"; import { onCleanHREFinput, random_uuid } from "../utils/misc.js"; @@ -32,11 +33,12 @@ export class NewShareScene { /** * @param {string} user * @param {string} password - * @param {string} pathMapped + * @param {import("../models/collection.js").Collection} collection * @param {string} shareType * @param {function():void} onclose */ - constructor(user, password, pathMapped, shareType, onclose) { + constructor(user, password, collection, shareType, onclose) { + let pathMapped = collection.href; /** @type {HTMLElement} */ let html_scene = document.getElementById("newshare"); /** @type {HTMLFormElement} */ let form = html_scene.querySelector("form"); /** @type {HTMLElement} */ let sharemapfields = html_scene.querySelector("[data-name=sharemapfields]"); @@ -46,7 +48,13 @@ export class NewShareScene { /** @type {HTMLInputElement} */ let hidden_checkbox = html_scene.querySelector("[data-name=hidden]"); let permissions_ro_radio = /** @type {HTMLInputElement} */ (document.getElementById("newshare_attr_permissions_ro")); let permissions_rw_radio = /** @type {HTMLInputElement} */ (document.getElementById("newshare_attr_permissions_rw")); - /** @type {HTMLInputElement} */ let properties_input = html_scene.querySelector("[data-name=properties]"); + + /** @type {HTMLElement} */ let properties_fieldset = html_scene.querySelector("[data-name=properties_override]"); + /** @type {HTMLInputElement} */ let description_override_enabled = html_scene.querySelector("[data-name=description_override_enabled]"); + /** @type {HTMLInputElement} */ let description_override_input = html_scene.querySelector("[data-name=description_override]"); + /** @type {HTMLInputElement} */ let color_override_enabled = html_scene.querySelector("[data-name=color_override_enabled]"); + /** @type {HTMLInputElement} */ let color_override_input = html_scene.querySelector("[data-name=color_override]"); + /** @type {HTMLElement} */ let error_form = html_scene.querySelector("[data-name=error]"); /** @type {HTMLElement} */ let cancel_btn = html_scene.querySelector("[data-name=cancel]"); @@ -58,6 +66,13 @@ export class NewShareScene { sharehref_input.addEventListener("input", onCleanHREFinput); + description_override_enabled.onchange = function () { + description_override_input.disabled = !description_override_enabled.checked; + }; + color_override_enabled.onchange = function () { + color_override_input.disabled = !color_override_enabled.checked; + }; + /** @type {?number} */ let scene_index = null; function oncancel() { @@ -82,7 +97,16 @@ export class NewShareScene { let enabled = enabled_checkbox.checked; let hidden = hidden_checkbox.checked; let permissions = permissions_rw_radio.checked ? "rw" : "r"; - let properties = properties_input.value; + + let properties = {}; + if (description_override_enabled.checked) { + let key = get_property_key(collection.type, "DESCRIPTION"); + if (key) properties[key] = description_override_input.value; + } + if (color_override_enabled.checked) { + let key = get_property_key(collection.type, "COLOR"); + if (key) properties[key] = color_override_input.value + (color_override_input.value ? "ff" : ""); + } let callback = function (/** @type {string} */ error) { if (scene_index === null) { @@ -122,7 +146,23 @@ export class NewShareScene { hidden_checkbox.checked = false; permissions_ro_radio.checked = true; permissions_rw_radio.checked = false; - properties_input.value = ""; + + description_override_enabled.checked = false; + description_override_input.value = collection.description || ""; + description_override_input.disabled = true; + + color_override_enabled.checked = false; + color_override_input.value = collection.color || "#ffffff"; + color_override_input.disabled = true; + + let is_calendar = CollectionType.is_subset(CollectionType.CALENDAR, collection.type); + let is_addressbook = collection.type === CollectionType.ADDRESSBOOK; + if (is_calendar || is_addressbook) { + properties_fieldset.classList.remove("hidden"); + } else { + properties_fieldset.classList.add("hidden"); + } + if (shareType === "map") { sharehref_input.value = random_uuid(); sharemapfields.classList.remove("hidden"); diff --git a/radicale/web/internal_data/js/scenes/ShareCollectionScene.js b/radicale/web/internal_data/js/scenes/ShareCollectionScene.js index d5e28fa8..00d0c418 100644 --- a/radicale/web/internal_data/js/scenes/ShareCollectionScene.js +++ b/radicale/web/internal_data/js/scenes/ShareCollectionScene.js @@ -73,14 +73,14 @@ export class ShareCollectionScene { } function onsharebytoken() { - let new_share_scene = new NewShareScene(user, password, collection.href, "token", function () { + let new_share_scene = new NewShareScene(user, password, collection, "token", function () { update_share_list(user, password, collection, errorHandler); }); push_scene(new_share_scene, false); } function onsharebymap() { - let new_share_scene = new NewShareScene(user, password, collection.href, "map", function () { + let new_share_scene = new NewShareScene(user, password, collection, "map", function () { update_share_list(user, password, collection, errorHandler); }); push_scene(new_share_scene, false);