ui: Allow action configs for bday conversion
This commit is contained in:
323
integ_tests/test_sharing_config.py
Normal file
323
integ_tests/test_sharing_config.py
Normal file
@@ -0,0 +1,323 @@
|
||||
# This file is part of Radicale - CalDAV and CardDAV server
|
||||
# Copyright © 2026-2026 Max Berger <max@berger.name>
|
||||
#
|
||||
# This library is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Integration tests for sharing actions configurations
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
from typing import Any, Generator
|
||||
|
||||
import pytest
|
||||
from playwright.sync_api import BrowserContext, Page, expect
|
||||
|
||||
from integ_tests.common import (SHARING_HTPASSWD, SHARING_XREMOTE, Config,
|
||||
login, start_radicale_server)
|
||||
|
||||
|
||||
@pytest.fixture(params=[SHARING_HTPASSWD, SHARING_XREMOTE])
|
||||
def config(request: pytest.FixtureRequest) -> Config:
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def radicale_server(
|
||||
tmp_path: pathlib.Path, config: Config
|
||||
) -> Generator[str, Any, None]:
|
||||
yield from start_radicale_server(tmp_path, config)
|
||||
|
||||
|
||||
def _create_addressbook_and_open_share(
|
||||
context: BrowserContext, page: Page, radicale_server: str, config: Config, name: str
|
||||
) -> None:
|
||||
login(page, radicale_server, config, context=context)
|
||||
|
||||
# create collection of type ADDRESSBOOK for bday (bday only works with ADDRESSBOOK)
|
||||
page.click('a[data-name="new"]')
|
||||
page.locator('#createcollectionscene select[data-name="type"]').select_option(
|
||||
"ADDRESSBOOK"
|
||||
)
|
||||
page.locator('#createcollectionscene input[data-name="displayname"]').fill(name)
|
||||
page.click('#createcollectionscene button[data-name="submit"]')
|
||||
|
||||
# Open share scene
|
||||
page.hover("article:not(.hidden)")
|
||||
page.click('article:not(.hidden) a[data-name="share"]', force=True, strict=True)
|
||||
|
||||
# Click Share by Map
|
||||
page.click('button[data-name="sharebymap"]')
|
||||
|
||||
|
||||
def test_sharing_config_save_and_reload(
|
||||
context: BrowserContext, page: Page, radicale_server: str, config: Config
|
||||
) -> None:
|
||||
_create_addressbook_and_open_share(
|
||||
context, page, radicale_server, config, "Addressbook for Save and Reload"
|
||||
)
|
||||
|
||||
# Config details should be hidden initially
|
||||
expect(page.locator("details[data-name='config']")).to_be_hidden()
|
||||
|
||||
# Select Birthday conversion
|
||||
page.click('label[for="newshare_conv_bday"]')
|
||||
|
||||
# Config details should now be visible and open
|
||||
expect(page.locator("details[data-name='config']")).to_be_visible()
|
||||
expect(page.locator("details[data-name='config']")).to_have_attribute("open", "")
|
||||
|
||||
# Fill share map fields (user and href)
|
||||
page.locator('input[data-name="shareuser"]').fill("max")
|
||||
page.locator('input[data-name="sharehref"]').fill("mapped-bday-reload")
|
||||
|
||||
# Verify all 5 config properties are present
|
||||
expect(
|
||||
page.locator("#newshare_config_conversion_bday_summary_template")
|
||||
).to_be_visible()
|
||||
expect(
|
||||
page.locator("#newshare_config_conversion_bday_description_template")
|
||||
).to_be_visible()
|
||||
expect(
|
||||
page.locator("#newshare_config_conversion_bday_alarm_trigger_template")
|
||||
).to_be_visible()
|
||||
expect(page.locator("#newshare_config_conversion_bday_categories")).to_be_visible()
|
||||
expect(page.locator("#newshare_config_conversion_bday_age_max")).to_be_visible()
|
||||
|
||||
# Set some config values
|
||||
page.locator("#newshare_config_conversion_bday_summary_template").fill(
|
||||
"{fn} Birthday"
|
||||
)
|
||||
page.locator("#newshare_config_conversion_bday_age_max").fill("120")
|
||||
|
||||
# Save
|
||||
page.click('#createeditsharescene button[data-name="submit"]')
|
||||
|
||||
# Verify share created
|
||||
expect(
|
||||
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)")
|
||||
).to_have_count(1)
|
||||
|
||||
# Re-open share in edit mode
|
||||
page.click(
|
||||
"tr[data-name='sharemaprowtemplate']:not(.hidden) button[data-name='edit']",
|
||||
strict=True,
|
||||
)
|
||||
|
||||
# Verify values are populated correctly in the inputs
|
||||
expect(
|
||||
page.locator("#newshare_config_conversion_bday_summary_template")
|
||||
).to_have_value("{fn} Birthday")
|
||||
expect(page.locator("#newshare_config_conversion_bday_age_max")).to_have_value(
|
||||
"120"
|
||||
)
|
||||
|
||||
page.click('#createeditsharescene button[data-name="cancel"]')
|
||||
|
||||
|
||||
def test_sharing_config_delete_checkbox(
|
||||
context: BrowserContext, page: Page, radicale_server: str, config: Config
|
||||
) -> None:
|
||||
_create_addressbook_and_open_share(
|
||||
context, page, radicale_server, config, "Addressbook for Delete Checkbox"
|
||||
)
|
||||
|
||||
# Select Birthday conversion
|
||||
page.click('label[for="newshare_conv_bday"]')
|
||||
|
||||
# Fill share map fields (user and href)
|
||||
page.locator('input[data-name="shareuser"]').fill("max")
|
||||
page.locator('input[data-name="sharehref"]').fill("mapped-bday-delete")
|
||||
|
||||
# Set config values
|
||||
page.locator("#newshare_config_conversion_bday_summary_template").fill(
|
||||
"{fn} Birthday"
|
||||
)
|
||||
page.locator("#newshare_config_conversion_bday_age_max").fill("120")
|
||||
|
||||
# Check delete checkbox for summary template
|
||||
page.click(
|
||||
"label[for='newshare_config_del_conversion_bday_summary_template']", strict=True
|
||||
)
|
||||
expect(
|
||||
page.locator("#newshare_config_conversion_bday_summary_template")
|
||||
).to_be_disabled()
|
||||
|
||||
# Uncheck delete checkbox for summary template
|
||||
page.click(
|
||||
"label[for='newshare_config_del_conversion_bday_summary_template']", strict=True
|
||||
)
|
||||
expect(
|
||||
page.locator("#newshare_config_conversion_bday_summary_template")
|
||||
).to_be_enabled()
|
||||
|
||||
# Check it again to save deletion
|
||||
page.click(
|
||||
"label[for='newshare_config_del_conversion_bday_summary_template']", strict=True
|
||||
)
|
||||
|
||||
# Save
|
||||
page.click('#createeditsharescene button[data-name="submit"]')
|
||||
|
||||
# Verify share created
|
||||
expect(
|
||||
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)")
|
||||
).to_have_count(1)
|
||||
|
||||
# Re-open share in edit mode
|
||||
page.click(
|
||||
"tr[data-name='sharemaprowtemplate']:not(.hidden) button[data-name='edit']",
|
||||
strict=True,
|
||||
)
|
||||
|
||||
# Verify that the summary template checkbox is checked, input is disabled and empty (since it was deleted)
|
||||
expect(
|
||||
page.locator("#newshare_config_del_conversion_bday_summary_template")
|
||||
).to_be_checked()
|
||||
expect(
|
||||
page.locator("#newshare_config_conversion_bday_summary_template")
|
||||
).to_be_disabled()
|
||||
expect(
|
||||
page.locator("#newshare_config_conversion_bday_summary_template")
|
||||
).to_have_value("")
|
||||
expect(page.locator("#newshare_config_conversion_bday_age_max")).to_have_value(
|
||||
"120"
|
||||
)
|
||||
|
||||
page.click('#createeditsharescene button[data-name="cancel"]')
|
||||
|
||||
|
||||
def test_sharing_config_conversion_none(
|
||||
context: BrowserContext, page: Page, radicale_server: str, config: Config
|
||||
) -> None:
|
||||
_create_addressbook_and_open_share(
|
||||
context, page, radicale_server, config, "Addressbook for Conversion None"
|
||||
)
|
||||
|
||||
# Select Birthday conversion
|
||||
page.click('label[for="newshare_conv_bday"]')
|
||||
|
||||
# Fill share map fields (user and href)
|
||||
page.locator('input[data-name="shareuser"]').fill("max")
|
||||
page.locator('input[data-name="sharehref"]').fill("mapped-bday-none")
|
||||
|
||||
# Set some config values
|
||||
page.locator("#newshare_config_conversion_bday_summary_template").fill(
|
||||
"{fn} Birthday"
|
||||
)
|
||||
page.locator("#newshare_config_conversion_bday_age_max").fill("120")
|
||||
|
||||
# Save
|
||||
page.click('#createeditsharescene button[data-name="submit"]')
|
||||
|
||||
# Verify share created
|
||||
expect(
|
||||
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)")
|
||||
).to_have_count(1)
|
||||
|
||||
# Re-open share in edit mode
|
||||
page.click(
|
||||
"tr[data-name='sharemaprowtemplate']:not(.hidden) button[data-name='edit']",
|
||||
strict=True,
|
||||
)
|
||||
|
||||
# Change conversion back to None
|
||||
page.click('label[for="newshare_conv_none"]')
|
||||
expect(page.locator("details[data-name='config']")).to_be_hidden()
|
||||
|
||||
# Save
|
||||
page.click('#createeditsharescene button[data-name="submit"]')
|
||||
|
||||
# Re-open share in edit mode once more
|
||||
page.click(
|
||||
"tr[data-name='sharemaprowtemplate']:not(.hidden) button[data-name='edit']",
|
||||
strict=True,
|
||||
)
|
||||
|
||||
# Change conversion back to Birthday
|
||||
page.click('label[for="newshare_conv_bday"]')
|
||||
|
||||
# Config fields should be empty and enabled, delete checkboxes unchecked
|
||||
expect(
|
||||
page.locator("#newshare_config_conversion_bday_summary_template")
|
||||
).to_have_value("")
|
||||
expect(
|
||||
page.locator("#newshare_config_conversion_bday_summary_template")
|
||||
).to_be_enabled()
|
||||
expect(
|
||||
page.locator("#newshare_config_del_conversion_bday_summary_template")
|
||||
).not_to_be_checked()
|
||||
|
||||
expect(page.locator("#newshare_config_conversion_bday_age_max")).to_have_value("")
|
||||
expect(page.locator("#newshare_config_conversion_bday_age_max")).to_be_enabled()
|
||||
expect(
|
||||
page.locator("#newshare_config_del_conversion_bday_age_max")
|
||||
).not_to_be_checked()
|
||||
|
||||
page.click('#createeditsharescene button[data-name="cancel"]')
|
||||
|
||||
|
||||
def test_sharing_config_invalid_integer(
|
||||
context: BrowserContext, page: Page, radicale_server: str, config: Config
|
||||
) -> None:
|
||||
_create_addressbook_and_open_share(
|
||||
context, page, radicale_server, config, "Addressbook for Invalid Int"
|
||||
)
|
||||
|
||||
# Select Birthday conversion
|
||||
page.click('label[for="newshare_conv_bday"]')
|
||||
|
||||
# Fill share map fields (user and href)
|
||||
page.locator('input[data-name="shareuser"]').fill("max")
|
||||
page.locator('input[data-name="sharehref"]').fill("mapped-bday-invalid")
|
||||
|
||||
# Set summary template (valid)
|
||||
page.locator("#newshare_config_conversion_bday_summary_template").fill(
|
||||
"{fn} Birthday"
|
||||
)
|
||||
|
||||
# Set invalid integer for age_max (which has type 'int')
|
||||
page.locator("#newshare_config_conversion_bday_age_max").fill("foo")
|
||||
|
||||
# Verify that there is some error message shown
|
||||
expect(page.locator("#createeditsharescene [data-name='error']")).not_to_be_empty()
|
||||
expect(page.locator("#createeditsharescene [data-name='error']")).to_contain_text(
|
||||
"Max age must be an integer"
|
||||
)
|
||||
|
||||
# Click submit button
|
||||
page.click('#createeditsharescene button[data-name="submit"]')
|
||||
|
||||
# Verify we are still on the edit share scene (submit is blocked)
|
||||
expect(page.locator("#createeditsharescene")).to_be_visible()
|
||||
expect(page.locator("#createeditsharescene [data-name='error']")).to_contain_text(
|
||||
"Max age must be an integer"
|
||||
)
|
||||
|
||||
# Fill valid integer now
|
||||
page.locator("#newshare_config_conversion_bday_age_max").fill("120")
|
||||
|
||||
# Verify error is cleared
|
||||
expect(page.locator("#createeditsharescene [data-name='error']")).to_be_empty()
|
||||
|
||||
# Save
|
||||
page.click('#createeditsharescene button[data-name="submit"]')
|
||||
|
||||
# Verify share created successfully
|
||||
expect(
|
||||
page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)")
|
||||
).to_have_count(1)
|
||||
|
||||
# Cancel/Close share collection scene
|
||||
page.click('#sharecollectionscene button[data-name="cancel"]')
|
||||
@@ -1324,7 +1324,8 @@ class BaseSharing:
|
||||
User=User,
|
||||
Timestamp=Timestamp,
|
||||
Properties=Properties,
|
||||
Actions=Actions)
|
||||
Actions=Actions,
|
||||
Conversion=Conversion)
|
||||
else:
|
||||
result = self.database_update_sharing(
|
||||
ShareType=ShareType,
|
||||
@@ -1337,7 +1338,8 @@ class BaseSharing:
|
||||
User=User,
|
||||
Timestamp=Timestamp,
|
||||
Properties=Properties,
|
||||
Actions=Actions)
|
||||
Actions=Actions,
|
||||
Conversion=Conversion)
|
||||
|
||||
elif user == share['User']:
|
||||
# User is only allowed to update Properties
|
||||
|
||||
@@ -306,6 +306,12 @@
|
||||
<!-- Dynamic radio buttons will be inserted here -->
|
||||
</div>
|
||||
</details>
|
||||
<details data-name="config" class="hidden">
|
||||
<summary>Config</summary>
|
||||
<div data-name="config_container">
|
||||
<!-- Dynamic edit fields will be inserted here -->
|
||||
</div>
|
||||
</details>
|
||||
<details data-name="properties_override">
|
||||
<summary>Properties override</summary>
|
||||
<div class="property-override">
|
||||
|
||||
@@ -138,8 +138,158 @@ export function discover_server_features(user, password, callback) {
|
||||
* @property {number} [TimestampUpdated]
|
||||
* @property {Object<String, String>} [Properties]
|
||||
* @property {string} [Conversion]
|
||||
* @property {Object<String, *>} [Actions]
|
||||
*/
|
||||
|
||||
export class ConfigProperty {
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string} type
|
||||
* @param {string} displayName
|
||||
*/
|
||||
constructor(key, type, displayName) {
|
||||
/** @type {string} */ this.key = key;
|
||||
/** @type {string} */ this.type = type;
|
||||
/** @type {string} */ this.displayName = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
export const BDAY_CONFIG = Object.freeze([
|
||||
Object.freeze(new ConfigProperty("conversion_bday_summary_template", "str", "Summary template")),
|
||||
Object.freeze(new ConfigProperty("conversion_bday_description_template", "str", "Description template")),
|
||||
Object.freeze(new ConfigProperty("conversion_bday_alarm_trigger_template", "str", "Alarm trigger template")),
|
||||
Object.freeze(new ConfigProperty("conversion_bday_categories", "str", "Categories")),
|
||||
Object.freeze(new ConfigProperty("conversion_bday_age_max", "int", "Max age"))
|
||||
]);
|
||||
|
||||
export class ShareConfig {
|
||||
/**
|
||||
* @param {ShareConfig|Record<string, any>} [data]
|
||||
*/
|
||||
constructor(data = {}) {
|
||||
/** @type {Record<string, any>} */
|
||||
this._values = {};
|
||||
let rawData = data;
|
||||
if (data instanceof ShareConfig) {
|
||||
rawData = data._values;
|
||||
}
|
||||
for (const [key, value] of Object.entries(rawData || {})) {
|
||||
const bdayProp = BDAY_CONFIG.find(c => c.key === key);
|
||||
if (bdayProp && bdayProp.type === "int" && value !== null && value !== undefined) {
|
||||
let parsed = parseInt(String(value), 10);
|
||||
this._values[key] = isNaN(parsed) ? value : parsed;
|
||||
} else {
|
||||
this._values[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ConfigProperty} property
|
||||
* @returns {any}
|
||||
*/
|
||||
get(property) {
|
||||
let val = this._values[property.key] ?? null;
|
||||
return val === "#DEL#" ? null : val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ConfigProperty} property
|
||||
* @param {any} val
|
||||
*/
|
||||
set(property, val) {
|
||||
if (val === undefined || val === null || val === "") {
|
||||
this._values[property.key] = null;
|
||||
} else if (property.type === "int") {
|
||||
let parsed = parseInt(String(val), 10);
|
||||
this._values[property.key] = isNaN(parsed) ? val : parsed;
|
||||
} else {
|
||||
this._values[property.key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ConfigProperty|string} property
|
||||
*/
|
||||
delete(property) {
|
||||
const key = typeof property === "string" ? property : property.key;
|
||||
this._values[key] = "#DEL#";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ConfigProperty} property
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isDeleted(property) {
|
||||
return this._values[property.key] === "#DEL#";
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Record<string, any>}
|
||||
*/
|
||||
toJSON() {
|
||||
/** @type {Record<string, any>} */
|
||||
let obj = {};
|
||||
for (const [key, value] of Object.entries(this._values)) {
|
||||
if (value !== null) {
|
||||
obj[key] = value;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
export class ShareActions {
|
||||
/**
|
||||
* @param {Record<string, any>} [data]
|
||||
*/
|
||||
constructor(data = {}) {
|
||||
/** @type {ShareConfig} */
|
||||
this._config = new ShareConfig(data.config || {});
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (key !== "config") {
|
||||
(/** @type {any} */ (this))[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ShareConfig}
|
||||
*/
|
||||
get config() {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ShareConfig|Record<string, any>} value
|
||||
*/
|
||||
set config(value) {
|
||||
if (value instanceof ShareConfig) {
|
||||
this._config = value;
|
||||
} else {
|
||||
this._config = new ShareConfig(value || {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Record<string, any>|undefined}
|
||||
*/
|
||||
toJSON() {
|
||||
/** @type {Record<string, any>} */
|
||||
let obj = {};
|
||||
for (const [key, value] of Object.entries(this)) {
|
||||
if (key === "_config" && value instanceof ShareConfig) {
|
||||
let configJSON = value.toJSON();
|
||||
if (Object.keys(configJSON).length > 0) {
|
||||
obj.config = configJSON;
|
||||
}
|
||||
} else if (value !== null && value !== undefined) {
|
||||
obj[key] = value;
|
||||
}
|
||||
}
|
||||
return Object.keys(obj).length > 0 ? obj : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class Share {
|
||||
/**
|
||||
@@ -160,6 +310,40 @@ export class Share {
|
||||
/** @type {number} */ this.TimestampUpdated = data.TimestampUpdated || 0;
|
||||
/** @type {Object<String, String>} */ this.Properties = data.Properties || {};
|
||||
/** @type {string} */ this.Conversion = data.Conversion || "";
|
||||
/** @type {ShareActions} */ this._Actions = new ShareActions();
|
||||
this.Actions = data.Actions || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ShareActions}
|
||||
*/
|
||||
get Actions() {
|
||||
return this._Actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ShareActions|Record<string, any>} value
|
||||
*/
|
||||
set Actions(value) {
|
||||
if (value instanceof ShareActions) {
|
||||
this._Actions = value;
|
||||
} else {
|
||||
this._Actions = new ShareActions(value || {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ShareConfig}
|
||||
*/
|
||||
get config() {
|
||||
return this.Actions.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ShareConfig|Record<string, any>} value
|
||||
*/
|
||||
set config(value) {
|
||||
this.Actions.config = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +431,7 @@ export function add_share_by_token(
|
||||
Hidden: share.HiddenByOwner,
|
||||
Properties: share.Properties,
|
||||
Conversion: share.Conversion,
|
||||
Actions: share.Actions,
|
||||
},
|
||||
function (response) {
|
||||
let json_response = JSON.parse(response);
|
||||
@@ -288,6 +473,7 @@ export function add_share_by_map(
|
||||
User: share.User,
|
||||
PathOrToken: decodeURIComponent(share.PathOrToken),
|
||||
Conversion: share.Conversion,
|
||||
Actions: share.Actions,
|
||||
},
|
||||
function (response) {
|
||||
let json_response = JSON.parse(response);
|
||||
@@ -390,6 +576,7 @@ export function update_share_by_token(
|
||||
Hidden: share.HiddenByOwner,
|
||||
Properties: share.Properties,
|
||||
Conversion: share.Conversion,
|
||||
Actions: share.Actions,
|
||||
},
|
||||
function (response) {
|
||||
let json_response = JSON.parse(response);
|
||||
@@ -431,6 +618,7 @@ export function update_share_by_map(
|
||||
Hidden: share.HiddenByOwner,
|
||||
Properties: share.Properties,
|
||||
Conversion: share.Conversion,
|
||||
Actions: share.Actions,
|
||||
},
|
||||
function (response) {
|
||||
let json_response = JSON.parse(response);
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Share, add_share_by_map, add_share_by_token, get_property_key, update_share_by_map, update_share_by_token } from "../api/sharing.js";
|
||||
import { BDAY_CONFIG, Share, ShareConfig, add_share_by_map, add_share_by_token, get_property_key, update_share_by_map, update_share_by_token } from "../api/sharing.js";
|
||||
import { CollectionType, Permission } from "../models/collection.js";
|
||||
import { extract_title, update_title_and_description } from "../utils/collection_utils.js";
|
||||
import { collectionsCache } from "../utils/collections_cache.js";
|
||||
import { ErrorHandler } from "../utils/error.js";
|
||||
import { FormValidator, validate_href, validate_non_empty, validate_not_empty_or_equals } from "../utils/form_validator.js";
|
||||
import { FormValidator, validate_href, validate_integer, validate_non_empty, validate_not_empty_or_equals } from "../utils/form_validator.js";
|
||||
import { get_element, get_element_by_id, onCleanHREFinput, random_uuid } from "../utils/misc.js";
|
||||
import { Scene, is_current_scene, pop_scene } from "./scene_manager.js";
|
||||
|
||||
@@ -64,6 +64,8 @@ export class CreateEditShareScene {
|
||||
this._token_write_warning = /** @type {HTMLElement} */ (get_element(this._html_scene, "[data-name=token_write_warning]"));
|
||||
this._conversions_details = /** @type {HTMLDetailsElement} */ (get_element(this._html_scene, "[data-name=conversions]"));
|
||||
this._conversions_container = get_element(this._html_scene, "[data-name=conversions_container]");
|
||||
this._config_details = /** @type {HTMLDetailsElement} */ (get_element(this._html_scene, "[data-name=config]"));
|
||||
this._config_container = get_element(this._html_scene, "[data-name=config_container]");
|
||||
|
||||
this._properties_fieldset = /** @type {HTMLDetailsElement} */ (get_element(this._html_scene, "[data-name=properties_override]"));
|
||||
this._displayname_override_enabled = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=displayname_override_enabled]"));
|
||||
@@ -78,9 +80,12 @@ export class CreateEditShareScene {
|
||||
this._cancel_btn = get_element(this._html_scene, "[data-name=cancel]");
|
||||
|
||||
this._errorHandler = new ErrorHandler(this._error_form);
|
||||
this._map_validator = new FormValidator(this._errorHandler);
|
||||
this._validator = new FormValidator(this._errorHandler);
|
||||
|
||||
this._map_validator.addValidator(this._shareuser_input, () => {
|
||||
this._validator.addValidator(this._shareuser_input, () => {
|
||||
if (this._shareType !== "map") {
|
||||
return null;
|
||||
}
|
||||
let conversion = this._get_selected_conversion();
|
||||
if (conversion != "none") {
|
||||
return validate_non_empty(this._shareuser_input, "Share User")();
|
||||
@@ -88,7 +93,34 @@ export class CreateEditShareScene {
|
||||
return validate_not_empty_or_equals(this._shareuser_input, user, "Share User")();
|
||||
}
|
||||
});
|
||||
this._map_validator.addValidator(this._sharehref_input, validate_href(this._sharehref_input, "Share Href"));
|
||||
this._validator.addValidator(this._sharehref_input, () => {
|
||||
if (this._shareType !== "map") {
|
||||
return null;
|
||||
}
|
||||
return validate_href(this._sharehref_input, "Share Href")();
|
||||
});
|
||||
|
||||
this._validator.addValidator(/** @type {any} */(this._form), () => {
|
||||
let conversion = this._get_selected_conversion();
|
||||
if (conversion === "bday") {
|
||||
for (let property of BDAY_CONFIG) {
|
||||
if (property.type === "int") {
|
||||
/** @type {HTMLInputElement | null} */
|
||||
let textInput = this._config_container.querySelector("#newshare_config_" + property.key);
|
||||
/** @type {HTMLInputElement | null} */
|
||||
let deleteCheckbox = this._config_container.querySelector("#newshare_config_del_" + property.key);
|
||||
let isDeleted = deleteCheckbox ? deleteCheckbox.checked : false;
|
||||
if (textInput && !isDeleted) {
|
||||
let error = validate_integer(textInput, property.displayName)();
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
this._sharehref_input.addEventListener("input", onCleanHREFinput);
|
||||
|
||||
@@ -150,8 +182,75 @@ export class CreateEditShareScene {
|
||||
}
|
||||
}
|
||||
|
||||
this._map_validator.validate();
|
||||
this._validator.validate();
|
||||
}
|
||||
if (conversion === "bday") {
|
||||
this._config_details.classList.remove("hidden");
|
||||
this._config_details.open = true;
|
||||
} else {
|
||||
this._config_details.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {readonly import("../api/sharing.js").ConfigProperty[]} config_properties
|
||||
* @param {string} conversion
|
||||
*/
|
||||
_create_config_container(config_properties, conversion) {
|
||||
this._config_container.innerHTML = "";
|
||||
config_properties.forEach(property => {
|
||||
let row = document.createElement("div");
|
||||
row.className = "property-override";
|
||||
|
||||
let label = document.createElement("label");
|
||||
label.textContent = property.displayName + ":";
|
||||
label.htmlFor = "newshare_config_" + property.key;
|
||||
|
||||
let textInput = document.createElement("input");
|
||||
textInput.type = "text";
|
||||
textInput.id = "newshare_config_" + property.key;
|
||||
textInput.dataset.key = property.key;
|
||||
|
||||
let deleteCheckbox = document.createElement("input");
|
||||
deleteCheckbox.type = "checkbox";
|
||||
deleteCheckbox.id = "newshare_config_del_" + property.key;
|
||||
deleteCheckbox.dataset.key = property.key;
|
||||
|
||||
let deleteLabel = document.createElement("label");
|
||||
deleteLabel.textContent = "delete";
|
||||
deleteLabel.htmlFor = deleteCheckbox.id;
|
||||
|
||||
let val = null;
|
||||
let isDel = false;
|
||||
if (this._edit && this._share && this._share.Conversion === conversion && this._share.config) {
|
||||
val = this._share.config.get(property);
|
||||
isDel = this._share.config.isDeleted(property);
|
||||
}
|
||||
|
||||
if (isDel) {
|
||||
deleteCheckbox.checked = true;
|
||||
textInput.disabled = true;
|
||||
textInput.value = "";
|
||||
} else {
|
||||
deleteCheckbox.checked = false;
|
||||
textInput.disabled = false;
|
||||
textInput.value = val !== null ? String(val) : "";
|
||||
}
|
||||
|
||||
deleteCheckbox.onchange = () => {
|
||||
if (deleteCheckbox.checked) {
|
||||
textInput.disabled = true;
|
||||
} else {
|
||||
textInput.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
row.appendChild(label);
|
||||
row.appendChild(deleteCheckbox);
|
||||
row.appendChild(deleteLabel);
|
||||
row.appendChild(textInput);
|
||||
this._config_container.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
_oncancel() {
|
||||
@@ -165,11 +264,9 @@ export class CreateEditShareScene {
|
||||
|
||||
_onsubmit() {
|
||||
try {
|
||||
if (this._shareType === "map") {
|
||||
if (!this._map_validator.validate()) {
|
||||
if (!this._validator.validate()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let conversion = this._get_selected_conversion();
|
||||
let is_conversion = conversion != "none";
|
||||
let enabled_by_owner = is_conversion ? true : this._enabled_checkbox.checked;
|
||||
@@ -209,6 +306,33 @@ export class CreateEditShareScene {
|
||||
}
|
||||
};
|
||||
|
||||
/** @type {Record<string, any>} */
|
||||
let new_actions = {};
|
||||
if (this._edit && this._share && this._share.Actions) {
|
||||
new_actions = JSON.parse(JSON.stringify(this._share.Actions));
|
||||
}
|
||||
if (conversion_value === "bday") {
|
||||
let new_config = new ShareConfig();
|
||||
BDAY_CONFIG.forEach(property => {
|
||||
/** @type {HTMLInputElement | null} */
|
||||
let textInput = this._config_container.querySelector("#newshare_config_" + property.key);
|
||||
/** @type {HTMLInputElement | null} */
|
||||
let deleteCheckbox = this._config_container.querySelector("#newshare_config_del_" + property.key);
|
||||
if (deleteCheckbox && deleteCheckbox.checked) {
|
||||
new_config.delete(property);
|
||||
} else if (textInput) {
|
||||
new_config.set(property, textInput.value);
|
||||
}
|
||||
});
|
||||
new_actions.config = new_config;
|
||||
} else {
|
||||
let new_config = new ShareConfig();
|
||||
BDAY_CONFIG.forEach(property => {
|
||||
new_config.delete(property);
|
||||
});
|
||||
new_actions.config = new_config;
|
||||
}
|
||||
|
||||
let new_share = new Share({
|
||||
ShareType: this._shareType,
|
||||
PathMapped: this._pathMapped,
|
||||
@@ -221,6 +345,7 @@ export class CreateEditShareScene {
|
||||
User: (this._edit && this._share) ? this._share.User : this._shareuser_input.value,
|
||||
PathOrToken: (this._edit && this._share) ? this._share.PathOrToken : (this._shareType === "map" ? "/" + this._shareuser_input.value + "/" + this._sharehref_input.value + "/" : ""),
|
||||
Conversion: conversion_value,
|
||||
Actions: new_actions,
|
||||
});
|
||||
|
||||
if (this._edit) {
|
||||
@@ -255,6 +380,8 @@ export class CreateEditShareScene {
|
||||
this._cancel_btn.onclick = () => this._oncancel();
|
||||
this._form.onsubmit = () => this._onsubmit();
|
||||
|
||||
this._create_config_container(BDAY_CONFIG, "bday");
|
||||
|
||||
let onChangeCallback = () => this._on_permissions_change();
|
||||
this._permissions_ro_radio.addEventListener("change", onChangeCallback);
|
||||
this._permissions_rw_radio.addEventListener("change", onChangeCallback);
|
||||
@@ -409,12 +536,12 @@ export class CreateEditShareScene {
|
||||
}
|
||||
this._sharehref_input.disabled = this._edit;
|
||||
this._sharemapfields.classList.remove("hidden");
|
||||
this._map_validator.validate();
|
||||
} else {
|
||||
this._sharehref_input.value = "";
|
||||
this._sharemapfields.classList.add("hidden");
|
||||
this._errorHandler.clearError();
|
||||
}
|
||||
this._validator.validate();
|
||||
this._on_permissions_change();
|
||||
|
||||
update_title_and_description(this._collection, this._title, this._description);
|
||||
|
||||
@@ -170,3 +170,23 @@ export function validate_files(input, field_name) {
|
||||
return "Please select at least one " + field_name;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the input is a valid integer (if not empty).
|
||||
* @param {HTMLInputElement} input
|
||||
* @param {string} field_name
|
||||
* @returns {function(): ?string}
|
||||
*/
|
||||
export function validate_integer(input, field_name) {
|
||||
return () => {
|
||||
let value = input.value.trim();
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
let parsed = parseInt(value, 10);
|
||||
if (isNaN(parsed) || String(parsed) !== value) {
|
||||
return field_name + " must be an integer";
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user