style(front/js): apply types on all objects

- reduce amount of warnings in IDE
    - maximize API type mapping with static/js/core/types.js
This commit is contained in:
Edouard Vanbelle
2026-05-07 23:40:02 +02:00
parent a38475bd2c
commit fac184ccfe
43 changed files with 1614 additions and 574 deletions
+76 -76
View File
@@ -6,18 +6,18 @@
import { switchToFilesSection } from '../../app/navigation.js';
import { ui } from '../../app/ui.js';
import { getCsrfHeaders } from '../../core/csrf.js';
import { formatDateShort } from '../../core/formatters.js';
import { formatDateShort, isEmailValid } from '../../core/formatters.js';
import { i18n } from '../../core/i18n.js';
import { fileSharing } from '../../features/sharing/fileSharing.js';
/** @import {Share} from '../../core/types.js' */
/** @import {ShareItem} from '../../core/types.js' */
const TTL = 5 * 60 * 1000; // 5 min
const sharedView = {
// State
/** @type {Array<Share>} */
/** @type {Array<ShareItem>} */
items: [],
_expires: 0,
@@ -25,8 +25,10 @@ const sharedView = {
/** @type {Map<string, boolean>} key = "file:<id>" | "folder:<id>" */
_knownItemsId: new Map(),
/** @type {Array<Share>} */
/** @type {Array<ShareItem>} */
filteredItems: [],
/** @type {ShareItem | null} */
currentItem: null,
/** Auth header helper — tokens are in HttpOnly cookies now */
@@ -238,6 +240,7 @@ const sharedView = {
// Close dropdowns when clicking outside
document.addEventListener('click', (e) => {
document.querySelectorAll('.shared-custom-select.open').forEach((sel) => {
if (!(e.target instanceof Node)) return;
if (!sel.contains(e.target)) sel.classList.remove('open');
});
});
@@ -249,8 +252,8 @@ const sharedView = {
if (closeBtn) closeBtn.addEventListener('click', () => this.closeShareDialog());
const copyLinkBtn = document.getElementById('sv-copy-link-btn');
if (copyLinkBtn) copyLinkBtn.addEventListener('click', () => this.copyShareLink());
const enablePw = document.getElementById('sv-enable-password');
const pwField = document.getElementById('sv-share-password');
const enablePw = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-password'));
const pwField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-password'));
if (enablePw)
enablePw.addEventListener('change', () => {
if (pwField) {
@@ -260,8 +263,8 @@ const sharedView = {
});
const genPwBtn = document.getElementById('sv-generate-password');
if (genPwBtn) genPwBtn.addEventListener('click', () => this.generatePassword());
const enableExp = document.getElementById('sv-enable-expiration');
const expField = document.getElementById('sv-share-expiration');
const enableExp = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-expiration'));
const expField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-expiration'));
if (enableExp)
enableExp.addEventListener('change', () => {
if (expField) {
@@ -294,6 +297,13 @@ const sharedView = {
},
// Initialize a custom select dropdown
/**
*
* @param {string} wrapperId
* @param {string} toggleId
* @param {string} dropdownId
* @returns
*/
_initCustomSelect(wrapperId, toggleId, dropdownId) {
const wrapper = document.getElementById(wrapperId);
const toggle = document.getElementById(toggleId);
@@ -330,14 +340,14 @@ const sharedView = {
// Filter and sort items
filterAndSortItems() {
const filterTypeActive = document.querySelector('#filter-type-dropdown .shared-select-option.active');
const sortByActive = document.querySelector('#sort-by-dropdown .shared-select-option.active');
const filterTypeActive = /** @type {HTMLDivElement} */ (document.querySelector('#filter-type-dropdown .shared-select-option.active'));
const sortByActive = /** @type {HTMLDivElement} */ (document.querySelector('#sort-by-dropdown .shared-select-option.active'));
const type = filterTypeActive ? filterTypeActive.dataset.value : 'all';
const sort = sortByActive ? sortByActive.dataset.value : 'date';
// Use the main top-bar search input
const searchInput = document.getElementById('search-input');
const searchInput = /** @type {HTMLInputElement} */ (document.getElementById('search-input'));
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
this.filteredItems = this.items.filter((item) => {
@@ -396,23 +406,23 @@ const sharedView = {
nameCell.appendChild(nameSpan);
const typeCell = document.createElement('td');
typeCell.textContent = item.item_type === 'file' ? this.translate('shared_typeFile', 'File') : this.translate('shared_typeFolder', 'Folder');
typeCell.textContent = item.item_type === 'file' ? i18n.t('shared_typeFile', 'File') : i18n.t('shared_typeFolder', 'Folder');
const dateCell = document.createElement('td');
dateCell.textContent = this.formatDate(item.created_at);
dateCell.textContent = formatDateShort(item.created_at);
const expCell = document.createElement('td');
expCell.textContent = item.expires_at ? this.formatDate(item.expires_at) : this.translate('shared_noExpiration', 'No expiration');
expCell.textContent = item.expires_at ? formatDateShort(item.expires_at) : i18n.t('shared_noExpiration', 'No expiration');
const permCell = document.createElement('td');
const perms = [];
if (item.permissions?.read) perms.push(this.translate('share_permissionRead', 'Read'));
if (item.permissions?.write) perms.push(this.translate('share_permissionWrite', 'Write'));
if (item.permissions?.reshare) perms.push(this.translate('share_permissionReshare', 'Reshare'));
if (item.permissions?.read) perms.push(i18n.t('share_permissionRead', 'Read'));
if (item.permissions?.write) perms.push(i18n.t('share_permissionWrite', 'Write'));
if (item.permissions?.reshare) perms.push(i18n.t('share_permissionReshare', 'Reshare'));
permCell.textContent = perms.join(', ') || 'Read';
const pwCell = document.createElement('td');
pwCell.textContent = item.has_password ? this.translate('shared_hasPassword', 'Yes') : this.translate('shared_noPassword', 'No');
pwCell.textContent = item.has_password ? i18n.t('shared_hasPassword', 'Yes') : i18n.t('shared_noPassword', 'No');
const actionsCell = document.createElement('td');
actionsCell.className = 'shared-item-actions';
@@ -420,30 +430,30 @@ const sharedView = {
const editBtn = document.createElement('button');
editBtn.className = 'action-btn edit-btn';
editBtn.innerHTML = '<span class="action-icon">✏️</span>';
editBtn.title = this.translate('shared_editShare', 'Edit Share');
editBtn.title = i18n.t('shared_editShare', 'Edit Share');
editBtn.addEventListener('click', () => this.openShareDialog(item));
const notifyBtn = document.createElement('button');
notifyBtn.className = 'action-btn notify-btn';
notifyBtn.innerHTML = '<span class="action-icon">📧</span>';
notifyBtn.title = this.translate('shared_notifyShare', 'Notify Someone');
notifyBtn.title = i18n.t('shared_notifyShare', 'Notify Someone');
notifyBtn.addEventListener('click', () => this.openNotificationDialog(item));
const copyBtn = document.createElement('button');
copyBtn.className = 'action-btn copy-btn';
copyBtn.innerHTML = '<span class="action-icon">📋</span>';
copyBtn.title = this.translate('shared_copyLink', 'Copy Link');
copyBtn.title = i18n.t('shared_copyLink', 'Copy Link');
copyBtn.addEventListener('click', () => {
navigator.clipboard
.writeText(item.url)
.then(() => this.showNotification(this.translate('shared_linkCopied', 'Link copied!')))
.catch(() => this.showNotification(this.translate('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
.then(() => ui.showNotification(i18n.t('shared_linkCopied', 'Link copied!'), 'success'))
.catch(() => ui.showNotification(i18n.t('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
});
const rmBtn = document.createElement('button');
rmBtn.className = 'action-btn remove-btn';
rmBtn.innerHTML = '<span class="action-icon">🗑️</span>';
rmBtn.title = this.translate('shared_removeShare', 'Remove Share');
rmBtn.title = i18n.t('shared_removeShare', 'Remove Share');
rmBtn.addEventListener('click', () => {
this.currentItem = item;
this.removeSharedItem();
@@ -456,6 +466,11 @@ const sharedView = {
},
// Open share dialog
/**
*
* @param {ShareItem} item
* @returns {void}
*/
openShareDialog(item) {
this.currentItem = item;
const shareDialog = document.getElementById('shared-view-edit-dialog');
@@ -463,14 +478,14 @@ const sharedView = {
const iconEl = document.getElementById('sv-dialog-icon');
const nameEl = document.getElementById('sv-dialog-name');
const urlEl = document.getElementById('sv-share-link-url');
const enablePw = document.getElementById('sv-enable-password');
const pwField = document.getElementById('sv-share-password');
const enableExp = document.getElementById('sv-enable-expiration');
const expField = document.getElementById('sv-share-expiration');
const permRead = document.getElementById('sv-permission-read');
const permWrite = document.getElementById('sv-permission-write');
const permReshare = document.getElementById('sv-permission-reshare');
const urlEl = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-link-url'));
const enablePw = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-password'));
const pwField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-password'));
const enableExp = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-expiration'));
const expField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-expiration'));
const permRead = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-read'));
const permWrite = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-write'));
const permReshare = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-reshare'));
if (!shareDialog) return;
if (iconEl) iconEl.textContent = item.item_type === 'file' ? '📄' : '📁';
@@ -505,14 +520,19 @@ const sharedView = {
this.currentItem = null;
},
/**
*
* @param {ShareItem} item
* @returns {void}
*/
openNotificationDialog(item) {
this.currentItem = item;
const dn = item.item_name || item.item_id || 'Unknown';
const d = document.getElementById('sv-notification-dialog');
const iconEl = document.getElementById('sv-notify-dialog-icon');
const nameEl = document.getElementById('sv-notify-dialog-name');
const emailEl = document.getElementById('sv-notification-email');
const msgEl = document.getElementById('sv-notification-message');
const emailEl = /** @type {HTMLInputElement} */ (document.getElementById('sv-notification-email'));
const msgEl = /** @type {HTMLInputElement} */ (document.getElementById('sv-notification-message'));
if (!d) return;
if (iconEl) iconEl.textContent = item.item_type === 'file' ? '📄' : '📁';
@@ -529,18 +549,18 @@ const sharedView = {
},
copyShareLink() {
const el = document.getElementById('sv-share-link-url');
const el = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-link-url'));
if (!el) return;
navigator.clipboard
.writeText(el.value)
.then(() => this.showNotification(this.translate('shared_linkCopied', 'Link copied!')))
.catch(() => this.showNotification(this.translate('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
.then(() => ui.showNotification(i18n.t('shared_linkCopied', 'Link copied!'), 'success'))
.catch(() => ui.showNotification(i18n.t('shared_linkCopyFailed', 'Failed to copy link'), 'error'));
},
// Generate secure password with crypto API
generatePassword() {
const pwField = document.getElementById('sv-share-password');
const enablePw = document.getElementById('sv-enable-password');
const pwField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-password'));
const enablePw = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-password'));
if (!pwField || !enablePw) return;
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
@@ -559,13 +579,13 @@ const sharedView = {
async updateSharedItem() {
if (!this.currentItem) return;
const permRead = document.getElementById('sv-permission-read');
const permWrite = document.getElementById('sv-permission-write');
const permReshare = document.getElementById('sv-permission-reshare');
const enablePw = document.getElementById('sv-enable-password');
const pwField = document.getElementById('sv-share-password');
const enableExp = document.getElementById('sv-enable-expiration');
const expField = document.getElementById('sv-share-expiration');
const permRead = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-read'));
const permWrite = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-write'));
const permReshare = /** @type {HTMLInputElement} */ (document.getElementById('sv-permission-reshare'));
const enablePw = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-password'));
const pwField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-password'));
const enableExp = /** @type {HTMLInputElement} */ (document.getElementById('sv-enable-expiration'));
const expField = /** @type {HTMLInputElement} */ (document.getElementById('sv-share-expiration'));
const body = {
permissions: {
@@ -588,10 +608,10 @@ const sharedView = {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `Server error ${res.status}`);
}
this.showNotification(this.translate('shared_itemUpdated', 'Share settings updated'));
ui.showNotification(i18n.t('shared_itemUpdated', 'Share settings updated'), 'success');
} catch (err) {
console.error('Error updating share:', err);
this.showNotification(err.message || 'Error updating share', 'error');
ui.showNotification(/** @type {Error} */ (err).message || 'Error updating share', 'error');
}
// update UI
ui.setSharedVisualState(this.currentItem.item_id, this.currentItem.item_type, true);
@@ -611,10 +631,10 @@ const sharedView = {
headers: this._headers()
});
if (!res.ok && res.status !== 204) throw new Error(`Server error ${res.status}`);
this.showNotification(this.translate('shared_itemRemoved', 'Share removed'));
ui.showNotification(i18n.t('shared_itemRemoved', 'Share removed'), 'success');
} catch (err) {
console.error('Error removing share:', err);
this.showNotification('Error removing share', 'error');
ui.showNotification('Error removing share', 'error');
}
this.closeShareDialog();
@@ -627,13 +647,13 @@ const sharedView = {
// Send notification (stub)
sendNotification() {
if (!this.currentItem) return;
const emailEl = document.getElementById('sv-notification-email');
const msgEl = document.getElementById('sv-notification-message');
const emailEl = /** @type {HTMLInputElement} */ (document.getElementById('sv-notification-email'));
const msgEl = /** @type {HTMLInputElement} */ (document.getElementById('sv-notification-message'));
const email = emailEl ? emailEl.value.trim() : '';
const message = msgEl ? msgEl.value.trim() : '';
if (!email || !this.validateEmail(email)) {
this.showNotification(this.translate('shared_invalidEmail', 'Please enter a valid email address'), 'error');
if (!email || !isEmailValid(email)) {
ui.showNotification(i18n.t('shared_invalidEmail', 'Please enter a valid email address'), 'error');
return;
}
@@ -642,30 +662,10 @@ const sharedView = {
.sendShareNotification(this.currentItem.url, email, message)
.then(() => {
this.closeNotificationDialog();
this.showNotification(this.translate('shared_notificationSent', 'Notification sent'));
ui.showNotification(i18n.t('shared_notificationSent', 'Notification sent'), 'success');
})
.catch(() => this.showNotification(this.translate('shared_notificationFailed', 'Failed to send notification'), 'error'));
.catch(() => ui.showNotification(i18n.t('shared_notificationFailed', 'Failed to send notification'), 'error'));
}
},
showNotification(message, type = 'success') {
if (ui?.showNotification) {
ui.showNotification(message, type);
} else {
alert(message);
}
},
validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
},
formatDate(value) {
return formatDateShort(value);
},
translate(key, defaultText) {
return i18n.t(key, defaultText);
}
};