Merge pull request #2042 from maxberger/master
Addess JS Linter issues due to TSC update
This commit is contained in:
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
@@ -191,17 +191,7 @@ jobs:
|
||||
node-version: 24
|
||||
- name: JS Type Check
|
||||
run: |
|
||||
set -o pipefail
|
||||
npx -p typescript tsc -p radicale/web/jsconfig.json | npx typescript-xunit-xml > tsc-results.xml
|
||||
- uses: mikepenz/action-junit-report@v6
|
||||
if: ${{ failure() && (github.event.pull_request.head.repo.full_name != github.repository) }}
|
||||
with:
|
||||
report_paths: 'tsc-results.xml'
|
||||
annotate_only: true # forked repo cannot write to checks so just do annotations
|
||||
- uses: mikepenz/action-junit-report@v6
|
||||
if: ${{ always() && github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
with:
|
||||
report_paths: 'tsc-results.xml'
|
||||
npx -p typescript tsc -p radicale/web/jsconfig.json
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -230,13 +220,4 @@ jobs:
|
||||
- name: Install Playwright Browsers
|
||||
run: uv run --extra integ_test playwright install --with-deps
|
||||
- name: Run Integration Tests
|
||||
run: uv run --extra integ_test pytest --junitxml=pytest-results.xml integ_tests
|
||||
- uses: mikepenz/action-junit-report@v6
|
||||
if: ${{ failure() && (github.event.pull_request.head.repo.full_name != github.repository) }}
|
||||
with:
|
||||
report_paths: 'pytest-results.xml'
|
||||
annotate_only: true # forked repo cannot write to checks so just do annotations
|
||||
- uses: mikepenz/action-junit-report@v6
|
||||
if: ${{ always() && github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
with:
|
||||
report_paths: 'pytest-results.xml'
|
||||
run: uv run --extra integ_test pytest integ_tests/ -v
|
||||
|
||||
@@ -26,8 +26,8 @@ import { create_request, to_error_message } from "./common.js";
|
||||
|
||||
/**
|
||||
* Find the principal collection.
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} user
|
||||
* @param {?string} password
|
||||
* @param {function(?Collection, ?string):void} callback Returns result or error
|
||||
* @return {XMLHttpRequest}
|
||||
*/
|
||||
@@ -39,18 +39,22 @@ export function get_principal(user, password, callback) {
|
||||
}
|
||||
if (request.status === 207) {
|
||||
let xml = request.responseXML;
|
||||
let principal_element = xml.querySelector("*|multistatus:root > *|response:first-of-type > *|propstat > *|prop > *|current-user-principal > *|href");
|
||||
let displayname_element = xml.querySelector("*|multistatus:root > *|response:first-of-type > *|propstat > *|prop > *|displayname");
|
||||
if (principal_element) {
|
||||
callback(new Collection(
|
||||
principal_element.textContent,
|
||||
CollectionType.PRINCIPAL,
|
||||
displayname_element ? displayname_element.textContent : "",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
""), null);
|
||||
if (xml) {
|
||||
let principal_element = xml.querySelector("*|multistatus:root > *|response:first-of-type > *|propstat > *|prop > *|current-user-principal > *|href");
|
||||
let displayname_element = xml.querySelector("*|multistatus:root > *|response:first-of-type > *|propstat > *|prop > *|displayname");
|
||||
if (principal_element) {
|
||||
callback(new Collection(
|
||||
principal_element.textContent,
|
||||
CollectionType.PRINCIPAL,
|
||||
displayname_element ? displayname_element.textContent : "",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
""), null);
|
||||
} else {
|
||||
callback(null, "No valid XML received")
|
||||
}
|
||||
} else {
|
||||
callback(null, "Internal error");
|
||||
}
|
||||
@@ -71,7 +75,7 @@ export function get_principal(user, password, callback) {
|
||||
/**
|
||||
* Find all calendars and addressbooks in collection.
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Collection} collection
|
||||
* @param {function(?Array<Collection>, ?string):void} callback Returns result or error
|
||||
* @return {XMLHttpRequest}
|
||||
@@ -85,81 +89,86 @@ export function get_collections(user, password, collection, callback) {
|
||||
}
|
||||
if (request.status === 207) {
|
||||
let xml = request.responseXML;
|
||||
let collections = [];
|
||||
let response_query = "*|multistatus:root > *|response";
|
||||
let responses = xml.querySelectorAll(response_query);
|
||||
for (let i = 0; i < responses.length; i++) {
|
||||
let response = responses[i];
|
||||
let href_element = response.querySelector(response_query + " > *|href");
|
||||
let resourcetype_query = response_query + " > *|propstat > *|prop > *|resourcetype";
|
||||
let resourcetype_element = response.querySelector(resourcetype_query);
|
||||
let displayname_element = response.querySelector(response_query + " > *|propstat > *|prop > *|displayname");
|
||||
let calendarcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-color");
|
||||
let addressbookcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-color");
|
||||
let calendardesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-description");
|
||||
let addressbookdesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-description");
|
||||
let contentcount_element = response.querySelector(response_query + " > *|propstat > *|prop > *|getcontentcount");
|
||||
let contentlength_element = response.querySelector(response_query + " > *|propstat > *|prop > *|getcontentlength");
|
||||
let webcalsource_element = response.querySelector(response_query + " > *|propstat > *|prop > *|source");
|
||||
let components_query = response_query + " > *|propstat > *|prop > *|supported-calendar-component-set";
|
||||
let components_element = response.querySelector(components_query);
|
||||
let href = href_element ? href_element.textContent : "";
|
||||
let displayname = displayname_element ? displayname_element.textContent : "";
|
||||
let type = "";
|
||||
let color = "";
|
||||
let description = "";
|
||||
let source = "";
|
||||
let count = 0;
|
||||
let size = 0;
|
||||
if (resourcetype_element) {
|
||||
if (resourcetype_element.querySelector(resourcetype_query + " > *|addressbook")) {
|
||||
type = CollectionType.ADDRESSBOOK;
|
||||
color = addressbookcolor_element ? addressbookcolor_element.textContent : "";
|
||||
description = addressbookdesc_element ? addressbookdesc_element.textContent : "";
|
||||
count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
|
||||
size = contentlength_element ? parseInt(contentlength_element.textContent) : 0;
|
||||
} else if (resourcetype_element.querySelector(resourcetype_query + " > *|subscribed")) {
|
||||
type = CollectionType.WEBCAL;
|
||||
source = webcalsource_element ? webcalsource_element.textContent : "";
|
||||
color = calendarcolor_element ? calendarcolor_element.textContent : "";
|
||||
description = calendardesc_element ? calendardesc_element.textContent : "";
|
||||
} else if (resourcetype_element.querySelector(resourcetype_query + " > *|calendar")) {
|
||||
if (components_element) {
|
||||
if (components_element.querySelector(components_query + " > *|comp[name=VEVENT]")) {
|
||||
type = CollectionType.union(type, CollectionType.CALENDAR);
|
||||
}
|
||||
if (components_element.querySelector(components_query + " > *|comp[name=VJOURNAL]")) {
|
||||
type = CollectionType.union(type, CollectionType.JOURNAL);
|
||||
}
|
||||
if (components_element.querySelector(components_query + " > *|comp[name=VTODO]")) {
|
||||
type = CollectionType.union(type, CollectionType.TASKS);
|
||||
if (xml) {
|
||||
let collections = [];
|
||||
let response_query = "*|multistatus:root > *|response";
|
||||
let responses = xml.querySelectorAll(response_query);
|
||||
for (let i = 0; i < responses.length; i++) {
|
||||
let response = responses[i];
|
||||
let href_element = response.querySelector(response_query + " > *|href");
|
||||
let resourcetype_query = response_query + " > *|propstat > *|prop > *|resourcetype";
|
||||
let resourcetype_element = response.querySelector(resourcetype_query);
|
||||
let displayname_element = response.querySelector(response_query + " > *|propstat > *|prop > *|displayname");
|
||||
let calendarcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-color");
|
||||
let addressbookcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-color");
|
||||
let calendardesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-description");
|
||||
let addressbookdesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-description");
|
||||
let contentcount_element = response.querySelector(response_query + " > *|propstat > *|prop > *|getcontentcount");
|
||||
let contentlength_element = response.querySelector(response_query + " > *|propstat > *|prop > *|getcontentlength");
|
||||
let webcalsource_element = response.querySelector(response_query + " > *|propstat > *|prop > *|source");
|
||||
let components_query = response_query + " > *|propstat > *|prop > *|supported-calendar-component-set";
|
||||
let components_element = response.querySelector(components_query);
|
||||
let href = href_element ? href_element.textContent : "";
|
||||
let displayname = displayname_element ? displayname_element.textContent : "";
|
||||
let type = "";
|
||||
let color = "";
|
||||
let description = "";
|
||||
let source = "";
|
||||
let count = 0;
|
||||
let size = 0;
|
||||
if (resourcetype_element) {
|
||||
if (resourcetype_element.querySelector(resourcetype_query + " > *|addressbook")) {
|
||||
type = CollectionType.ADDRESSBOOK;
|
||||
color = addressbookcolor_element ? addressbookcolor_element.textContent : "";
|
||||
description = addressbookdesc_element ? addressbookdesc_element.textContent : "";
|
||||
count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
|
||||
size = contentlength_element ? parseInt(contentlength_element.textContent) : 0;
|
||||
} else if (resourcetype_element.querySelector(resourcetype_query + " > *|subscribed")) {
|
||||
type = CollectionType.WEBCAL;
|
||||
source = webcalsource_element ? webcalsource_element.textContent : "";
|
||||
color = calendarcolor_element ? calendarcolor_element.textContent : "";
|
||||
description = calendardesc_element ? calendardesc_element.textContent : "";
|
||||
} else if (resourcetype_element.querySelector(resourcetype_query + " > *|calendar")) {
|
||||
if (components_element) {
|
||||
if (components_element.querySelector(components_query + " > *|comp[name=VEVENT]")) {
|
||||
type = CollectionType.union(type, CollectionType.CALENDAR);
|
||||
}
|
||||
if (components_element.querySelector(components_query + " > *|comp[name=VJOURNAL]")) {
|
||||
type = CollectionType.union(type, CollectionType.JOURNAL);
|
||||
}
|
||||
if (components_element.querySelector(components_query + " > *|comp[name=VTODO]")) {
|
||||
type = CollectionType.union(type, CollectionType.TASKS);
|
||||
}
|
||||
}
|
||||
color = calendarcolor_element ? calendarcolor_element.textContent : "";
|
||||
description = calendardesc_element ? calendardesc_element.textContent : "";
|
||||
count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
|
||||
size = contentlength_element ? parseInt(contentlength_element.textContent) : 0;
|
||||
}
|
||||
color = calendarcolor_element ? calendarcolor_element.textContent : "";
|
||||
description = calendardesc_element ? calendardesc_element.textContent : "";
|
||||
count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
|
||||
size = contentlength_element ? parseInt(contentlength_element.textContent) : 0;
|
||||
}
|
||||
let sane_color = color.trim();
|
||||
if (sane_color) {
|
||||
let color_match = COLOR_RE.exec(sane_color);
|
||||
if (color_match) {
|
||||
sane_color = color_match[1];
|
||||
} else {
|
||||
sane_color = "";
|
||||
}
|
||||
}
|
||||
if (href.substr(-1) === "/" && href !== collection.href && type) {
|
||||
collections.push(new Collection(href, type, displayname, description, sane_color, count, size, source));
|
||||
}
|
||||
}
|
||||
let sane_color = color.trim();
|
||||
if (sane_color) {
|
||||
let color_match = COLOR_RE.exec(sane_color);
|
||||
if (color_match) {
|
||||
sane_color = color_match[1];
|
||||
} else {
|
||||
sane_color = "";
|
||||
}
|
||||
}
|
||||
if (href.substr(-1) === "/" && href !== collection.href && type) {
|
||||
collections.push(new Collection(href, type, displayname, description, sane_color, count, size, source));
|
||||
}
|
||||
}
|
||||
collections.sort(function (a, b) {
|
||||
collections.sort(function (a, b) {
|
||||
/** @type {string} */ let ca = a.displayname || a.href;
|
||||
/** @type {string} */ let cb = b.displayname || b.href;
|
||||
return ca.localeCompare(cb);
|
||||
});
|
||||
callback(collections, null);
|
||||
return ca.localeCompare(cb);
|
||||
});
|
||||
callback(collections, null);
|
||||
} else {
|
||||
callback(null, "No valid XML received")
|
||||
}
|
||||
|
||||
} else {
|
||||
callback(null, to_error_message(request));
|
||||
}
|
||||
@@ -192,7 +201,7 @@ export function get_collections(user, password, collection, callback) {
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {string} collection_href Must always start and end with /.
|
||||
* @param {File} file
|
||||
* @param {function(?string):void} callback Returns error or null
|
||||
@@ -217,7 +226,7 @@ export function upload_collection(user, password, collection_href, file, callbac
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Collection} collection
|
||||
* @param {function(?string):void} callback Returns error or null
|
||||
* @return {XMLHttpRequest}
|
||||
@@ -240,7 +249,7 @@ export function delete_collection(user, password, collection, callback) {
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Collection} collection
|
||||
* @param {boolean} create
|
||||
* @param {function(?string):void} callback Returns error or null
|
||||
@@ -320,7 +329,7 @@ function create_edit_collection(user, password, collection, create, callback) {
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Collection} collection
|
||||
* @param {function(?string):void} callback Returns error or null
|
||||
* @return {XMLHttpRequest}
|
||||
@@ -331,7 +340,7 @@ export function create_collection(user, password, collection, callback) {
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Collection} collection
|
||||
* @param {function(?string):void} callback Returns error or null
|
||||
* @return {XMLHttpRequest}
|
||||
|
||||
@@ -39,12 +39,12 @@ import { create_request, to_error_message } from "./common.js";
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {string} path
|
||||
* @param {object} body
|
||||
* @param {function(string):void} on_success
|
||||
* @param {function():void} on_not_found
|
||||
* @param {function(string):void} on_error
|
||||
* @param {?function():void} on_not_found
|
||||
* @param {?function(string):void} on_error
|
||||
* @returns {XMLHttpRequest}
|
||||
*/
|
||||
function call_sharing_api(
|
||||
@@ -92,7 +92,7 @@ function call_sharing_api(
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {function(import("../api/sharing.js").ServerFeatures, ?string):void} callback
|
||||
*/
|
||||
export function discover_server_features(user, password, callback) {
|
||||
@@ -106,7 +106,11 @@ export function discover_server_features(user, password, callback) {
|
||||
let features = { "sharing": JSON.parse(response) };
|
||||
callback(features, null);
|
||||
} catch (e) {
|
||||
callback({}, e.message);
|
||||
if (e instanceof Error) {
|
||||
callback({}, e.message);
|
||||
} else {
|
||||
callback({}, e ? e.toString() : "Unknown error");
|
||||
}
|
||||
}
|
||||
},
|
||||
function () {
|
||||
@@ -119,9 +123,29 @@ export function discover_server_features(user, password, callback) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} ShareData
|
||||
* @property {string} [ShareType]
|
||||
* @property {string} [PathOrToken]
|
||||
* @property {string} [PathMapped]
|
||||
* @property {string} [Owner]
|
||||
* @property {string} [User]
|
||||
* @property {string} [Permissions]
|
||||
* @property {?boolean} [EnabledByOwner]
|
||||
* @property {?boolean} [EnabledByUser]
|
||||
* @property {?boolean} [Enabled]
|
||||
* @property {?boolean} [HiddenByOwner]
|
||||
* @property {?boolean} [HiddenByUser]
|
||||
* @property {?boolean} [Hidden]
|
||||
* @property {number} [TimestampCreated]
|
||||
* @property {number} [TimestampUpdated]
|
||||
* @property {Object<String, String>} [Properties]
|
||||
*/
|
||||
|
||||
|
||||
export class Share {
|
||||
/**
|
||||
* @param {Object} [data]
|
||||
* @param {ShareData} [data]
|
||||
*/
|
||||
constructor(data = {}) {
|
||||
/** @type {string} */ this.ShareType = data.ShareType || "";
|
||||
@@ -130,20 +154,20 @@ export class Share {
|
||||
/** @type {string} */ this.Owner = data.Owner || "";
|
||||
/** @type {string} */ this.User = data.User || "";
|
||||
/** @type {string} */ this.Permissions = data.Permissions || "r";
|
||||
/** @type {boolean} */ this.EnabledByOwner = data.EnabledByOwner ?? data.Enabled ?? false;
|
||||
/** @type {?boolean} */ this.EnabledByOwner = data.EnabledByOwner ?? data.Enabled ?? false;
|
||||
/** @type {?boolean} */ this.EnabledByUser = data.EnabledByUser ?? data.Enabled ?? null;
|
||||
/** @type {boolean} */ this.HiddenByOwner = data.HiddenByOwner ?? data.Hidden ?? false;
|
||||
/** @type {?boolean} */ this.HiddenByOwner = data.HiddenByOwner ?? data.Hidden ?? false;
|
||||
/** @type {?boolean} */ this.HiddenByUser = data.HiddenByUser ?? data.Hidden ?? null;
|
||||
/** @type {number} */ this.TimestampCreated = data.TimestampCreated || 0;
|
||||
/** @type {number} */ this.TimestampUpdated = data.TimestampUpdated || 0;
|
||||
/** @type {Object} */ this.Properties = data.Properties || {};
|
||||
/** @type {Object<String, String>} */ this.Properties = data.Properties || {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {import("../models/collection.js").Collection} collection
|
||||
* @param {?string} password
|
||||
* @param {?import("../models/collection.js").Collection} collection
|
||||
* @param {function(Array<Share>, ?string):void} callback
|
||||
*/
|
||||
export function reload_sharing_list(user, password, collection, callback) {
|
||||
@@ -155,7 +179,7 @@ export function reload_sharing_list(user, password, collection, callback) {
|
||||
body,
|
||||
function (response) {
|
||||
let parsed = JSON.parse(response);
|
||||
let shares = (parsed["Content"] || []).map(data => new Share(data));
|
||||
let shares = (parsed["Content"] || []).map((/** @type {ShareData} */ data) => new Share(data));
|
||||
callback(shares, null);
|
||||
},
|
||||
null, // on_not_found
|
||||
@@ -200,7 +224,7 @@ export function get_property_key(type, property) {
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Share} share
|
||||
* @param {function(?string):void} callback
|
||||
*/
|
||||
@@ -238,7 +262,7 @@ export function add_share_by_token(
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Share} share
|
||||
* @param {function(?string):void} callback
|
||||
*/
|
||||
@@ -278,7 +302,7 @@ export function add_share_by_map(
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Share} share
|
||||
* @param {function(?string):void} callback
|
||||
*/
|
||||
@@ -310,7 +334,7 @@ export function delete_share_by_token(
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Share} share
|
||||
* @param {function(?string):void} callback
|
||||
*/
|
||||
@@ -341,7 +365,7 @@ export function delete_share_by_map(
|
||||
}
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Share} share
|
||||
* @param {function(?string):void} callback
|
||||
*/
|
||||
@@ -379,7 +403,7 @@ export function update_share_by_token(
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Share} share
|
||||
* @param {function(?string):void} callback
|
||||
*/
|
||||
@@ -421,7 +445,7 @@ export function update_share_by_map(
|
||||
* Update a shared map entry as the recipient user.
|
||||
* Only sends fields the non-owner user is allowed to change: PathOrToken, EnabledByUser, HiddenByUser.
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Share} share
|
||||
* @param {function(?string):void} callback
|
||||
*/
|
||||
@@ -457,7 +481,7 @@ export function update_incoming_share(
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Share} share
|
||||
* @param {function(?string):void} callback
|
||||
*/
|
||||
@@ -497,7 +521,7 @@ export function add_share_by_bday(
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Share} share
|
||||
* @param {function(?string):void} callback
|
||||
*/
|
||||
@@ -529,7 +553,7 @@ export function delete_share_by_bday(
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Share} share
|
||||
* @param {function(?string):void} callback
|
||||
*/
|
||||
|
||||
@@ -36,6 +36,7 @@ export const ROOT_PATH = location.pathname.replace(new RegExp("/+[^/]+/*(/index\
|
||||
/**
|
||||
* Regex to match and normalize color
|
||||
* @const
|
||||
* @type {RegExp}
|
||||
*/
|
||||
export const COLOR_RE = new RegExp("^(#[0-9A-Fa-f]{6})(?:[0-9A-Fa-f]{2})?$");
|
||||
|
||||
@@ -43,5 +44,6 @@ export const COLOR_RE = new RegExp("^(#[0-9A-Fa-f]{6})(?:[0-9A-Fa-f]{2})?$");
|
||||
/**
|
||||
* The text needed to confirm deleting a collection
|
||||
* @const
|
||||
* @type {string}
|
||||
*/
|
||||
export const DELETE_CONFIRMATION_TEXT = "DELETE";
|
||||
@@ -25,7 +25,7 @@ import { SERVER } from "../constants.js";
|
||||
import { Collection, CollectionType } from "../models/collection.js";
|
||||
import { collectionsCache } from "../utils/collections_cache.js";
|
||||
import { ErrorHandler } from "../utils/error.js";
|
||||
import { bytesToHumanReadable } from "../utils/misc.js";
|
||||
import { bytesToHumanReadable, get_element, get_element_by_id } from "../utils/misc.js";
|
||||
import { CreateEditCollectionScene } from "./CreateEditCollectionScene.js";
|
||||
import { DeleteConfirmationScene } from "./DeleteConfirmationScene.js";
|
||||
import { IncomingSharingScene } from "./IncomingSharingScene.js";
|
||||
@@ -39,18 +39,18 @@ import { UploadCollectionScene } from "./UploadCollectionScene.js";
|
||||
export class CollectionsScene {
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Collection} principal_collection The princial collection
|
||||
* @param {function(string):void} onerror Called when an error occurs, before the
|
||||
* @param {function(?string):void} onerror Called when an error occurs, before the
|
||||
* scene is popped.
|
||||
*/
|
||||
constructor(user, password, principal_collection, onerror) {
|
||||
/** @type {HTMLElement} */ let html_scene = document.getElementById("collectionsscene");
|
||||
/** @type {HTMLElement} */ let template = html_scene.querySelector("[data-name=collectiontemplate]");
|
||||
/** @type {HTMLElement} */ let new_btn = html_scene.querySelector("[data-name=new]");
|
||||
/** @type {HTMLElement} */ let upload_btn = html_scene.querySelector("[data-name=upload]");
|
||||
/** @type {HTMLElement} */ let incomingshares_btn = html_scene.querySelector("[data-name=incomingshares]");
|
||||
/** @type {HTMLElement} */ let error_div = html_scene.querySelector("[data-name=collectionsscene_error]");
|
||||
/** @type {HTMLElement} */ let html_scene = get_element_by_id("collectionsscene");
|
||||
/** @type {HTMLElement} */ let template = get_element(html_scene, "[data-name=collectiontemplate]");
|
||||
/** @type {HTMLElement} */ let new_btn = get_element(html_scene, "[data-name=new]");
|
||||
/** @type {HTMLElement} */ let upload_btn = get_element(html_scene, "[data-name=upload]");
|
||||
/** @type {HTMLElement} */ let incomingshares_btn = get_element(html_scene, "[data-name=incomingshares]");
|
||||
/** @type {HTMLElement} */ let error_div = get_element(html_scene, "[data-name=collectionsscene_error]");
|
||||
|
||||
/** @type {Array<HTMLElement>} */ let nodes = [];
|
||||
let errorHandler = new ErrorHandler(error_div);
|
||||
@@ -133,7 +133,7 @@ export class CollectionsScene {
|
||||
* @param {boolean} clear_error
|
||||
*/
|
||||
function show_collections(collections, shares, clear_error) {
|
||||
/** @type {HTMLElement} */ let navBar = document.querySelector("#logoutview");
|
||||
/** @type {HTMLElement} */ let navBar = get_element(document, "#logoutview");
|
||||
let heightOfNavBar = navBar.offsetHeight + "px";
|
||||
html_scene.style.marginTop = heightOfNavBar;
|
||||
html_scene.style.height = "calc(100vh - " + heightOfNavBar + ")";
|
||||
@@ -144,22 +144,24 @@ export class CollectionsScene {
|
||||
|
||||
// Clear old nodes
|
||||
nodes.forEach(function (node) {
|
||||
node.parentNode.removeChild(node);
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
});
|
||||
nodes = [];
|
||||
|
||||
collections.forEach(function (/** @type {Collection} */ collection) {
|
||||
/** @type {HTMLElement} */ let node = /** @type {HTMLElement} */(template.cloneNode(true));
|
||||
node.classList.remove("hidden");
|
||||
/** @type {HTMLElement} */ let title_form = node.querySelector("[data-name=title]");
|
||||
/** @type {HTMLElement} */ let description_form = node.querySelector("[data-name=description]");
|
||||
/** @type {HTMLElement} */ let contentcount_form = node.querySelector("[data-name=contentcount]");
|
||||
/** @type {HTMLInputElement} */ let url_form = node.querySelector("[data-name=url]");
|
||||
/** @type {HTMLElement} */ let color_form = node.querySelector("[data-name=color]");
|
||||
/** @type {HTMLElement} */ let delete_btn = node.querySelector("[data-name=delete]");
|
||||
/** @type {HTMLElement} */ let edit_btn = node.querySelector("[data-name=edit]");
|
||||
/** @type {HTMLElement} */ let share_btn = node.querySelector("[data-name=share]");
|
||||
/** @type {HTMLAnchorElement} */ let download_btn = node.querySelector("[data-name=download]");
|
||||
/** @type {HTMLElement} */ let title_form = get_element(node, "[data-name=title]");
|
||||
/** @type {HTMLElement} */ let description_form = get_element(node, "[data-name=description]");
|
||||
/** @type {HTMLElement} */ let contentcount_form = get_element(node, "[data-name=contentcount]");
|
||||
/** @type {HTMLInputElement} */ let url_form = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=url]"));
|
||||
/** @type {HTMLElement} */ let color_form = get_element(node, "[data-name=color]");
|
||||
/** @type {HTMLElement} */ let delete_btn = get_element(node, "[data-name=delete]");
|
||||
/** @type {HTMLElement} */ let edit_btn = get_element(node, "[data-name=edit]");
|
||||
/** @type {HTMLElement} */ let share_btn = get_element(node, "[data-name=share]");
|
||||
/** @type {HTMLAnchorElement} */ let download_btn = /** @type {HTMLAnchorElement} */ (get_element(node, "[data-name=download]"));
|
||||
if (collection.color) {
|
||||
color_form.style.background = collection.color;
|
||||
}
|
||||
@@ -175,22 +177,22 @@ export class CollectionsScene {
|
||||
});
|
||||
possible_types.forEach(function (e) {
|
||||
if (e !== collection.type) {
|
||||
node.querySelector("[data-name=" + e + "]").classList.add("hidden");
|
||||
get_element(node, "[data-name=" + e + "]").classList.add("hidden");
|
||||
}
|
||||
});
|
||||
let share_info = node.querySelector("[data-name=shared-by]");
|
||||
let transformed_from = node.querySelector("[data-name=transformed-from]");
|
||||
let share_info = get_element(node, "[data-name=shared-by]");
|
||||
let transformed_from = get_element(node, "[data-name=transformed-from]");
|
||||
let share = (shares || []).find(
|
||||
s => (s.ShareType === "map" || s.ShareType === "bday") &&
|
||||
(s.PathOrToken || "").replace(/\/+$/, "") === (collection.href || "").replace(/\/+$/, ""));
|
||||
if (share) {
|
||||
if (share.Owner !== user) {
|
||||
share_info.classList.remove("hidden");
|
||||
node.querySelector("[data-name=shared-by-owner]").textContent = share.Owner;
|
||||
get_element(node, "[data-name=shared-by-owner]").textContent = share.Owner;
|
||||
} else {
|
||||
transformed_from.classList.remove("hidden");
|
||||
}
|
||||
let share_option = node.querySelector("[data-name=shareoption]");
|
||||
let share_option = get_element(node, "[data-name=shareoption]");
|
||||
if (share_option) {
|
||||
share_option.classList.add("hidden");
|
||||
share_option.removeAttribute("data-name");
|
||||
@@ -223,10 +225,10 @@ export class CollectionsScene {
|
||||
download_btn.onclick = function (event) {
|
||||
event.preventDefault();
|
||||
let auth = get_auth_header(user, password);
|
||||
let headers = auth ? { 'Authorization': auth } : {};
|
||||
fetch(href, {
|
||||
headers: headers
|
||||
}).then(function (response) {
|
||||
let headers = auth ? {
|
||||
'Authorization': auth
|
||||
} : undefined;
|
||||
fetch(href, { headers: headers }).then(function (response) {
|
||||
if (response.ok) {
|
||||
return response.blob();
|
||||
}
|
||||
@@ -245,14 +247,18 @@ export class CollectionsScene {
|
||||
});
|
||||
};
|
||||
if (collection.type == CollectionType.WEBCAL) {
|
||||
download_btn.parentElement.classList.add("hidden");
|
||||
if (download_btn.parentElement) {
|
||||
download_btn.parentElement.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
delete_btn.onclick = function () { return ondelete(collection); };
|
||||
edit_btn.onclick = function () { return onedit(collection); };
|
||||
share_btn.onclick = function () { return onshare(collection); };
|
||||
node.classList.remove("hidden");
|
||||
nodes.push(node);
|
||||
template.parentNode.insertBefore(node, template);
|
||||
if (template.parentNode) {
|
||||
template.parentNode.insertBefore(node, template);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -276,7 +282,9 @@ export class CollectionsScene {
|
||||
incomingshares_btn.onclick = null;
|
||||
// remove collection
|
||||
nodes.forEach(function (node) {
|
||||
node.parentNode.removeChild(node);
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
});
|
||||
nodes = [];
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Collection, CollectionType } from "../models/collection.js";
|
||||
import { collectionsCache } from "../utils/collections_cache.js";
|
||||
import { ErrorHandler } from "../utils/error.js";
|
||||
import { FormValidator, validate_color, validate_href } from "../utils/form_validator.js";
|
||||
import { cleanHREFinput, onCleanHREFinput, random_hex, random_uuid } from "../utils/misc.js";
|
||||
import { cleanHREFinput, get_element, get_element_by_id, onCleanHREFinput, random_hex, random_uuid } from "../utils/misc.js";
|
||||
import { LoadingScene } from "./LoadingScene.js";
|
||||
import { Scene, is_current_scene, pop_scene, pop_to_parent, push_scene } from "./scene_manager.js";
|
||||
|
||||
@@ -35,25 +35,25 @@ import { Scene, is_current_scene, pop_scene, pop_to_parent, push_scene } from ".
|
||||
export class CreateEditCollectionScene {
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Collection} collection if it's a principal collection, a new
|
||||
* collection will be created inside of it.
|
||||
* Otherwise the collection will be edited.
|
||||
*/
|
||||
constructor(user, password, collection) {
|
||||
let edit = collection.type !== CollectionType.PRINCIPAL;
|
||||
let html_scene = document.getElementById(edit ? "editcollectionscene" : "createcollectionscene");
|
||||
/** @type {HTMLElement} */ let title_form = edit ? html_scene.querySelector("[data-name=title]") : null;
|
||||
/** @type {HTMLElement} */ let error_form = html_scene.querySelector("[data-name=error]");
|
||||
/** @type {HTMLInputElement} */ let href_form = html_scene.querySelector("[data-name=href]");
|
||||
/** @type {HTMLInputElement} */ let displayname_form = html_scene.querySelector("[data-name=displayname]");
|
||||
/** @type {HTMLInputElement} */ let description_form = html_scene.querySelector("[data-name=description]");
|
||||
/** @type {HTMLInputElement} */ let source_form = html_scene.querySelector("[data-name=source]");
|
||||
/** @type {HTMLElement} */ let source_label = html_scene.querySelector("label[for=source]");
|
||||
/** @type {HTMLSelectElement} */ let type_form = html_scene.querySelector("[data-name=type]");
|
||||
/** @type {HTMLInputElement} */ let color_form = html_scene.querySelector("[data-name=color]");
|
||||
/** @type {HTMLElement} */ let submit_btn = html_scene.querySelector("[data-name=submit]");
|
||||
/** @type {HTMLElement} */ let cancel_btn = html_scene.querySelector("[data-name=cancel]");
|
||||
let html_scene = get_element_by_id(edit ? "editcollectionscene" : "createcollectionscene");
|
||||
/** @type {?HTMLElement} */ let title_form = edit ? get_element(html_scene, "[data-name=title]") : null;
|
||||
/** @type {HTMLElement} */ let error_form = get_element(html_scene, "[data-name=error]");
|
||||
/** @type {?HTMLInputElement} */ let href_form = edit ? null : /** @type {HTMLInputElement} */(get_element(html_scene, "[data-name=href]"));
|
||||
/** @type {HTMLInputElement} */ let displayname_form = /** @type {HTMLInputElement} */(get_element(html_scene, "[data-name=displayname]"));
|
||||
/** @type {HTMLInputElement} */ let description_form = /** @type {HTMLInputElement} */(get_element(html_scene, "[data-name=description]"));
|
||||
/** @type {HTMLInputElement} */ let source_form = /** @type {HTMLInputElement} */(get_element(html_scene, "[data-name=source]"));
|
||||
/** @type {HTMLElement} */ let source_label = get_element(html_scene, "label[for=source]");
|
||||
/** @type {HTMLSelectElement} */ let type_form = /** @type {HTMLSelectElement} */(get_element(html_scene, "[data-name=type]"));
|
||||
/** @type {HTMLInputElement} */ let color_form = /** @type {HTMLInputElement} */(get_element(html_scene, "[data-name=color]"));
|
||||
/** @type {HTMLElement} */ let submit_btn = get_element(html_scene, "[data-name=submit]");
|
||||
/** @type {HTMLElement} */ let cancel_btn = get_element(html_scene, "[data-name=cancel]");
|
||||
|
||||
|
||||
/** @type {?XMLHttpRequest} */ let create_edit_req = null;
|
||||
@@ -62,7 +62,7 @@ export class CreateEditCollectionScene {
|
||||
let errorHandler = new ErrorHandler(error_form);
|
||||
let validator = new FormValidator(errorHandler);
|
||||
|
||||
if (!edit) {
|
||||
if (!edit && href_form) {
|
||||
validator.addValidator(href_form, validate_href(href_form, "HREF"));
|
||||
}
|
||||
validator.addValidator(color_form, validate_color(color_form, "Color"));
|
||||
@@ -74,7 +74,7 @@ export class CreateEditCollectionScene {
|
||||
let type = edit ? collection.type : CollectionType.CALENDAR_JOURNAL_TASKS;
|
||||
let color = edit && collection.color ? collection.color : "#" + random_hex(6);
|
||||
|
||||
if (!edit) {
|
||||
if (!edit && href_form) {
|
||||
href_form.addEventListener("input", onCleanHREFinput);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export class CreateEditCollectionScene {
|
||||
}
|
||||
|
||||
function read_form() {
|
||||
if (!edit) {
|
||||
if (!edit && href_form) {
|
||||
cleanHREFinput(href_form);
|
||||
let newhreftxtvalue = href_form.value.trim().toLowerCase();
|
||||
href = collection.href + newhreftxtvalue + "/";
|
||||
@@ -107,7 +107,7 @@ export class CreateEditCollectionScene {
|
||||
}
|
||||
|
||||
function fill_form() {
|
||||
if (!edit) {
|
||||
if (!edit && href_form) {
|
||||
href_form.value = random_uuid();
|
||||
}
|
||||
displayname_form.value = displayname;
|
||||
@@ -127,12 +127,15 @@ export class CreateEditCollectionScene {
|
||||
read_form();
|
||||
let sane_color = color.trim();
|
||||
if (sane_color) {
|
||||
sane_color = COLOR_RE.exec(sane_color)[1];
|
||||
/** @type {?RegExpExecArray} */ let match = COLOR_RE.exec(sane_color);
|
||||
if (match) {
|
||||
sane_color = match[1];
|
||||
}
|
||||
}
|
||||
let loading_scene = new LoadingScene();
|
||||
push_scene(loading_scene);
|
||||
let collection = new Collection(href, type, displayname, description, sane_color, 0, 0, source);
|
||||
let callback = function (error1) {
|
||||
let callback = function (/** @type {?string} */ error1) {
|
||||
if (!is_current_scene(loading_scene)) {
|
||||
return;
|
||||
}
|
||||
@@ -166,7 +169,7 @@ export class CreateEditCollectionScene {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} _e
|
||||
* @param {?Event} _e
|
||||
*/
|
||||
function onTypeChange(_e) {
|
||||
if (type_form.value == CollectionType.WEBCAL) {
|
||||
@@ -183,10 +186,12 @@ export class CreateEditCollectionScene {
|
||||
// Clone type_form because it's impossible to hide options without removing them
|
||||
saved_type_form = type_form;
|
||||
type_form = /** @type {HTMLSelectElement} */ (type_form.cloneNode(true));
|
||||
saved_type_form.parentNode.replaceChild(type_form, saved_type_form);
|
||||
if (saved_type_form.parentNode) {
|
||||
saved_type_form.parentNode.replaceChild(type_form, saved_type_form);
|
||||
}
|
||||
remove_invalid_types();
|
||||
html_scene.classList.remove("hidden");
|
||||
if (edit) {
|
||||
if (edit && title_form) {
|
||||
title_form.textContent = collection.displayname || collection.href;
|
||||
}
|
||||
fill_form();
|
||||
@@ -198,8 +203,10 @@ export class CreateEditCollectionScene {
|
||||
read_form();
|
||||
html_scene.classList.add("hidden");
|
||||
// restore type_form
|
||||
type_form.parentNode.replaceChild(saved_type_form, type_form);
|
||||
type_form = saved_type_form;
|
||||
if (type_form.parentNode && saved_type_form) {
|
||||
type_form.parentNode.replaceChild(saved_type_form, type_form);
|
||||
type_form = saved_type_form;
|
||||
}
|
||||
saved_type_form = null;
|
||||
submit_btn.onclick = null;
|
||||
cancel_btn.onclick = null;
|
||||
|
||||
@@ -24,7 +24,7 @@ import { CollectionType } from "../models/collection.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 { onCleanHREFinput, random_uuid } from "../utils/misc.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";
|
||||
|
||||
/**
|
||||
@@ -33,7 +33,7 @@ import { Scene, is_current_scene, pop_scene } from "./scene_manager.js";
|
||||
export class CreateEditShareScene {
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {import("../models/collection.js").Collection} collection
|
||||
* @param {string} shareType
|
||||
* @param {Share} [share] If provided, the scene will be in edit mode.
|
||||
@@ -42,27 +42,27 @@ export class CreateEditShareScene {
|
||||
let self = this;
|
||||
let edit = !!share;
|
||||
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]");
|
||||
/** @type {HTMLInputElement} */ let shareuser_input = html_scene.querySelector("[data-name=shareuser]");
|
||||
/** @type {HTMLInputElement} */ let sharehref_input = html_scene.querySelector("[data-name=sharehref]");
|
||||
/** @type {HTMLInputElement} */ let enabled_checkbox = html_scene.querySelector("[data-name=enabled]");
|
||||
/** @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 {HTMLElement} */ let html_scene = get_element_by_id("newshare");
|
||||
/** @type {HTMLFormElement} */ let form = /** @type {HTMLFormElement} */ (get_element(html_scene, "form"));
|
||||
/** @type {HTMLElement} */ let sharemapfields = get_element(html_scene, "[data-name=sharemapfields]");
|
||||
/** @type {HTMLInputElement} */ let shareuser_input = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=shareuser]"));
|
||||
/** @type {HTMLInputElement} */ let sharehref_input = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=sharehref]"));
|
||||
/** @type {HTMLInputElement} */ let enabled_checkbox = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=enabled]"));
|
||||
/** @type {HTMLInputElement} */ let hidden_checkbox = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=hidden]"));
|
||||
let permissions_ro_radio = /** @type {HTMLInputElement} */ (get_element_by_id("newshare_attr_permissions_ro"));
|
||||
let permissions_rw_radio = /** @type {HTMLInputElement} */ (get_element_by_id("newshare_attr_permissions_rw"));
|
||||
|
||||
/** @type {HTMLDetailsElement} */ let properties_fieldset = /** @type {HTMLDetailsElement} */ (html_scene.querySelector("[data-name=properties_override]"));
|
||||
/** @type {HTMLInputElement} */ let displayname_override_enabled = html_scene.querySelector("[data-name=displayname_override_enabled]");
|
||||
/** @type {HTMLInputElement} */ let displayname_override_input = html_scene.querySelector("[data-name=displayname_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 {HTMLDetailsElement} */ let properties_fieldset = /** @type {HTMLDetailsElement} */ (get_element(html_scene, "[data-name=properties_override]"));
|
||||
/** @type {HTMLInputElement} */ let displayname_override_enabled = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=displayname_override_enabled]"));
|
||||
/** @type {HTMLInputElement} */ let displayname_override_input = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=displayname_override]"));
|
||||
/** @type {HTMLInputElement} */ let description_override_enabled = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=description_override_enabled]"));
|
||||
/** @type {HTMLInputElement} */ let description_override_input = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=description_override]"));
|
||||
/** @type {HTMLInputElement} */ let color_override_enabled = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=color_override_enabled]"));
|
||||
/** @type {HTMLInputElement} */ let color_override_input = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=color_override]"));
|
||||
|
||||
/** @type {HTMLElement} */ let error_form = html_scene.querySelector("[data-name=error]");
|
||||
/** @type {HTMLElement} */ let submit_btn = html_scene.querySelector("[data-name=submit]");
|
||||
/** @type {HTMLElement} */ let cancel_btn = html_scene.querySelector("[data-name=cancel]");
|
||||
/** @type {HTMLElement} */ let error_form = get_element(html_scene, "[data-name=error]");
|
||||
/** @type {HTMLElement} */ let submit_btn = get_element(html_scene, "[data-name=submit]");
|
||||
/** @type {HTMLElement} */ let cancel_btn = get_element(html_scene, "[data-name=cancel]");
|
||||
|
||||
let errorHandler = new ErrorHandler(error_form);
|
||||
let map_validator = new FormValidator(errorHandler);
|
||||
@@ -108,7 +108,7 @@ export class CreateEditShareScene {
|
||||
let hidden_by_owner = (shareType === "bday" && shareuser_input.value === user) ? false : hidden_checkbox.checked;
|
||||
let permissions = permissions_rw_radio.checked ? "rw" : "r";
|
||||
|
||||
let properties = {};
|
||||
/** @type {Object<string, string>} */ let properties = {};
|
||||
if (displayname_override_enabled.checked) {
|
||||
let key = get_property_key(collection.type, "DISPLAYNAME");
|
||||
if (key) properties[key] = displayname_override_input.value;
|
||||
@@ -122,7 +122,7 @@ export class CreateEditShareScene {
|
||||
if (key) properties[key] = color_override_input.value + (color_override_input.value ? "ff" : "");
|
||||
}
|
||||
|
||||
let callback = function (/** @type {string} */ error) {
|
||||
let callback = function (/** @type {?string} */ error) {
|
||||
if (!is_current_scene(self)) {
|
||||
return;
|
||||
}
|
||||
@@ -143,12 +143,12 @@ export class CreateEditShareScene {
|
||||
PathMapped: pathMapped,
|
||||
Permissions: permissions,
|
||||
EnabledByOwner: enabled_by_owner,
|
||||
EnabledByUser: edit ? share.EnabledByUser : null,
|
||||
EnabledByUser: (edit && share) ? share.EnabledByUser : null,
|
||||
HiddenByOwner: hidden_by_owner,
|
||||
HiddenByUser: edit ? share.HiddenByUser : null,
|
||||
HiddenByUser: (edit && share) ? share.HiddenByUser : null,
|
||||
Properties: properties,
|
||||
User: edit ? share.User : shareuser_input.value,
|
||||
PathOrToken: edit ? share.PathOrToken : ((shareType === "map" || shareType === "bday") ? "/" + shareuser_input.value + "/" + sharehref_input.value + "/" : ""),
|
||||
User: (edit && share) ? share.User : shareuser_input.value,
|
||||
PathOrToken: (edit && share) ? share.PathOrToken : ((shareType === "map" || shareType === "bday") ? "/" + shareuser_input.value + "/" + sharehref_input.value + "/" : ""),
|
||||
});
|
||||
|
||||
if (edit) {
|
||||
@@ -187,13 +187,14 @@ export class CreateEditShareScene {
|
||||
cancel_btn.onclick = oncancel;
|
||||
form.onsubmit = onsubmit;
|
||||
|
||||
html_scene.querySelector("h1").textContent = edit ? "Edit Share" : "New Share";
|
||||
/** @type {HTMLHeadingElement} */ let title = /** @type {HTMLHeadingElement} */ (get_element(html_scene, "h1"));
|
||||
title.textContent = edit ? "Edit Share" : "New Share";
|
||||
submit_btn.textContent = edit ? "Save" : "Create";
|
||||
|
||||
shareuser_input.value = edit ? share.User : (shareType === "bday" ? user : "");
|
||||
shareuser_input.value = (edit && share) ? share.User : (shareType === "bday" ? user : "");
|
||||
shareuser_input.disabled = edit;
|
||||
enabled_checkbox.checked = edit ? share.EnabledByOwner : true;
|
||||
hidden_checkbox.checked = edit ? share.HiddenByOwner : false;
|
||||
enabled_checkbox.checked = (edit && share && share.EnabledByOwner !== null) ? share.EnabledByOwner : true;
|
||||
hidden_checkbox.checked = (edit && share && share.HiddenByOwner !== null) ? share.HiddenByOwner : false;
|
||||
|
||||
if (shareType === "bday") {
|
||||
// bday is always read-only; hide the permissions section entirely
|
||||
@@ -206,8 +207,8 @@ export class CreateEditShareScene {
|
||||
permissions_ro_radio.closest("details")?.classList.remove("hidden");
|
||||
permissions_ro_radio.disabled = false;
|
||||
permissions_rw_radio.disabled = false;
|
||||
permissions_ro_radio.checked = edit ? share.Permissions.toLowerCase() === "r" : true;
|
||||
permissions_rw_radio.checked = edit ? share.Permissions.toLowerCase() === "rw" : false;
|
||||
permissions_ro_radio.checked = (edit && share) ? share.Permissions.toLowerCase() === "r" : true;
|
||||
permissions_rw_radio.checked = (edit && share) ? share.Permissions.toLowerCase() === "rw" : false;
|
||||
}
|
||||
|
||||
let displayname = collection.displayname || "";
|
||||
@@ -217,7 +218,7 @@ export class CreateEditShareScene {
|
||||
let description_override_enabled_value = false;
|
||||
let color_override_enabled_value = false;
|
||||
|
||||
if (edit && share.Properties) {
|
||||
if (edit && share && share.Properties) {
|
||||
let displayname_key = get_property_key(collection.type, "DISPLAYNAME");
|
||||
if (displayname_key && share.Properties[displayname_key] !== undefined) {
|
||||
displayname = share.Properties[displayname_key];
|
||||
@@ -257,15 +258,23 @@ export class CreateEditShareScene {
|
||||
properties_fieldset.open = true;
|
||||
}
|
||||
if (is_calendar || is_addressbook) {
|
||||
description_override_enabled.parentElement.classList.remove("hidden");
|
||||
color_override_enabled.parentElement.classList.remove("hidden");
|
||||
if (description_override_enabled.parentElement) {
|
||||
description_override_enabled.parentElement.classList.remove("hidden");
|
||||
}
|
||||
if (color_override_enabled.parentElement) {
|
||||
color_override_enabled.parentElement.classList.remove("hidden");
|
||||
}
|
||||
} else {
|
||||
description_override_enabled.parentElement.classList.add("hidden");
|
||||
color_override_enabled.parentElement.classList.add("hidden");
|
||||
if (description_override_enabled.parentElement) {
|
||||
description_override_enabled.parentElement.classList.add("hidden");
|
||||
}
|
||||
if (color_override_enabled.parentElement) {
|
||||
color_override_enabled.parentElement.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
if (shareType === "map" || shareType === "bday") {
|
||||
if (edit) {
|
||||
if (edit && share) {
|
||||
sharehref_input.value = share.PathOrToken.split("/").filter(Boolean).pop() || "";
|
||||
} else {
|
||||
sharehref_input.value = random_uuid();
|
||||
|
||||
@@ -23,6 +23,7 @@ import { DELETE_CONFIRMATION_TEXT } from "../constants.js";
|
||||
import { collectionsCache } from "../utils/collections_cache.js";
|
||||
import { ErrorHandler } from "../utils/error.js";
|
||||
import { FormValidator, validate_equals } from "../utils/form_validator.js";
|
||||
import { get_element, get_element_by_id } from "../utils/misc.js";
|
||||
import { LoadingScene } from "./LoadingScene.js";
|
||||
import { Scene, is_current_scene, pop_scene, push_scene } from "./scene_manager.js";
|
||||
|
||||
@@ -32,7 +33,7 @@ import { Scene, is_current_scene, pop_scene, push_scene } from "./scene_manager.
|
||||
export class DeleteConfirmationScene {
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {string} header_title
|
||||
* @param {any} item
|
||||
* @param {string} item_title
|
||||
@@ -41,16 +42,16 @@ export class DeleteConfirmationScene {
|
||||
* @param {function} [on_success]
|
||||
*/
|
||||
constructor(user, password, header_title, item, item_title, delete_action, needsconfirmation, on_success) {
|
||||
/** @type {HTMLElement} */ let html_scene = document.getElementById("deleteconfirmationscene");
|
||||
/** @type {HTMLElement} */ let header_html = html_scene.querySelector("[data-name=headertitle]");
|
||||
/** @type {HTMLElement} */ let html_scene = get_element_by_id("deleteconfirmationscene");
|
||||
/** @type {HTMLElement} */ let header_html = get_element(html_scene, "[data-name=headertitle]");
|
||||
if (header_html) header_html.textContent = header_title;
|
||||
/** @type {HTMLElement} */ let title_form = html_scene.querySelector("[data-name=title]");
|
||||
/** @type {HTMLElement} */ let error_form = html_scene.querySelector("[data-name=error]");
|
||||
/** @type {HTMLElement} */ let confirmation_prompt = html_scene.querySelector("[data-name=confirmationprompt]");
|
||||
/** @type {HTMLInputElement} */ let confirmation_txt = html_scene.querySelector("[data-name=confirmationtxt]");
|
||||
/** @type {HTMLElement} */ let delete_confirmation_lbl = html_scene.querySelector("[data-name=deleteconfirmationtext]");
|
||||
/** @type {HTMLElement} */ let delete_btn = html_scene.querySelector("[data-name=delete]");
|
||||
/** @type {HTMLElement} */ let cancel_btn = html_scene.querySelector("[data-name=cancel]");
|
||||
/** @type {HTMLElement} */ let title_form = get_element(html_scene, "[data-name=title]");
|
||||
/** @type {HTMLElement} */ let error_form = get_element(html_scene, "[data-name=error]");
|
||||
/** @type {HTMLElement} */ let confirmation_prompt = get_element(html_scene, "[data-name=confirmationprompt]");
|
||||
/** @type {HTMLInputElement} */ let confirmation_txt = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=confirmationtxt]"));
|
||||
/** @type {HTMLElement} */ let delete_confirmation_lbl = get_element(html_scene, "[data-name=deleteconfirmationtext]");
|
||||
/** @type {HTMLElement} */ let delete_btn = get_element(html_scene, "[data-name=delete]");
|
||||
/** @type {HTMLElement} */ let cancel_btn = get_element(html_scene, "[data-name=cancel]");
|
||||
|
||||
if (needsconfirmation) {
|
||||
delete_confirmation_lbl.innerHTML = DELETE_CONFIRMATION_TEXT;
|
||||
@@ -80,7 +81,7 @@ export class DeleteConfirmationScene {
|
||||
try {
|
||||
let loading_scene = new LoadingScene();
|
||||
push_scene(loading_scene);
|
||||
delete_req = delete_action(user, password, item, function (error1) {
|
||||
delete_req = delete_action(user, password, item, function (/** @type {?string} */ error1) {
|
||||
if (!is_current_scene(loading_scene)) {
|
||||
return;
|
||||
}
|
||||
@@ -113,8 +114,8 @@ export class DeleteConfirmationScene {
|
||||
return false;
|
||||
}
|
||||
|
||||
function onkeydown(event) {
|
||||
if (event.keyCode !== 13) {
|
||||
function onkeydown(/** @type {KeyboardEvent}*/ event) {
|
||||
if (event.code !== "Enter") {
|
||||
return;
|
||||
}
|
||||
ondelete();
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import { update_incoming_share } from "../api/sharing.js";
|
||||
import { collectionsCache } from "../utils/collections_cache.js";
|
||||
import { ErrorHandler } from "../utils/error.js";
|
||||
import { get_element, get_element_by_id } from "../utils/misc.js";
|
||||
import { displayPermissions } from "../utils/permissions.js";
|
||||
import { Scene, pop_scene } from "./scene_manager.js";
|
||||
|
||||
@@ -31,16 +32,16 @@ import { Scene, pop_scene } from "./scene_manager.js";
|
||||
export class IncomingSharingScene {
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
*/
|
||||
constructor(user, password) {
|
||||
/** @type {HTMLElement} */ let html_scene = document.getElementById("incomingsharingscene");
|
||||
/** @type {HTMLElement} */ let cancel_btn = html_scene.querySelector("[data-name=cancel]");
|
||||
/** @type {HTMLElement} */ let error_element = html_scene.querySelector("[data-name=error]");
|
||||
/** @type {HTMLElement} */ let tbody = html_scene.querySelector("tbody[data-name=incomingsharesbody]");
|
||||
/** @type {HTMLElement} */ let template = tbody.querySelector("[data-name=incomingsharerowtemplate]");
|
||||
/** @type {HTMLElement} */ let table = html_scene.querySelector("table");
|
||||
/** @type {HTMLElement} */ let noshares_message = html_scene.querySelector("[data-name=nosharesmessage]");
|
||||
/** @type {HTMLElement} */ let html_scene = get_element_by_id("incomingsharingscene");
|
||||
/** @type {HTMLElement} */ let cancel_btn = get_element(html_scene, "[data-name=cancel]");
|
||||
/** @type {HTMLElement} */ let error_element = get_element(html_scene, "[data-name=error]");
|
||||
/** @type {HTMLElement} */ let tbody = get_element(html_scene, "tbody[data-name=incomingsharesbody]");
|
||||
/** @type {HTMLElement} */ let template = get_element(tbody, "[data-name=incomingsharerowtemplate]");
|
||||
/** @type {HTMLElement} */ let table = get_element(html_scene, "table");
|
||||
/** @type {HTMLElement} */ let noshares_message = get_element(html_scene, "[data-name=nosharesmessage]");
|
||||
|
||||
let error_handler = new ErrorHandler(error_element);
|
||||
|
||||
@@ -62,8 +63,8 @@ export class IncomingSharingScene {
|
||||
enabled_cb.disabled = true;
|
||||
shown_cb.disabled = true;
|
||||
|
||||
let old_enabled = share.EnabledByUser;
|
||||
let old_hidden = share.HiddenByUser;
|
||||
let old_enabled = share.EnabledByUser || false;
|
||||
let old_hidden = share.HiddenByUser || false;
|
||||
|
||||
share.EnabledByUser = enabled_cb.checked;
|
||||
share.HiddenByUser = !shown_cb.checked;
|
||||
@@ -90,7 +91,9 @@ export class IncomingSharingScene {
|
||||
function render_shares(shares) {
|
||||
// clear old nodes
|
||||
nodes.forEach(function (node) {
|
||||
node.parentNode.removeChild(node);
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
});
|
||||
nodes = [];
|
||||
|
||||
@@ -111,11 +114,11 @@ export class IncomingSharingScene {
|
||||
let node = /** @type {HTMLElement} */(template.cloneNode(true));
|
||||
node.classList.remove("hidden");
|
||||
|
||||
let pathortoken = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=pathortoken]"));
|
||||
let owner_td = node.querySelector("[data-name=owner]");
|
||||
let permissions_td = /** @type {HTMLElement} */ (node.querySelector("[data-name=permissions]"));
|
||||
let enabled_cb = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=enabled]"));
|
||||
let shown_cb = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=shown]"));
|
||||
let pathortoken = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=pathortoken]"));
|
||||
let owner_td = get_element(node, "[data-name=owner]");
|
||||
let permissions_td = /** @type {HTMLElement} */ (get_element(node, "[data-name=permissions]"));
|
||||
let enabled_cb = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=enabled]"));
|
||||
let shown_cb = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=shown]"));
|
||||
|
||||
let displayPath = share.PathOrToken.substring(prefix.length);
|
||||
if (displayPath.endsWith("/")) {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { get_element_by_id } from "../utils/misc.js";
|
||||
import { Scene } from "./scene_manager.js";
|
||||
|
||||
/**
|
||||
@@ -26,7 +27,7 @@ import { Scene } from "./scene_manager.js";
|
||||
*/
|
||||
export class LoadingScene {
|
||||
constructor() {
|
||||
this.html_scene = document.getElementById("loadingscene");
|
||||
this.html_scene = get_element_by_id("loadingscene");
|
||||
}
|
||||
show() {
|
||||
this.html_scene.classList.remove("hidden");
|
||||
|
||||
@@ -21,27 +21,30 @@
|
||||
|
||||
import { get_principal } from "../api/api.js";
|
||||
import { ROOT_PATH, SERVER } from "../constants.js";
|
||||
import { Collection } from "../models/collection.js";
|
||||
import { collectionsCache } from "../utils/collections_cache.js";
|
||||
import { ErrorHandler } from "../utils/error.js";
|
||||
import { FormValidator, validate_non_empty } from "../utils/form_validator.js";
|
||||
import { get_element, get_element_by_id } from "../utils/misc.js";
|
||||
import { CollectionsScene } from "./CollectionsScene.js";
|
||||
import { LoadingScene } from "./LoadingScene.js";
|
||||
import { Scene, is_current_scene, pop_scene, pop_to_root, push_scene, replace_scene } from "./scene_manager.js";
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @implements {Scene}
|
||||
*/
|
||||
export class LoginScene {
|
||||
constructor() {
|
||||
/** @type {HTMLElement} */ let html_scene = document.getElementById("loginscene");
|
||||
/** @type {HTMLElement} */ let form = html_scene.querySelector("[data-name=form]");
|
||||
/** @type {HTMLInputElement} */ let user_form = html_scene.querySelector("[data-name=user]");
|
||||
/** @type {HTMLInputElement} */ let password_form = html_scene.querySelector("[data-name=password]");
|
||||
/** @type {HTMLElement} */ let error_form = html_scene.querySelector("[data-name=error]");
|
||||
/** @type {HTMLElement} */ let logout_view = document.getElementById("logoutview");
|
||||
/** @type {HTMLElement} */ let logout_user_form = logout_view.querySelector("[data-name=user]");
|
||||
/** @type {HTMLElement} */ let logout_btn = logout_view.querySelector("[data-name=logout]");
|
||||
/** @type {HTMLElement} */ let refresh_btn = logout_view.querySelector("[data-name=refresh]");
|
||||
/** @type {HTMLElement} */ let html_scene = get_element_by_id("loginscene");
|
||||
/** @type {HTMLElement} */ let form = get_element(html_scene, "[data-name=form]");
|
||||
/** @type {HTMLInputElement} */ let user_form = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=user]"));
|
||||
/** @type {HTMLInputElement} */ let password_form = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=password]"));
|
||||
/** @type {HTMLElement} */ let error_form = get_element(html_scene, "[data-name=error]");
|
||||
/** @type {HTMLElement} */ let logout_view = get_element_by_id("logoutview");
|
||||
/** @type {HTMLElement} */ let logout_user_form = get_element(logout_view, "[data-name=user]");
|
||||
/** @type {HTMLElement} */ let logout_btn = get_element(logout_view, "[data-name=logout]");
|
||||
/** @type {HTMLElement} */ let refresh_btn = get_element(logout_view, "[data-name=refresh]");
|
||||
|
||||
let user = "";
|
||||
/** @type {?XMLHttpRequest} */ let principal_req = null;
|
||||
@@ -60,7 +63,7 @@ export class LoginScene {
|
||||
|
||||
/**
|
||||
* @param {string} p_user
|
||||
* @param {string} p_password
|
||||
* @param {?string} p_password
|
||||
*/
|
||||
function perform_login(p_user, p_password) {
|
||||
user = p_user;
|
||||
@@ -77,7 +80,7 @@ export class LoginScene {
|
||||
// Fetch principal
|
||||
let loading_scene = new LoadingScene();
|
||||
push_scene(loading_scene);
|
||||
principal_req = get_principal(user, p_password, function (principal_collection, error1) {
|
||||
principal_req = get_principal(user, p_password, function (/** @type {?Collection} */ principal_collection, error1) {
|
||||
if (!is_current_scene(loading_scene)) {
|
||||
return;
|
||||
}
|
||||
@@ -85,12 +88,12 @@ export class LoginScene {
|
||||
if (error1) {
|
||||
errorHandler.setError(error1);
|
||||
pop_scene();
|
||||
} else {
|
||||
} else if (principal_collection) {
|
||||
// show collections
|
||||
let saved_user = user;
|
||||
user = "";
|
||||
let collections_scene = new CollectionsScene(
|
||||
saved_user, p_password, principal_collection, function (error1) {
|
||||
saved_user, p_password, principal_collection, function (/** @type {?string} */ error1) {
|
||||
errorHandler.setError(error1);
|
||||
user = saved_user;
|
||||
});
|
||||
@@ -150,29 +153,27 @@ export class LoginScene {
|
||||
|
||||
// Probe for existing authentication (e.g. X-Remote-User)
|
||||
// Use fetch with credentials: 'omit' to avoid browser login prompt on 401
|
||||
if (window.fetch) {
|
||||
fetch(SERVER + ROOT_PATH, {
|
||||
method: 'PROPFIND',
|
||||
headers: { 'Depth': '0' },
|
||||
credentials: 'omit'
|
||||
}).then(function (response) {
|
||||
if (response.ok) {
|
||||
// Authenticated! Now it's safe to call get_principal
|
||||
get_principal(null, null, function (principal_collection, error) {
|
||||
if (!error && principal_collection) {
|
||||
let authenticated_user = principal_collection.displayname;
|
||||
if (!authenticated_user) {
|
||||
let href = principal_collection.href.replace(/\/+$/, "");
|
||||
authenticated_user = href.substring(href.lastIndexOf("/") + 1);
|
||||
}
|
||||
perform_login(authenticated_user, null);
|
||||
fetch(SERVER + ROOT_PATH, {
|
||||
method: 'PROPFIND',
|
||||
headers: { 'Depth': '0' },
|
||||
credentials: 'omit'
|
||||
}).then(function (response) {
|
||||
if (response.ok) {
|
||||
// Authenticated! Now it's safe to call get_principal
|
||||
get_principal(null, null, function (/** @type {?Collection} */ principal_collection, error) {
|
||||
if (!error && principal_collection) {
|
||||
let authenticated_user = principal_collection.displayname;
|
||||
if (!authenticated_user) {
|
||||
let href = principal_collection.href.replace(/\/+$/, "");
|
||||
authenticated_user = href.substring(href.lastIndexOf("/") + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
})["catch"](function () {
|
||||
// Ignore error: we are not authenticated or something else went wrong
|
||||
});
|
||||
}
|
||||
perform_login(authenticated_user, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
})["catch"](function () {
|
||||
// Ignore error: we are not authenticated or something else went wrong
|
||||
});
|
||||
};
|
||||
this.hide = function () {
|
||||
read_form();
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { Collection, CollectionType } from "../models/collection.js";
|
||||
import { collectionsCache } from "../utils/collections_cache.js";
|
||||
import { ErrorHandler } from "../utils/error.js";
|
||||
import { get_element, get_element_by_id } from "../utils/misc.js";
|
||||
import { displayPermissions } from "../utils/permissions.js";
|
||||
import { CreateEditShareScene } from "./CreateEditShareScene.js";
|
||||
import { DeleteConfirmationScene } from "./DeleteConfirmationScene.js";
|
||||
@@ -39,37 +40,25 @@ import { Scene, pop_scene, push_scene } from "./scene_manager.js";
|
||||
export class ShareCollectionScene {
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Collection} collection The collection on which to edit sharing setting. Must exist.
|
||||
*/
|
||||
constructor(user, password, collection) {
|
||||
|
||||
let html_scene = document.getElementById("sharecollectionscene");
|
||||
let html_scene = get_element_by_id("sharecollectionscene");
|
||||
|
||||
/** @type {HTMLElement} */ let cancel_btn = html_scene.querySelector("[data-name=cancel]");
|
||||
/** @type {HTMLElement} */ let share_by_token_btn = html_scene.querySelector(
|
||||
"button[data-name=sharebytoken]"
|
||||
);
|
||||
/** @type {HTMLElement} */ let share_by_map_btn = html_scene.querySelector(
|
||||
"button[data-name=sharebymap]"
|
||||
);
|
||||
/** @type {HTMLElement} */ let share_by_bday_btn = html_scene.querySelector(
|
||||
"button[data-name=sharebybday]"
|
||||
);
|
||||
/** @type {HTMLElement} */ let share_by_token_div = html_scene.querySelector(
|
||||
"div[data-name=sharebytoken]"
|
||||
);
|
||||
/** @type {HTMLElement} */ let share_by_map_div = html_scene.querySelector(
|
||||
"div[data-name=sharebymap]"
|
||||
);
|
||||
/** @type {HTMLElement} */ let share_by_bday_div = html_scene.querySelector(
|
||||
"div[data-name=sharebybday]"
|
||||
);
|
||||
/** @type {HTMLElement} */ let error_form = html_scene.querySelector("[data-name=error]");
|
||||
/** @type {HTMLElement} */ let cancel_btn = get_element(html_scene, "[data-name=cancel]");
|
||||
/** @type {HTMLElement} */ let share_by_token_btn = get_element(html_scene, "button[data-name=sharebytoken]");
|
||||
/** @type {HTMLElement} */ let share_by_map_btn = get_element(html_scene, "button[data-name=sharebymap]");
|
||||
/** @type {HTMLElement} */ let share_by_bday_btn = get_element(html_scene, "button[data-name=sharebybday]");
|
||||
/** @type {HTMLElement} */ let share_by_token_div = get_element(html_scene, "div[data-name=sharebytoken]");
|
||||
/** @type {HTMLElement} */ let share_by_map_div = get_element(html_scene, "div[data-name=sharebymap]");
|
||||
/** @type {HTMLElement} */ let share_by_bday_div = get_element(html_scene, "div[data-name=sharebybday]");
|
||||
/** @type {HTMLElement} */ let error_form = get_element(html_scene, "[data-name=error]");
|
||||
|
||||
let errorHandler = new ErrorHandler(error_form);
|
||||
|
||||
/** @type {HTMLElement} */ let title = html_scene.querySelector("[data-name=title]");
|
||||
/** @type {HTMLElement} */ let title = get_element(html_scene, "[data-name=title]");
|
||||
|
||||
function oncancel() {
|
||||
try {
|
||||
@@ -169,7 +158,7 @@ export class ShareCollectionScene {
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Collection} collection
|
||||
* @param {ErrorHandler} errorHandler
|
||||
*/
|
||||
@@ -179,7 +168,9 @@ function update_share_list(user, password, collection, errorHandler) {
|
||||
);
|
||||
share_rows.forEach(function (row) {
|
||||
if (!row.classList.contains("hidden")) {
|
||||
row.parentNode.removeChild(row);
|
||||
if (row.parentNode) {
|
||||
row.parentNode.removeChild(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -195,7 +186,7 @@ function update_share_list(user, password, collection, errorHandler) {
|
||||
/**
|
||||
*
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Collection} collection
|
||||
* @param {import('../api/sharing.js').Share} share
|
||||
* @param {HTMLElement} template
|
||||
@@ -209,7 +200,7 @@ function add_share_row_node(user, password, collection, share, template, delete_
|
||||
let node = /** @type {HTMLElement} */ (template.cloneNode(true));
|
||||
node.classList.remove("hidden");
|
||||
|
||||
/** @type {HTMLInputElement} */ let pathortoken_form = node.querySelector("[data-name=pathortoken]");
|
||||
/** @type {HTMLInputElement} */ let pathortoken_form = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=pathortoken]"));
|
||||
if (pathortoken_form) {
|
||||
pathortoken_form.value = pathortoken;
|
||||
}
|
||||
@@ -217,13 +208,13 @@ function add_share_row_node(user, password, collection, share, template, delete_
|
||||
let permissions = (share["Permissions"] || "").toLowerCase();
|
||||
displayPermissions(permissions, node);
|
||||
|
||||
/** @type {HTMLElement} */ let edit_btn = node.querySelector("[data-name=edit]");
|
||||
/** @type {HTMLElement} */ let edit_btn = get_element(node, "[data-name=edit]");
|
||||
edit_btn.onclick = function () {
|
||||
let create_edit_share_scene = new CreateEditShareScene(user, password, collection, share.ShareType, share);
|
||||
push_scene(create_edit_share_scene);
|
||||
};
|
||||
|
||||
/** @type {HTMLElement} */ let delete_btn = node.querySelector("[data-name=delete]");
|
||||
/** @type {HTMLElement} */ let delete_btn = get_element(node, "[data-name=delete]");
|
||||
delete_btn.onclick = function () {
|
||||
let delete_collection_scene = new DeleteConfirmationScene(
|
||||
user, password, "Delete Share", share, delete_label + " " + pathortoken, delete_action, false,
|
||||
@@ -236,20 +227,22 @@ function add_share_row_node(user, password, collection, share, template, delete_
|
||||
push_scene(delete_collection_scene);
|
||||
};
|
||||
|
||||
template.parentNode.insertBefore(node, template);
|
||||
if (template.parentNode) {
|
||||
template.parentNode.insertBefore(node, template);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Collection} collection
|
||||
* @param {Array<import('../api/sharing.js').Share>} shares
|
||||
* @param {ErrorHandler} errorHandler
|
||||
*/
|
||||
function add_share_rows(user, password, collection, shares, errorHandler) {
|
||||
/** @type {HTMLElement} */ let token_template = document.querySelector("[data-name=sharetokenrowtemplate]");
|
||||
/** @type {HTMLElement} */ let map_template = document.querySelector("[data-name=sharemaprowtemplate]");
|
||||
/** @type {HTMLElement} */ let bday_template = document.querySelector("[data-name=sharebdayrowtemplate]");
|
||||
/** @type {HTMLElement} */ let token_template = get_element(document, "[data-name=sharetokenrowtemplate]");
|
||||
/** @type {HTMLElement} */ let map_template = get_element(document, "[data-name=sharemaprowtemplate]");
|
||||
/** @type {HTMLElement} */ let bday_template = get_element(document, "[data-name=sharebdayrowtemplate]");
|
||||
shares.forEach(function (share) {
|
||||
let pathortoken = share["PathOrToken"] || "";
|
||||
let pathmapped = share["PathMapped"] || "";
|
||||
|
||||
@@ -24,7 +24,7 @@ import { Collection } from "../models/collection.js";
|
||||
import { collectionsCache } from "../utils/collections_cache.js";
|
||||
import { ErrorHandler } from "../utils/error.js";
|
||||
import { FormValidator, validate_files, validate_href } from "../utils/form_validator.js";
|
||||
import { cleanHREFinput, onCleanHREFinput, random_uuid } from "../utils/misc.js";
|
||||
import { cleanHREFinput, get_element, get_element_by_id, onCleanHREFinput, random_uuid } from "../utils/misc.js";
|
||||
import { Scene, pop_scene } from "./scene_manager.js";
|
||||
|
||||
/**
|
||||
@@ -33,21 +33,21 @@ import { Scene, pop_scene } from "./scene_manager.js";
|
||||
export class UploadCollectionScene {
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {Collection} collection parent collection
|
||||
*/
|
||||
constructor(user, password, collection) {
|
||||
/** @type {HTMLElement} */ let html_scene = document.getElementById("uploadcollectionscene");
|
||||
/** @type {HTMLElement} */ let template = html_scene.querySelector("[data-name=filetemplate]");
|
||||
/** @type {HTMLElement} */ let upload_btn = html_scene.querySelector("[data-name=submit]");
|
||||
/** @type {HTMLElement} */ let close_btn = html_scene.querySelector("[data-name=close]");
|
||||
/** @type {HTMLInputElement} */ let uploadfile_form = html_scene.querySelector("[data-name=uploadfile]");
|
||||
/** @type {HTMLElement} */ let uploadfile_lbl = html_scene.querySelector("label[for=uploadfile]");
|
||||
/** @type {HTMLInputElement} */ let href_form = html_scene.querySelector("[data-name=href]");
|
||||
/** @type {HTMLElement} */ let href_label = html_scene.querySelector("label[for=href]");
|
||||
/** @type {HTMLElement} */ let hreflimitmsg_html = html_scene.querySelector("[data-name=hreflimitmsg]");
|
||||
/** @type {HTMLElement} */ let pending_html = html_scene.querySelector("[data-name=pending]");
|
||||
/** @type {HTMLElement} */ let error_form = html_scene.querySelector(":scope > span[data-name=error]");
|
||||
/** @type {HTMLElement} */ let html_scene = get_element_by_id("uploadcollectionscene");
|
||||
/** @type {HTMLElement} */ let template = get_element(html_scene, "[data-name=filetemplate]");
|
||||
/** @type {HTMLElement} */ let upload_btn = get_element(html_scene, "[data-name=submit]");
|
||||
/** @type {HTMLElement} */ let close_btn = get_element(html_scene, "[data-name=close]");
|
||||
/** @type {HTMLInputElement} */ let uploadfile_form = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=uploadfile]"));
|
||||
/** @type {HTMLElement} */ let uploadfile_lbl = get_element(html_scene, "label[for=uploadfile]");
|
||||
/** @type {HTMLInputElement} */ let href_form = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=href]"));
|
||||
/** @type {HTMLElement} */ let href_label = get_element(html_scene, "label[for=href]");
|
||||
/** @type {HTMLElement} */ let hreflimitmsg_html = get_element(html_scene, "[data-name=hreflimitmsg]");
|
||||
/** @type {HTMLElement} */ let pending_html = get_element(html_scene, "[data-name=pending]");
|
||||
/** @type {HTMLElement} */ let error_form = get_element(html_scene, ":scope > span[data-name=error]");
|
||||
|
||||
let files = uploadfile_form.files;
|
||||
href_form.addEventListener("input", onCleanHREFinput);
|
||||
@@ -64,7 +64,7 @@ export class UploadCollectionScene {
|
||||
validator.addValidator(uploadfile_form, validate_files(uploadfile_form, "file"));
|
||||
|
||||
/** @type {?XMLHttpRequest} */ let upload_req = null;
|
||||
/** @type {Array<string>} */ let results = [];
|
||||
/** @type {Array<?string>} */ let results = [];
|
||||
/** @type {?Array<HTMLElement>} */ let nodes = null;
|
||||
|
||||
function upload_start() {
|
||||
@@ -72,6 +72,9 @@ export class UploadCollectionScene {
|
||||
if (!validator.validate()) {
|
||||
return false;
|
||||
}
|
||||
if (!files) {
|
||||
return false;
|
||||
}
|
||||
read_form();
|
||||
uploadfile_form.classList.add("hidden");
|
||||
uploadfile_lbl.classList.add("hidden");
|
||||
@@ -88,12 +91,14 @@ export class UploadCollectionScene {
|
||||
let file = files[i];
|
||||
let node = /** @type {HTMLElement} */ (template.cloneNode(true));
|
||||
node.classList.remove("hidden");
|
||||
let name_form = node.querySelector("[data-name=name]");
|
||||
let name_form = get_element(node, "[data-name=name]");
|
||||
name_form.textContent = file.name;
|
||||
node.classList.remove("hidden");
|
||||
nodes.push(node);
|
||||
updateFileStatus(i);
|
||||
template.parentNode.insertBefore(node, template);
|
||||
if (template.parentNode) {
|
||||
template.parentNode.insertBefore(node, template);
|
||||
}
|
||||
}
|
||||
upload_next();
|
||||
} catch (err) {
|
||||
@@ -103,6 +108,9 @@ export class UploadCollectionScene {
|
||||
}
|
||||
|
||||
function upload_next() {
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (files.length === results.length) {
|
||||
pending_html.classList.add("hidden");
|
||||
@@ -145,8 +153,8 @@ export class UploadCollectionScene {
|
||||
if (nodes === null) {
|
||||
return;
|
||||
}
|
||||
/** @type {HTMLElement} */ let file_success_form = nodes[i].querySelector("[data-name=success]");
|
||||
/** @type {HTMLElement} */ let file_error_form = nodes[i].querySelector("[data-name=error]");
|
||||
/** @type {HTMLElement} */ let file_success_form = get_element(nodes[i], "[data-name=success]");
|
||||
/** @type {HTMLElement} */ let file_error_form = get_element(nodes[i], "[data-name=error]");
|
||||
if (results.length > i) {
|
||||
if (results[i]) {
|
||||
file_success_form.classList.add("hidden");
|
||||
@@ -171,7 +179,7 @@ export class UploadCollectionScene {
|
||||
|
||||
function onfileschange() {
|
||||
files = uploadfile_form.files;
|
||||
if (files.length > 1) {
|
||||
if (files && files.length > 1) {
|
||||
hreflimitmsg_html.classList.remove("hidden");
|
||||
href_form.classList.add("hidden");
|
||||
href_label.classList.add("hidden");
|
||||
@@ -213,7 +221,9 @@ export class UploadCollectionScene {
|
||||
return;
|
||||
}
|
||||
nodes.forEach(function (node) {
|
||||
node.parentNode.removeChild(node);
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
});
|
||||
nodes = null;
|
||||
};
|
||||
|
||||
@@ -56,6 +56,19 @@ export function push_scene(scene) {
|
||||
scene.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop the current scene and release it.
|
||||
*/
|
||||
function pop_and_release() {
|
||||
if (scene_stack.length === 0) {
|
||||
return;
|
||||
}
|
||||
let scene = scene_stack.pop();
|
||||
if (scene) {
|
||||
scene.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the current scene with a new one.
|
||||
* @param {Scene} scene
|
||||
@@ -63,7 +76,7 @@ export function push_scene(scene) {
|
||||
export function replace_scene(scene) {
|
||||
if (scene_stack.length >= 1) {
|
||||
scene_stack[scene_stack.length - 1].hide();
|
||||
scene_stack.pop().release();
|
||||
pop_and_release();
|
||||
}
|
||||
scene_stack.push(scene);
|
||||
scene.show();
|
||||
@@ -77,7 +90,7 @@ export function pop_scene() {
|
||||
return;
|
||||
}
|
||||
scene_stack[scene_stack.length - 1].hide();
|
||||
scene_stack.pop().release();
|
||||
pop_and_release();
|
||||
if (scene_stack.length >= 1) {
|
||||
scene_stack[scene_stack.length - 1].show();
|
||||
}
|
||||
@@ -92,9 +105,9 @@ export function pop_to_parent() {
|
||||
return;
|
||||
}
|
||||
scene_stack[scene_stack.length - 1].hide();
|
||||
scene_stack.pop().release();
|
||||
pop_and_release();
|
||||
if (scene_stack.length >= 1) {
|
||||
scene_stack.pop().release();
|
||||
pop_and_release();
|
||||
}
|
||||
if (scene_stack.length >= 1) {
|
||||
scene_stack[scene_stack.length - 1].show();
|
||||
@@ -112,7 +125,7 @@ export function pop_to_root() {
|
||||
// Pop all scenes until only the first one remains
|
||||
while (scene_stack.length > 1) {
|
||||
scene_stack[scene_stack.length - 1].hide();
|
||||
scene_stack.pop().release();
|
||||
pop_and_release();
|
||||
}
|
||||
// The root scene is now at the top (index 0) and should be shown
|
||||
if (scene_stack.length === 1) {
|
||||
|
||||
@@ -49,7 +49,7 @@ class CollectionsCache {
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {import("../models/collection.js").Collection} principal_collection
|
||||
* @param {function(string):void} onerror
|
||||
* @param {function(Array<import("../models/collection.js").Collection>, Array<import("../api/sharing.js").Share>, boolean):void} displayData
|
||||
@@ -63,9 +63,9 @@ class CollectionsCache {
|
||||
let loading_scene = new LoadingScene();
|
||||
push_scene(loading_scene);
|
||||
|
||||
let collections = null;
|
||||
let shares = null;
|
||||
let error = null;
|
||||
/** @type {?Array<import("../models/collection.js").Collection>} */ let collections = null;
|
||||
/** @type {?Array<import("../api/sharing.js").Share>} */ let shares = null;
|
||||
/** @type {?string} */ let error = null;
|
||||
|
||||
let check_if_completed = () => {
|
||||
if (!is_current_scene(loading_scene)) {
|
||||
@@ -101,7 +101,7 @@ class CollectionsCache {
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {function(string):void} onerror
|
||||
* @param {function(Array<import("../api/sharing.js").Share>):void} displayData
|
||||
*/
|
||||
@@ -132,7 +132,7 @@ class CollectionsCache {
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} password
|
||||
* @param {?string} password
|
||||
* @param {function(string):void} onerror
|
||||
* @param {function(import("../api/sharing.js").ServerFeatures):void} displayData
|
||||
*/
|
||||
|
||||
@@ -31,10 +31,14 @@ export class ErrorHandler {
|
||||
|
||||
/**
|
||||
* Sets an error message for a given key.
|
||||
* @param {string} errorMessage
|
||||
* @param {?string} errorMessage
|
||||
*/
|
||||
setError(errorMessage) {
|
||||
this._update([errorMessage]);
|
||||
if (errorMessage) {
|
||||
this._update([errorMessage]);
|
||||
} else {
|
||||
this.clearError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,6 +30,7 @@ export class FormValidator {
|
||||
*/
|
||||
constructor(error_handler) {
|
||||
this.error_handler = error_handler;
|
||||
/** @type {Array<{field: HTMLInputElement, validation_method: function(): ?string}>} */
|
||||
this.validation_methods = [];
|
||||
}
|
||||
|
||||
@@ -51,8 +52,8 @@ export class FormValidator {
|
||||
*/
|
||||
validate() {
|
||||
let errorMessages = [];
|
||||
for (let { field, validation_method } of this.validation_methods) {
|
||||
let errorMessage = validation_method(field);
|
||||
for (let { validation_method } of this.validation_methods) {
|
||||
let errorMessage = validation_method();
|
||||
if (errorMessage) {
|
||||
errorMessages.push(errorMessage);
|
||||
}
|
||||
@@ -136,7 +137,9 @@ export function validate_color(input, field_name) {
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}/**
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the input matches a specific string.
|
||||
* @param {HTMLInputElement} input
|
||||
* @param {string} target
|
||||
|
||||
@@ -48,11 +48,7 @@ export function random_uuid() {
|
||||
export function random_hex(length) {
|
||||
let bytes = new Uint8Array(Math.ceil(length / 2));
|
||||
window.crypto.getRandomValues(bytes);
|
||||
// Fallback for compatibility with older browsers which may not have padStart
|
||||
return bytes.reduce((s, b) => {
|
||||
let hex = b.toString(16);
|
||||
return s + (String.prototype["padStart"] ? hex["padStart"](2, "0") : ("0" + hex).slice(-2));
|
||||
}, "").substring(0, length);
|
||||
return bytes.reduce((s, b) => s + b.toString(16).padStart(2, "0"), "").substring(0, length);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,3 +115,30 @@ export function setupSelectAll() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an element by its ID and throw an error if it's not found.
|
||||
* @param {string} id The ID of the element to find.
|
||||
* @return {HTMLElement} The found element.
|
||||
*/
|
||||
export function get_element_by_id(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) {
|
||||
throw new Error("Element with ID '" + id + "' not found");
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an element by a selector and throw an error if it's not found.
|
||||
* @param {ParentNode} node The parent node to search within.
|
||||
* @param {string} selector The CSS selector to use.
|
||||
* @return {HTMLElement} The found element.
|
||||
*/
|
||||
export function get_element(node, selector) {
|
||||
const element = node.querySelector(selector);
|
||||
if (!element) {
|
||||
throw new Error("Element with selector '" + selector + "' not found");
|
||||
}
|
||||
return /** @type {HTMLElement} */ (element);
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@ export function displayPermissions(permissions, node) {
|
||||
permissions = (permissions || "").toLowerCase();
|
||||
if (permissions === "rw") {
|
||||
const roElement = node.querySelector("[data-name=ro]");
|
||||
if (roElement) {
|
||||
if (roElement && roElement.parentNode) {
|
||||
roElement.parentNode.removeChild(roElement);
|
||||
}
|
||||
} else if (permissions === "r") {
|
||||
const rwElement = node.querySelector("[data-name=rw]");
|
||||
if (rwElement) {
|
||||
if (rwElement && rwElement.parentNode) {
|
||||
rwElement.parentNode.removeChild(rwElement);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"target": "ES6",
|
||||
"target": "ES2017",
|
||||
"checkJs": true,
|
||||
"noEmit": true,
|
||||
"noUnusedLocals": true,
|
||||
|
||||
Reference in New Issue
Block a user