ui: Allow action configs for bday conversion
This commit is contained in:
@@ -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,10 +264,8 @@ export class CreateEditShareScene {
|
||||
|
||||
_onsubmit() {
|
||||
try {
|
||||
if (this._shareType === "map") {
|
||||
if (!this._map_validator.validate()) {
|
||||
return false;
|
||||
}
|
||||
if (!this._validator.validate()) {
|
||||
return false;
|
||||
}
|
||||
let conversion = this._get_selected_conversion();
|
||||
let is_conversion = conversion != "none";
|
||||
@@ -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