1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-08-01 18:18:31 +00:00

[WEB UI] New WebUI Improvements

Added WebUI improvements as discussed in discussion #1416 in March 2024.
This commit is contained in:
MatthewHana 2024-03-13 07:42:47 +11:00
parent 825464f102
commit ed6432706f
3 changed files with 114 additions and 6 deletions

View file

@ -196,6 +196,11 @@ main{
text-align: center; text-align: center;
} }
#collectionsscene article small[data-name=contentcount]{
font-weight: bold;
font-style: normal;
}
#editcollectionscene p span{ #editcollectionscene p span{
word-wrap:break-word; word-wrap:break-word;
font-weight: bold; font-weight: bold;
@ -228,6 +233,12 @@ main{
margin-top: 15px; margin-top: 15px;
} }
.deleteconfirmationtxt{
text-align: center;
font-size: 1em;
font-weight: bold;
}
.fabcontainer{ .fabcontainer{
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column-reverse;

View file

@ -36,6 +36,13 @@ const ROOT_PATH = location.pathname.replace(new RegExp("/+[^/]+/*(/index\\.html?
*/ */
const COLOR_RE = new RegExp("^(#[0-9A-Fa-f]{6})(?:[0-9A-Fa-f]{2})?$"); const COLOR_RE = new RegExp("^(#[0-9A-Fa-f]{6})(?:[0-9A-Fa-f]{2})?$");
/**
* The text needed to confirm deleting a collection
* @const
*/
const DELETE_CONFIRMATION_TEXT = "DELETE";
/** /**
* Escape string for usage in XML * Escape string for usage in XML
* @param {string} s * @param {string} s
@ -94,6 +101,23 @@ const CollectionType = {
union.push(this.WEBCAL); union.push(this.WEBCAL);
} }
return union.join("_"); return union.join("_");
},
valid_options_for_type: function(a){
a = a.trim().toUpperCase();
switch(a){
case CollectionType.CALENDAR_JOURNAL_TASKS:
case CollectionType.CALENDAR_JOURNAL:
case CollectionType.CALENDAR_TASKS:
case CollectionType.JOURNAL_TASKS:
case CollectionType.CALENDAR:
case CollectionType.JOURNAL:
case CollectionType.TASKS:
return [CollectionType.CALENDAR_JOURNAL_TASKS, CollectionType.CALENDAR_JOURNAL, CollectionType.CALENDAR_TASKS, CollectionType.JOURNAL_TASKS, CollectionType.CALENDAR, CollectionType.JOURNAL, CollectionType.TASKS];
case CollectionType.ADDRESSBOOK:
case CollectionType.WEBCAL:
default:
return [a];
}
} }
}; };
@ -106,13 +130,14 @@ const CollectionType = {
* @param {string} description * @param {string} description
* @param {string} color * @param {string} color
*/ */
function Collection(href, type, displayname, description, color, source) { function Collection(href, type, displayname, description, color, contentcount, source) {
this.href = href; this.href = href;
this.type = type; this.type = type;
this.displayname = displayname; this.displayname = displayname;
this.color = color; this.color = color;
this.description = description; this.description = description;
this.source = source; this.source = source;
this.contentcount = contentcount;
} }
/** /**
@ -139,6 +164,7 @@ function get_principal(user, password, callback) {
CollectionType.PRINCIPAL, CollectionType.PRINCIPAL,
displayname_element ? displayname_element.textContent : "", displayname_element ? displayname_element.textContent : "",
"", "",
0,
""), null); ""), null);
} else { } else {
callback(null, "Internal error"); callback(null, "Internal error");
@ -188,6 +214,7 @@ function get_collections(user, password, collection, callback) {
let addressbookcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-color"); let addressbookcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-color");
let calendardesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-description"); let calendardesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-description");
let addressbookdesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-description"); let addressbookdesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-description");
let contentcount_element = response.querySelector(response_query + " > *|propstat > *|prop > *|getcontentcount");
let webcalsource_element = response.querySelector(response_query + " > *|propstat > *|prop > *|source"); let webcalsource_element = response.querySelector(response_query + " > *|propstat > *|prop > *|source");
let components_query = response_query + " > *|propstat > *|prop > *|supported-calendar-component-set"; let components_query = response_query + " > *|propstat > *|prop > *|supported-calendar-component-set";
let components_element = response.querySelector(components_query); let components_element = response.querySelector(components_query);
@ -197,11 +224,13 @@ function get_collections(user, password, collection, callback) {
let color = ""; let color = "";
let description = ""; let description = "";
let source = ""; let source = "";
let count = 0;
if (resourcetype_element) { if (resourcetype_element) {
if (resourcetype_element.querySelector(resourcetype_query + " > *|addressbook")) { if (resourcetype_element.querySelector(resourcetype_query + " > *|addressbook")) {
type = CollectionType.ADDRESSBOOK; type = CollectionType.ADDRESSBOOK;
color = addressbookcolor_element ? addressbookcolor_element.textContent : ""; color = addressbookcolor_element ? addressbookcolor_element.textContent : "";
description = addressbookdesc_element ? addressbookdesc_element.textContent : ""; description = addressbookdesc_element ? addressbookdesc_element.textContent : "";
count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
} else if (resourcetype_element.querySelector(resourcetype_query + " > *|subscribed")) { } else if (resourcetype_element.querySelector(resourcetype_query + " > *|subscribed")) {
type = CollectionType.WEBCAL; type = CollectionType.WEBCAL;
source = webcalsource_element ? webcalsource_element.textContent : ""; source = webcalsource_element ? webcalsource_element.textContent : "";
@ -221,6 +250,7 @@ function get_collections(user, password, collection, callback) {
} }
color = calendarcolor_element ? calendarcolor_element.textContent : ""; color = calendarcolor_element ? calendarcolor_element.textContent : "";
description = calendardesc_element ? calendardesc_element.textContent : ""; description = calendardesc_element ? calendardesc_element.textContent : "";
count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
} }
} }
let sane_color = color.trim(); let sane_color = color.trim();
@ -233,7 +263,7 @@ function get_collections(user, password, collection, callback) {
} }
} }
if (href.substr(-1) === "/" && href !== collection.href && type) { if (href.substr(-1) === "/" && href !== collection.href && type) {
collections.push(new Collection(href, type, displayname, description, sane_color, source)); collections.push(new Collection(href, type, displayname, description, sane_color, count, source));
} }
} }
collections.sort(function(a, b) { collections.sort(function(a, b) {
@ -265,6 +295,7 @@ function get_collections(user, password, collection, callback) {
'<C:supported-calendar-component-set />' + '<C:supported-calendar-component-set />' +
'<CR:addressbook-description />' + '<CR:addressbook-description />' +
'<CS:source />' + '<CS:source />' +
'<RADICALE:getcontentcount />' +
'</prop>' + '</prop>' +
'</propfind>'); '</propfind>');
return request; return request;
@ -708,6 +739,7 @@ function CollectionsScene(user, password, collection, onerror) {
node.classList.remove("hidden"); node.classList.remove("hidden");
let title_form = node.querySelector("[data-name=title]"); let title_form = node.querySelector("[data-name=title]");
let description_form = node.querySelector("[data-name=description]"); let description_form = node.querySelector("[data-name=description]");
let contentcount_form = node.querySelector("[data-name=contentcount]");
let url_form = node.querySelector("[data-name=url]"); let url_form = node.querySelector("[data-name=url]");
let color_form = node.querySelector("[data-name=color]"); let color_form = node.querySelector("[data-name=color]");
let delete_btn = node.querySelector("[data-name=delete]"); let delete_btn = node.querySelector("[data-name=delete]");
@ -739,6 +771,9 @@ function CollectionsScene(user, password, collection, onerror) {
if(description_form.textContent.length > 150){ if(description_form.textContent.length > 150){
description_form.classList.add("smalltext"); description_form.classList.add("smalltext");
} }
if(collection.type != CollectionType.WEBCAL){
contentcount_form.textContent = (collection.contentcount > 0 ? collection.contentcount : "No") + " item" + (collection.contentcount == 1 ? "" : "s") + " in collection";
}
let href = SERVER + collection.href; let href = SERVER + collection.href;
url_form.value = href; url_form.value = href;
download_btn.href = href; download_btn.href = href;
@ -939,14 +974,25 @@ function DeleteCollectionScene(user, password, collection) {
let html_scene = document.getElementById("deletecollectionscene"); let html_scene = document.getElementById("deletecollectionscene");
let title_form = html_scene.querySelector("[data-name=title]"); let title_form = html_scene.querySelector("[data-name=title]");
let error_form = html_scene.querySelector("[data-name=error]"); let error_form = html_scene.querySelector("[data-name=error]");
let confirmation_txt = html_scene.querySelector("[data-name=confirmationtxt]");
let delete_confirmation_lbl = html_scene.querySelector("[data-name=deleteconfirmationtext]");
let delete_btn = html_scene.querySelector("[data-name=delete]"); let delete_btn = html_scene.querySelector("[data-name=delete]");
let cancel_btn = html_scene.querySelector("[data-name=cancel]"); let cancel_btn = html_scene.querySelector("[data-name=cancel]");
delete_confirmation_lbl.innerHTML = DELETE_CONFIRMATION_TEXT;
confirmation_txt.value = "";
confirmation_txt.addEventListener("keydown", onkeydown);
/** @type {?number} */ let scene_index = null; /** @type {?number} */ let scene_index = null;
/** @type {?XMLHttpRequest} */ let delete_req = null; /** @type {?XMLHttpRequest} */ let delete_req = null;
let error = ""; let error = "";
function ondelete() { function ondelete() {
let confirmation_text_value = confirmation_txt.value;
if(confirmation_text_value != DELETE_CONFIRMATION_TEXT){
alert("Please type the confirmation text to delete this collection.");
return;
}
try { try {
let loading_scene = new LoadingScene(); let loading_scene = new LoadingScene();
push_scene(loading_scene); push_scene(loading_scene);
@ -977,6 +1023,13 @@ function DeleteCollectionScene(user, password, collection) {
return false; return false;
} }
function onkeydown(event){
if (event.keyCode !== 13) {
return;
}
ondelete();
}
this.show = function() { this.show = function() {
this.release(); this.release();
scene_index = scene_stack.length - 1; scene_index = scene_stack.length - 1;
@ -1031,6 +1084,8 @@ function CreateEditCollectionScene(user, password, collection) {
let html_scene = document.getElementById(edit ? "editcollectionscene" : "createcollectionscene"); let html_scene = document.getElementById(edit ? "editcollectionscene" : "createcollectionscene");
let title_form = edit ? html_scene.querySelector("[data-name=title]") : null; let title_form = edit ? html_scene.querySelector("[data-name=title]") : null;
let error_form = html_scene.querySelector("[data-name=error]"); let error_form = html_scene.querySelector("[data-name=error]");
let href_form = html_scene.querySelector("[data-name=href]");
let href_label = html_scene.querySelector("label[for=href]");
let displayname_form = html_scene.querySelector("[data-name=displayname]"); let displayname_form = html_scene.querySelector("[data-name=displayname]");
let displayname_label = html_scene.querySelector("label[for=displayname]"); let displayname_label = html_scene.querySelector("label[for=displayname]");
let description_form = html_scene.querySelector("[data-name=description]"); let description_form = html_scene.querySelector("[data-name=description]");
@ -1057,28 +1112,46 @@ function CreateEditCollectionScene(user, password, collection) {
let type = edit ? collection.type : CollectionType.CALENDAR_JOURNAL_TASKS; let type = edit ? collection.type : CollectionType.CALENDAR_JOURNAL_TASKS;
let color = edit && collection.color ? collection.color : "#" + random_hex(6); let color = edit && collection.color ? collection.color : "#" + random_hex(6);
if(!edit){
href_form.addEventListener("keydown", cleanHREFinput);
}
function remove_invalid_types() { function remove_invalid_types() {
if (!edit) { if (!edit) {
return; return;
} }
/** @type {HTMLOptionsCollection} */ let options = type_form.options; /** @type {HTMLOptionsCollection} */ let options = type_form.options;
// remove all options that are not supersets // remove all options that are not supersets
let valid_type_options = CollectionType.valid_options_for_type(type);
for (let i = options.length - 1; i >= 0; i--) { for (let i = options.length - 1; i >= 0; i--) {
if (!CollectionType.is_subset(type, options[i].value)) { if (valid_type_options.indexOf(options[i].value) < 0) {
options.remove(i); options.remove(i);
} }
} }
} }
function read_form() { function read_form() {
if(!edit){
cleanHREFinput();
let newhreftxtvalue = href_form.value.trim().toLowerCase();
if(!isValidHREF(newhreftxtvalue)){
alert("You must enter a valid HREF");
return false;
}
href = collection.href + "/" + newhreftxtvalue + "/";
}
displayname = displayname_form.value; displayname = displayname_form.value;
description = description_form.value; description = description_form.value;
source = source_form.value; source = source_form.value;
type = type_form.value; type = type_form.value;
color = color_form.value; color = color_form.value;
return true;
} }
function fill_form() { function fill_form() {
if(!edit){
href_form.value = random_uuid();
}
displayname_form.value = displayname; displayname_form.value = displayname;
description_form.value = description; description_form.value = description;
source_form.value = source; source_form.value = source;
@ -1095,7 +1168,9 @@ function CreateEditCollectionScene(user, password, collection) {
function onsubmit() { function onsubmit() {
try { try {
read_form(); if(!read_form()){
return false;
}
let sane_color = color.trim(); let sane_color = color.trim();
if (sane_color) { if (sane_color) {
let color_match = COLOR_RE.exec(sane_color); let color_match = COLOR_RE.exec(sane_color);
@ -1108,7 +1183,7 @@ function CreateEditCollectionScene(user, password, collection) {
} }
let loading_scene = new LoadingScene(); let loading_scene = new LoadingScene();
push_scene(loading_scene); push_scene(loading_scene);
let collection = new Collection(href, type, displayname, description, sane_color, source); let collection = new Collection(href, type, displayname, description, sane_color, 0, source);
let callback = function(error1) { let callback = function(error1) {
if (scene_index === null) { if (scene_index === null) {
return; return;
@ -1141,6 +1216,13 @@ function CreateEditCollectionScene(user, password, collection) {
return false; return false;
} }
function cleanHREFinput(event){
let currentTxtVal = href_form.value.trim().toLowerCase();
//Clean the HREF to remove non lowercase letters and dashes
currentTxtVal = currentTxtVal.replace(/(?![0-9a-z\-\_])./g, '');
href_form.value = currentTxtVal;
}
function onTypeChange(e){ function onTypeChange(e){
if(type_form.value == CollectionType.WEBCAL){ if(type_form.value == CollectionType.WEBCAL){
source_label.classList.remove("hidden"); source_label.classList.remove("hidden");
@ -1151,6 +1233,17 @@ function CreateEditCollectionScene(user, password, collection) {
} }
} }
function isValidHREF(href){
if(href.length < 1){
return false;
}
if(href.indexOf("/") != -1){
return false;
}
return true;
}
this.show = function() { this.show = function() {
this.release(); this.release();
scene_index = scene_stack.length - 1; scene_index = scene_stack.length - 1;

View file

@ -62,6 +62,7 @@
<span data-name="TASKS">Tasks</span> <span data-name="TASKS">Tasks</span>
<span data-name="WEBCAL">Webcal</span> <span data-name="WEBCAL">Webcal</span>
</small> </small>
<small data-name="contentcount"></small>
<input type="text" data-name="url" value="" readonly="" onfocus="this.setSelectionRange(0, 99999);"> <input type="text" data-name="url" value="" readonly="" onfocus="this.setSelectionRange(0, 99999);">
<p data-name="description" style="word-wrap:break-word;">Description</p> <p data-name="description" style="word-wrap:break-word;">Description</p>
<ul> <ul>
@ -131,6 +132,8 @@
<option value="TASKS">Tasks</option> <option value="TASKS">Tasks</option>
<option value="WEBCAL">Webcal</option> <option value="WEBCAL">Webcal</option>
</select> </select>
<label for="href">HREF:</label>
<input data-name="href" type="text">
<label for="displayname">Title:</label> <label for="displayname">Title:</label>
<input data-name="displayname" type="text"> <input data-name="displayname" type="text">
<label for="description">Description:</label> <label for="description">Description:</label>
@ -164,7 +167,8 @@
<section id="deletecollectionscene" class="container hidden"> <section id="deletecollectionscene" class="container hidden">
<h1>Delete Collection</h1> <h1>Delete Collection</h1>
<p>Do you want to delete the collection <span class="title" data-name="title">title</span>? </p> <p>To delete the collection <span class="title" data-name="title">title</span> please enter the phrase <strong data-name="deleteconfirmationtext"></strong> in the box below:</p>
<input type="text" class="deleteconfirmationtxt" data-name="confirmationtxt" />
<p class="red">WARNING: This action cannot be reversed.</p> <p class="red">WARNING: This action cannot be reversed.</p>
<form> <form>
<button type="button" class="red" data-name="delete">Delete</button> <button type="button" class="red" data-name="delete">Delete</button>