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:
@@ -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 } : {};
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user