Skip to content

Commit ed64327

Browse files
committed
[WEB UI] New WebUI Improvements
Added WebUI improvements as discussed in discussion #1416 in March 2024.
1 parent 825464f commit ed64327

File tree

3 files changed

+114
-6
lines changed

3 files changed

+114
-6
lines changed

radicale/web/internal_data/css/main.css

+11
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@ main{
196196
text-align: center;
197197
}
198198

199+
#collectionsscene article small[data-name=contentcount]{
200+
font-weight: bold;
201+
font-style: normal;
202+
}
203+
199204
#editcollectionscene p span{
200205
word-wrap:break-word;
201206
font-weight: bold;
@@ -228,6 +233,12 @@ main{
228233
margin-top: 15px;
229234
}
230235

236+
.deleteconfirmationtxt{
237+
text-align: center;
238+
font-size: 1em;
239+
font-weight: bold;
240+
}
241+
231242
.fabcontainer{
232243
display: flex;
233244
flex-direction: column-reverse;

radicale/web/internal_data/fn.js

+98-5
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ const ROOT_PATH = location.pathname.replace(new RegExp("/+[^/]+/*(/index\\.html?
3636
*/
3737
const COLOR_RE = new RegExp("^(#[0-9A-Fa-f]{6})(?:[0-9A-Fa-f]{2})?$");
3838

39+
40+
/**
41+
* The text needed to confirm deleting a collection
42+
* @const
43+
*/
44+
const DELETE_CONFIRMATION_TEXT = "DELETE";
45+
3946
/**
4047
* Escape string for usage in XML
4148
* @param {string} s
@@ -94,6 +101,23 @@ const CollectionType = {
94101
union.push(this.WEBCAL);
95102
}
96103
return union.join("_");
104+
},
105+
valid_options_for_type: function(a){
106+
a = a.trim().toUpperCase();
107+
switch(a){
108+
case CollectionType.CALENDAR_JOURNAL_TASKS:
109+
case CollectionType.CALENDAR_JOURNAL:
110+
case CollectionType.CALENDAR_TASKS:
111+
case CollectionType.JOURNAL_TASKS:
112+
case CollectionType.CALENDAR:
113+
case CollectionType.JOURNAL:
114+
case CollectionType.TASKS:
115+
return [CollectionType.CALENDAR_JOURNAL_TASKS, CollectionType.CALENDAR_JOURNAL, CollectionType.CALENDAR_TASKS, CollectionType.JOURNAL_TASKS, CollectionType.CALENDAR, CollectionType.JOURNAL, CollectionType.TASKS];
116+
case CollectionType.ADDRESSBOOK:
117+
case CollectionType.WEBCAL:
118+
default:
119+
return [a];
120+
}
97121
}
98122
};
99123

@@ -106,13 +130,14 @@ const CollectionType = {
106130
* @param {string} description
107131
* @param {string} color
108132
*/
109-
function Collection(href, type, displayname, description, color, source) {
133+
function Collection(href, type, displayname, description, color, contentcount, source) {
110134
this.href = href;
111135
this.type = type;
112136
this.displayname = displayname;
113137
this.color = color;
114138
this.description = description;
115139
this.source = source;
140+
this.contentcount = contentcount;
116141
}
117142

118143
/**
@@ -139,6 +164,7 @@ function get_principal(user, password, callback) {
139164
CollectionType.PRINCIPAL,
140165
displayname_element ? displayname_element.textContent : "",
141166
"",
167+
0,
142168
""), null);
143169
} else {
144170
callback(null, "Internal error");
@@ -188,6 +214,7 @@ function get_collections(user, password, collection, callback) {
188214
let addressbookcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-color");
189215
let calendardesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-description");
190216
let addressbookdesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-description");
217+
let contentcount_element = response.querySelector(response_query + " > *|propstat > *|prop > *|getcontentcount");
191218
let webcalsource_element = response.querySelector(response_query + " > *|propstat > *|prop > *|source");
192219
let components_query = response_query + " > *|propstat > *|prop > *|supported-calendar-component-set";
193220
let components_element = response.querySelector(components_query);
@@ -197,11 +224,13 @@ function get_collections(user, password, collection, callback) {
197224
let color = "";
198225
let description = "";
199226
let source = "";
227+
let count = 0;
200228
if (resourcetype_element) {
201229
if (resourcetype_element.querySelector(resourcetype_query + " > *|addressbook")) {
202230
type = CollectionType.ADDRESSBOOK;
203231
color = addressbookcolor_element ? addressbookcolor_element.textContent : "";
204232
description = addressbookdesc_element ? addressbookdesc_element.textContent : "";
233+
count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
205234
} else if (resourcetype_element.querySelector(resourcetype_query + " > *|subscribed")) {
206235
type = CollectionType.WEBCAL;
207236
source = webcalsource_element ? webcalsource_element.textContent : "";
@@ -221,6 +250,7 @@ function get_collections(user, password, collection, callback) {
221250
}
222251
color = calendarcolor_element ? calendarcolor_element.textContent : "";
223252
description = calendardesc_element ? calendardesc_element.textContent : "";
253+
count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
224254
}
225255
}
226256
let sane_color = color.trim();
@@ -233,7 +263,7 @@ function get_collections(user, password, collection, callback) {
233263
}
234264
}
235265
if (href.substr(-1) === "/" && href !== collection.href && type) {
236-
collections.push(new Collection(href, type, displayname, description, sane_color, source));
266+
collections.push(new Collection(href, type, displayname, description, sane_color, count, source));
237267
}
238268
}
239269
collections.sort(function(a, b) {
@@ -265,6 +295,7 @@ function get_collections(user, password, collection, callback) {
265295
'<C:supported-calendar-component-set />' +
266296
'<CR:addressbook-description />' +
267297
'<CS:source />' +
298+
'<RADICALE:getcontentcount />' +
268299
'</prop>' +
269300
'</propfind>');
270301
return request;
@@ -708,6 +739,7 @@ function CollectionsScene(user, password, collection, onerror) {
708739
node.classList.remove("hidden");
709740
let title_form = node.querySelector("[data-name=title]");
710741
let description_form = node.querySelector("[data-name=description]");
742+
let contentcount_form = node.querySelector("[data-name=contentcount]");
711743
let url_form = node.querySelector("[data-name=url]");
712744
let color_form = node.querySelector("[data-name=color]");
713745
let delete_btn = node.querySelector("[data-name=delete]");
@@ -739,6 +771,9 @@ function CollectionsScene(user, password, collection, onerror) {
739771
if(description_form.textContent.length > 150){
740772
description_form.classList.add("smalltext");
741773
}
774+
if(collection.type != CollectionType.WEBCAL){
775+
contentcount_form.textContent = (collection.contentcount > 0 ? collection.contentcount : "No") + " item" + (collection.contentcount == 1 ? "" : "s") + " in collection";
776+
}
742777
let href = SERVER + collection.href;
743778
url_form.value = href;
744779
download_btn.href = href;
@@ -939,14 +974,25 @@ function DeleteCollectionScene(user, password, collection) {
939974
let html_scene = document.getElementById("deletecollectionscene");
940975
let title_form = html_scene.querySelector("[data-name=title]");
941976
let error_form = html_scene.querySelector("[data-name=error]");
977+
let confirmation_txt = html_scene.querySelector("[data-name=confirmationtxt]");
978+
let delete_confirmation_lbl = html_scene.querySelector("[data-name=deleteconfirmationtext]");
942979
let delete_btn = html_scene.querySelector("[data-name=delete]");
943980
let cancel_btn = html_scene.querySelector("[data-name=cancel]");
944981

982+
delete_confirmation_lbl.innerHTML = DELETE_CONFIRMATION_TEXT;
983+
confirmation_txt.value = "";
984+
confirmation_txt.addEventListener("keydown", onkeydown);
985+
945986
/** @type {?number} */ let scene_index = null;
946987
/** @type {?XMLHttpRequest} */ let delete_req = null;
947988
let error = "";
948989

949990
function ondelete() {
991+
let confirmation_text_value = confirmation_txt.value;
992+
if(confirmation_text_value != DELETE_CONFIRMATION_TEXT){
993+
alert("Please type the confirmation text to delete this collection.");
994+
return;
995+
}
950996
try {
951997
let loading_scene = new LoadingScene();
952998
push_scene(loading_scene);
@@ -977,6 +1023,13 @@ function DeleteCollectionScene(user, password, collection) {
9771023
return false;
9781024
}
9791025

1026+
function onkeydown(event){
1027+
if (event.keyCode !== 13) {
1028+
return;
1029+
}
1030+
ondelete();
1031+
}
1032+
9801033
this.show = function() {
9811034
this.release();
9821035
scene_index = scene_stack.length - 1;
@@ -1031,6 +1084,8 @@ function CreateEditCollectionScene(user, password, collection) {
10311084
let html_scene = document.getElementById(edit ? "editcollectionscene" : "createcollectionscene");
10321085
let title_form = edit ? html_scene.querySelector("[data-name=title]") : null;
10331086
let error_form = html_scene.querySelector("[data-name=error]");
1087+
let href_form = html_scene.querySelector("[data-name=href]");
1088+
let href_label = html_scene.querySelector("label[for=href]");
10341089
let displayname_form = html_scene.querySelector("[data-name=displayname]");
10351090
let displayname_label = html_scene.querySelector("label[for=displayname]");
10361091
let description_form = html_scene.querySelector("[data-name=description]");
@@ -1057,28 +1112,46 @@ function CreateEditCollectionScene(user, password, collection) {
10571112
let type = edit ? collection.type : CollectionType.CALENDAR_JOURNAL_TASKS;
10581113
let color = edit && collection.color ? collection.color : "#" + random_hex(6);
10591114

1115+
if(!edit){
1116+
href_form.addEventListener("keydown", cleanHREFinput);
1117+
}
1118+
10601119
function remove_invalid_types() {
10611120
if (!edit) {
10621121
return;
10631122
}
10641123
/** @type {HTMLOptionsCollection} */ let options = type_form.options;
10651124
// remove all options that are not supersets
1125+
let valid_type_options = CollectionType.valid_options_for_type(type);
10661126
for (let i = options.length - 1; i >= 0; i--) {
1067-
if (!CollectionType.is_subset(type, options[i].value)) {
1127+
if (valid_type_options.indexOf(options[i].value) < 0) {
10681128
options.remove(i);
10691129
}
10701130
}
10711131
}
10721132

10731133
function read_form() {
1134+
if(!edit){
1135+
cleanHREFinput();
1136+
let newhreftxtvalue = href_form.value.trim().toLowerCase();
1137+
if(!isValidHREF(newhreftxtvalue)){
1138+
alert("You must enter a valid HREF");
1139+
return false;
1140+
}
1141+
href = collection.href + "/" + newhreftxtvalue + "/";
1142+
}
10741143
displayname = displayname_form.value;
10751144
description = description_form.value;
10761145
source = source_form.value;
10771146
type = type_form.value;
10781147
color = color_form.value;
1148+
return true;
10791149
}
10801150

10811151
function fill_form() {
1152+
if(!edit){
1153+
href_form.value = random_uuid();
1154+
}
10821155
displayname_form.value = displayname;
10831156
description_form.value = description;
10841157
source_form.value = source;
@@ -1095,7 +1168,9 @@ function CreateEditCollectionScene(user, password, collection) {
10951168

10961169
function onsubmit() {
10971170
try {
1098-
read_form();
1171+
if(!read_form()){
1172+
return false;
1173+
}
10991174
let sane_color = color.trim();
11001175
if (sane_color) {
11011176
let color_match = COLOR_RE.exec(sane_color);
@@ -1108,7 +1183,7 @@ function CreateEditCollectionScene(user, password, collection) {
11081183
}
11091184
let loading_scene = new LoadingScene();
11101185
push_scene(loading_scene);
1111-
let collection = new Collection(href, type, displayname, description, sane_color, source);
1186+
let collection = new Collection(href, type, displayname, description, sane_color, 0, source);
11121187
let callback = function(error1) {
11131188
if (scene_index === null) {
11141189
return;
@@ -1141,6 +1216,13 @@ function CreateEditCollectionScene(user, password, collection) {
11411216
return false;
11421217
}
11431218

1219+
function cleanHREFinput(event){
1220+
let currentTxtVal = href_form.value.trim().toLowerCase();
1221+
//Clean the HREF to remove non lowercase letters and dashes
1222+
currentTxtVal = currentTxtVal.replace(/(?![0-9a-z\-\_])./g, '');
1223+
href_form.value = currentTxtVal;
1224+
}
1225+
11441226
function onTypeChange(e){
11451227
if(type_form.value == CollectionType.WEBCAL){
11461228
source_label.classList.remove("hidden");
@@ -1151,6 +1233,17 @@ function CreateEditCollectionScene(user, password, collection) {
11511233
}
11521234
}
11531235

1236+
function isValidHREF(href){
1237+
if(href.length < 1){
1238+
return false;
1239+
}
1240+
if(href.indexOf("/") != -1){
1241+
return false;
1242+
}
1243+
1244+
return true;
1245+
}
1246+
11541247
this.show = function() {
11551248
this.release();
11561249
scene_index = scene_stack.length - 1;

radicale/web/internal_data/index.html

+5-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ <h3 class="title" data-name="title">Title</h3>
6262
<span data-name="TASKS">Tasks</span>
6363
<span data-name="WEBCAL">Webcal</span>
6464
</small>
65+
<small data-name="contentcount"></small>
6566
<input type="text" data-name="url" value="" readonly="" onfocus="this.setSelectionRange(0, 99999);">
6667
<p data-name="description" style="word-wrap:break-word;">Description</p>
6768
<ul>
@@ -131,6 +132,8 @@ <h1>Create a new Collection</h1>
131132
<option value="TASKS">Tasks</option>
132133
<option value="WEBCAL">Webcal</option>
133134
</select>
135+
<label for="href">HREF:</label>
136+
<input data-name="href" type="text">
134137
<label for="displayname">Title:</label>
135138
<input data-name="displayname" type="text">
136139
<label for="description">Description:</label>
@@ -164,7 +167,8 @@ <h1>Upload Collection</h1>
164167

165168
<section id="deletecollectionscene" class="container hidden">
166169
<h1>Delete Collection</h1>
167-
<p>Do you want to delete the collection <span class="title" data-name="title">title</span>? </p>
170+
<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>
171+
<input type="text" class="deleteconfirmationtxt" data-name="confirmationtxt" />
168172
<p class="red">WARNING: This action cannot be reversed.</p>
169173
<form>
170174
<button type="button" class="red" data-name="delete">Delete</button>

0 commit comments

Comments
 (0)