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
+4
View File
@@ -18,6 +18,10 @@ function getCsrfToken() {
return match ? match.split('=')[1] : '';
}
/**
* returns headers to add, this includes the X-CSRF-Token
* @returns {Record<String, String>}
*/
function getCsrfHeaders() {
const token = getCsrfToken();
return token ? { 'X-CSRF-Token': token } : {};
+1
View File
@@ -37,6 +37,7 @@ const WRAPPER_USER_DATA_KEY = 'oxicloud_user';
let _originalFetch = window.fetch.bind(window);
/** Deduplicates concurrent refresh attempts into a single in-flight promise. */
/** @type {Promise<boolean> | null} */
let _refreshInFlight = null;
async function _refresh() {
+11 -1
View File
@@ -1,6 +1,7 @@
/**
* OxiCloud - Shared format and escaping utilities
* Centralized global helpers for date/size/text formatting and XSS-safe escaping.
* Contains also checkers
*/
/**
@@ -97,4 +98,13 @@ function isTextViewable(mimeType) {
return TEXT_TYPES.includes(mimeType);
}
export { escapeHtml, formatDateShort, formatDateTime, formatFileSize, formatQuotaSize, isTextViewable };
/**
* Chekif an email is valid
* @param {string} email
* @returns boolean
*/
function isEmailValid(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
export { escapeHtml, formatDateShort, formatDateTime, formatFileSize, formatQuotaSize, isEmailValid, isTextViewable };
+15 -7
View File
@@ -37,7 +37,7 @@ function resolveBrowserLocale() {
let currentLocale = resolveBrowserLocale();
// Cache for translations
/** @type {Record<string, Object>} */
/** @type {Record<string, any>} */
const translations = {};
/**
@@ -71,7 +71,7 @@ async function loadTranslations(locale) {
/**
* Get a nested translation value
* @param {object} obj - The translations object
* @param {Record<string, any>} obj - The translations object
* @param {string} path - The dot-notation path to the translation
* @returns {string|null} - The translation value or null if not found
*/
@@ -110,7 +110,7 @@ function getNestedValue(obj, path) {
/**
* Replace parameters in a translation string
* @param {string} text - The translation string with placeholders
* @param {object} params - The parameters to replace
* @param {Record<string, any>} params - The parameters to replace
* @returns {string} - The interpolated string
*/
function interpolate(text, params) {
@@ -244,11 +244,19 @@ document.addEventListener('DOMContentLoaded', async () => {
// Self-contained t wrapper — does NOT call the global t() because other
// scripts (e.g. admin.js) may shadow it, which would cause infinite recursion.
function safeT(key, params = {}) {
/**
* @param {string} key
* @param {string | Record<string, any>} [paramsOrFallback] - interpolation params object, or a string fallback used when the key is missing
* @returns {string}
*/
function safeT(key, paramsOrFallback = {}) {
const fallback = typeof paramsOrFallback === 'string' ? paramsOrFallback : null;
const params = typeof paramsOrFallback === 'object' ? paramsOrFallback : {};
const localeData = translations[currentLocale];
if (!localeData) {
// Translations not loaded yet — return humanised key suffix
return key.split('.').pop() || key;
// Translations not loaded yet — return fallback or humanised key suffix
return fallback ?? key.split('.').pop() ?? key;
}
let value = getNestedValue(localeData, key);
@@ -258,7 +266,7 @@ function safeT(key, params = {}) {
value = getNestedValue(translations.en, key);
}
if (!value) return key;
if (!value) return fallback ?? key;
return interpolate(value, params);
}
+2 -1
View File
@@ -15,6 +15,7 @@
// All icons use viewBox="0 0 {width} 512" and fill="currentColor".
// Keys use FA5 class names (without "fa-" prefix) for backward compatibility.
/** @type {Record<String, Array<number | String>>} */
const OxiIcons = {
'arrow-left': [
448,
@@ -503,7 +504,7 @@ function replaceIconsInElement(container) {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('fill', 'currentColor');
path.setAttribute('d', d);
path.setAttribute('d', /** @type {string} */ (d));
svg.appendChild(path);
el.replaceWith(svg);
+12 -1
View File
@@ -34,6 +34,10 @@ function getAvailableLanguages() {
const rtlLanguages = ['fa', 'ar'];
// Update HTML lang attribute and dir for RTL languages
/**
*
* @param {string} langCode
*/
function updateHtmlAttributes(langCode) {
const htmlElement = document.documentElement;
@@ -150,6 +154,7 @@ function createLanguageSelector(containerId = 'language-selector') {
/**
* Toggle dropdown open/closed
* @param {HTMLElement} container
*/
function toggleDropdown(container) {
const isOpen = container.classList.contains('open');
@@ -162,6 +167,7 @@ function toggleDropdown(container) {
/**
* Open dropdown
* @param {HTMLElement} container
*/
function openDropdown(container) {
container.classList.add('open');
@@ -173,6 +179,7 @@ function openDropdown(container) {
/**
* Close dropdown
* @param {HTMLElement} container
*/
function closeDropdown(container) {
container.classList.remove('open');
@@ -184,6 +191,8 @@ function closeDropdown(container) {
/**
* Select a language
* @param {String} langCode
* @param {HTMLElement} container
*/
async function selectLanguage(langCode, container) {
await i18n.setLocale(langCode);
@@ -197,6 +206,8 @@ async function selectLanguage(langCode, container) {
/**
* Update the UI to reflect selected language
* @param {String} langCode
* @param {HTMLElement} container
*/
function updateSelectedLanguage(langCode, container) {
const languages = getAvailableLanguages();
@@ -213,7 +224,7 @@ function updateSelectedLanguage(langCode, container) {
options.forEach((option) => {
const isActive = option.getAttribute('data-lang') === langCode;
option.classList.toggle('active', isActive);
option.setAttribute('aria-selected', isActive);
option.setAttribute('aria-selected', String(isActive));
});
}
+18 -18
View File
@@ -10,25 +10,25 @@ const Modal = {
// Modal element references
/** @private @type {HTMLElement | null} */
overlay: null,
// FIXME: unused ?
container: null,
/** @private @type {HTMLElement | null} */
icon: null,
/** @private @type {HTMLElement | null} */
title: null,
/** @private @type {HTMLElement | null} */
label: null,
/** @private @type {HTMLElement | null} */
/** @private @type {HTMLInputElement | null} */
input: null,
/** @private @type {HTMLElement | null} */
/** @private @type {HTMLButtonElement | null} */
cancelBtn: null,
/** @private @type {HTMLElement | null} */
/** @private @type {HTMLButtonElement | null} */
confirmBtn: null,
/** @private @type {HTMLElement | null} */
/** @private @type {HTMLButtonElement | null} */
closeBtn: null,
// Current callback
/** @private @type {Function | null} */
onConfirm: null,
/** @private @type {Function | null} */
onCancel: null,
/** @private @type {((value: string) => Promise<void>) | null} */
@@ -53,10 +53,10 @@ const Modal = {
this.icon = document.getElementById('modal-icon');
this.title = document.getElementById('modal-title');
this.label = document.getElementById('modal-label');
this.input = document.getElementById('modal-input');
this.cancelBtn = document.getElementById('modal-cancel-btn');
this.confirmBtn = document.getElementById('modal-confirm-btn');
this.closeBtn = document.getElementById('modal-close-btn');
this.input = /** @type {HTMLInputElement} */ (document.getElementById('modal-input'));
this.cancelBtn = /** @type {HTMLButtonElement} */ (document.getElementById('modal-cancel-btn'));
this.confirmBtn = /** @type {HTMLButtonElement} */ (document.getElementById('modal-confirm-btn'));
this.closeBtn = /** @type {HTMLButtonElement} */ (document.getElementById('modal-close-btn'));
// Event listeners
this.errorEl = document.getElementById('modal-error');
@@ -106,13 +106,13 @@ const Modal = {
/**
* Show input modal (replacement for prompt())
* @param {Object} options - Modal configuration
* @param {string} options.title - Modal title
* @param {string} options.label - Input label
* @param {string} options.placeholder - Input placeholder
* @param {string} options.value - Initial input value
* @param {string} options.icon - Font Awesome icon class (e.g., 'fa-folder-plus')
* @param {string} options.confirmText - Confirm button text
* @param {string} options.cancelText - Cancel button text
* @param {string} [options.title] - Modal title
* @param {string} [options.label] - Input label
* @param {string} [options.placeholder] - Input placeholder
* @param {string} [options.value] - Initial input value
* @param {string} [options.icon] - Font Awesome icon class (e.g., 'fa-folder-plus')
* @param {string} [options.confirmText] - Confirm button text
* @param {string} [options.cancelText] - Cancel button text
* @param {(value: string) => Promise<void>} [options.action] - Async action called on confirm.
* Throw an Error to keep the modal open and display the error message inline.
* When omitted the modal resolves immediately with the input value (legacy behaviour).
@@ -292,7 +292,7 @@ const Modal = {
if (this.onConfirm) this.onConfirm();
this.close(true);
} catch (e) {
this.showError(e.message || 'An error occurred');
this.showError(/** @type {Error} */ (e).message || 'An error occurred');
this.confirmBtn.disabled = false;
this.input.focus();
}
+25 -1
View File
@@ -15,13 +15,28 @@ import { i18n } from './i18n.js';
* clear()
*/
/**
* @typedef {Object} BatchNotification
* @property {HTMLElement} el
* @property {Number} totalFiles,
* @property {Number} completed
* @property {Number} successCount
* @property {Number} errorCount
* @property {Number} lastLabelUpdateTs
* @property {String} lastLabelFile
*/
const notifications = (() => {
/* ── state ──────────────────────────────────────────────── */
let _badgeCount = 0;
let _batchSeq = 0;
const _batches = {}; // batchId → { el, files:{}, totalFiles }
/** @type {Record<String,BatchNotification>} */
const _batches = {};
/* ── DOM refs (resolved lazily) ─────────────────────────── */
/** @type {(id: string) => HTMLElement | null } */
const $ = (id) => document.getElementById(id);
/* ── bell toggle ────────────────────────────────────────── */
@@ -237,6 +252,8 @@ const notifications = (() => {
/**
* Mark a file as completed within a batch (updates overall bar).
* DOM updates are throttled to every 5 files to avoid reflow starvation.
* @param {string} batchId
* @param {boolean} success
*/
function fileCompleted(batchId, success) {
const batch = _batches[batchId];
@@ -262,6 +279,9 @@ const notifications = (() => {
/**
* Finalise a batch – update icon and title.
* @param {string} batchId
* @param {number} successCount
* @param {number} totalFiles
*/
function finishBatch(batchId, successCount, totalFiles) {
const batch = _batches[batchId];
@@ -320,6 +340,10 @@ const notifications = (() => {
}
/* ── util ───────────────────────────────────────────────── */
// FIXME move to global library
/**
* @param {string} s
*/
function _esc(s) {
const d = document.createElement('div');
d.textContent = s;
+186 -4
View File
@@ -1,5 +1,19 @@
/**
* @typedef {Object} FolderInfo
* @typedef {'file' | 'folder'} ItemTypeEnum
*/
// FIXME to simplify
/**
* @typedef {Object} LightItem
* @property {string} id
* @property {string} name
* @property {ItemTypeEnum} type
* @property {string} parentId
*/
//FIXME: rename into FolderItem
/**
* @typedef {Object} FolderItem
* @property {string} category
* @property {number} created_at - timestamp
* @property {string} icon_class
@@ -13,8 +27,9 @@
* @property {string} path the full path
*/
//FIXME: rename into FileItem
/**
* @typedef {Object} FileInfo
* @typedef {Object} FileItem
* @property {string} category
* @property {number} created_at - timestamp
* @property {string} icon_class
@@ -39,7 +54,7 @@
*/
/**
* @typedef {Object} Share
* @typedef {Object} ShareItem
* @property {number} access_count
* @property {number} created_at - timestamp
* @property {String} created_by
@@ -48,8 +63,175 @@
* @property {string} id
* @property {string} item_id
* @property {string} item_name
* @property {string} item_type
* @property {ItemTypeEnum} item_type
* @property {SharePermissions} permissions
* @property {string | null} token
* @property {string} url
*/
/**
* @typedef {Object} CreateShare
* @property {string} item_id
* @property {string|null} [item_name]
* @property {ItemTypeEnum} item_type
* @property {string|null} password
* @property {number|null} expires_at - timestamp
* @property {SharePermissions|null} permissions
*/
/**
* @typedef {Object} UpdateShare
* @property {string|null} password
* @property {number|null} expires_at - timestamp
* @property {SharePermissions|null} permissions
*/
/**
* @typedef {Object} FavoriteItem
* @property {string} id
* @property {string} user_id
* @property {string} item_id /// ID of the favorited item (file or folder)
* @property {ItemTypeEnum} item_type
* @property {number} created_at
* @property {string|null} item_name: null if folder
* @property {number|null} item_size null if folder
* @property {string|null} item_mime_type if file
* @property {string|null} parent_id
* @property {number|null} modified_at: Option<DateTime<Utc>>,
* @property {String} item_path Full human-readable path (e.g. "Documents/Work" for a folder, "Documents/Work/report.pdf" for a file)
* @property {String} icon_class
* @property {String} icon_special_class
* @property {String} category
* @property {String} size_formatted
*/
/**
* @typedef {Object} TrashItem
* @property {string} id
* @property {string} original_id
* @property {ItemTypeEnum} item_type
* @property {string} name
* @property {string} original_path - timestamp
* @property {number} trashed_at
* @property {number} days_until_deletion
* @property {string} category
* @property {string} icon_class
* @property {string} icon_special_class
*/
/**
* @typedef {Object} User
* @property {string} id
* @property {string} username
* @property {string} email
* @property {string} role
* @property {number} storage_quota_bytes
* @property {number} storage_used_bytes
* @property {number} created_at
* @property {number} updated_at
* @property {number} last_login_at
* @property {boolean} active
* @property {string} auth_provider
*/
/**
* @typedef {Object} AuthResponse
* @property {User} user
* @property {String} access_token
* @property {String} refresh_token
* @property {String} token_type
* @property {number} expires_in
*/
/**
* @typedef {'user' | 'admin'} RoleEnum
*/
/**
* @typedef {"relevance" | "name" | "name_desc" | "date" | "date_desc" | "size" | "size_desc"} SortByEnnum
*/
/**
* @typedef {Object} SearchCriteria
* @property {SortByEnnum} sort_by
* @property {boolean} recursive
* @property {number} limit
* @property {number} offset
*
* @property {String} [name_contains]
* @property {String[]} [file_types] pdf, jpg, ...
* @property {String} [folder_id]
*
*
* @property {number} [min_size]
* @property {number} [max_size]
*
* @property {number} [created_before]
* @property {number} [created_after]
*
* @property {number} [modified_before]
* @property {number} [modified_after]
*/
/**
* @typedef {Object} SearchResults
* FIXME: is in fact Vec<SearchFileResultDto>,
* @property {FileItem[]} files
* FIXME: is infact Vec<SearchFolderResultDto>,
* @property {FolderItem[]} folders:
* @property {number | null} total_count
* @property {number} limit
* @property {number} offset
* @property {boolean} has_more
* @property {number} query_time_ms
* @property {string} sort_by
*/
/**
* @typedef {Object} Playlist
* @property {String} id
* @property {String} name
* @property {String | null} description
* @property {String} owner_id
* @property {boolean} is_public
* @property {String | null} cover_file_id
* @property {number} track_count
* @property {number} total_duration_secs
* @property {number} created_at
* @property {number} updated_at
*/
/**
* @typedef {Object} PlaylistItem
* @property {String} id
* @property {String} playlist_id
* @property {String} file_id
* @property {number} position
* @property {number} added_at
* @property {String|null} file_name
* @property {number|null} file_size
* @property {String|null} mime_type
* @property {String|null} title
* @property {String|null} artist
* @property {String|null} album
* @property {number|null} duration_secs
*/
/**
* @typedef {Object} Musicshare
* @property {String} user_id
* @property {boolean|null} can_write
*/
/**
* @typedef {Object} FileMetadata
* @property {String} file_id
* @property {number} captured_at
* @property {number|null} latitude
* @property {number|null} longitude
* @property {String|null} camera_make
* @property {String|null} camera_model
* @property {number|null} orientation
* @property {number|null} width
* @property {number|null} height
*/