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