feat(trash): move trash API to normalized version (with cursor, orderBy) + normalize Trash section to existing components

normalize also component to format badges (expiry, role, etc)
This commit is contained in:
Edouard Vanbelle
2026-05-30 00:15:07 +02:00
parent 6dab878919
commit ea83891a61
34 changed files with 2043 additions and 418 deletions
+161 -1
View File
@@ -162,12 +162,169 @@ function normalizeExpiryBucket(value) {
}
const daysUntil = Math.floor((date.getTime() - Date.now()) / 86_400_000);
if (daysUntil < 0) return i18n.t('expiryBucket.expired', 'Expired');
if (daysUntil <= 1) return i18n.t('expiryBucket.tomorrow', 'Tomorrow');
if (daysUntil === 0) return i18n.t('expiryBucket.today', 'Today');
if (daysUntil === 1) return i18n.t('expiryBucket.tomorrow', 'Tomorrow');
if (daysUntil <= 7) return i18n.t('expiryBucket.week', 'In less than 7 days');
if (daysUntil <= 30) return i18n.t('expiryBucket.month', 'In less than 30 days');
return String(date.getFullYear());
}
/**
* Format a future timestamp as a precise "days until" label.
*
* Used by the Trash view's date column to surface the remaining lifetime
* before the retention sweeper purges an item. Unlike `normalizeExpiryBucket`
* (which produces coarse bucket labels for grouping), this returns an
* exact-day count so users can see "In 27 days" at a glance.
*
* Buckets:
* < 0 → Expired
* = 0 → Today
* = 1 → Tomorrow
* > 1 → In N days
*
* @param {string | number | Date | null | undefined} value
* @returns {string}
*/
function formatDaysRemaining(value) {
if (value === null || value === undefined) return '';
/** @type {Date} */
let date;
if (value instanceof Date) {
date = value;
} else if (typeof value === 'number') {
date = new Date(value < 1e12 ? value * 1000 : value);
} else {
date = new Date(value);
}
if (Number.isNaN(date.getTime())) return String(value);
const daysUntil = Math.floor((date.getTime() - Date.now()) / 86_400_000);
if (daysUntil < 0) return i18n.t('daysRemaining.expired', 'Expired');
if (daysUntil === 0) return i18n.t('daysRemaining.today', 'Today');
if (daysUntil === 1) return i18n.t('daysRemaining.tomorrow', 'Tomorrow');
// Translation file holds the `{{count}}` template; the fallback is only
// used pre-load and embeds the literal count.
const translated = i18n.t('daysRemaining.inDays', { count: daysUntil });
return translated === 'daysRemaining.inDays' ? `${daysUntil} days` : translated;
}
/**
* Format a date as a compact "Mar 5, 2026" label.
*
* Shared helper used by `formatExpiryChip` (read-only chip) and by
* `buildExpiryChip` in `utils/expiryChip.js` (interactive editor) so the
* displayed deadline reads identically across both surfaces.
*
* Accepts:
* - `string` YYYY-MM-DD — parsed at LOCAL midnight (avoids the off-by-one
* shift `new Date("2026-05-30")` causes in negative-offset zones).
* - `string` ISO-8601 with time — parsed as-is.
* - `number` Unix seconds/ms (auto-detected at 1e12).
* - `Date` object — used directly.
*
* @param {string | number | Date | null | undefined} value
* @returns {string}
*/
function formatExpiryDate(value) {
if (value === null || value === undefined) return '';
/** @type {Date} */
let date;
if (value instanceof Date) {
date = value;
} else if (typeof value === 'number') {
date = new Date(value < 1e12 ? value * 1000 : value);
} else if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
// Bare YYYY-MM-DD: pin to local midnight so the day doesn't shift.
date = new Date(`${value}T00:00:00`);
} else {
date = new Date(value);
}
if (Number.isNaN(date.getTime())) return String(value);
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
}
/**
* Render an expiry / retention date as a tiered chip.
*
* Single shared formatter used by My Shares (link expiry), Trash
* (remaining lifetime before purge), and any other section that needs
* to surface a future deadline. Output:
*
* `<span class="expiry-chip expiry-chip--{tier}"><i class="..."></i>LABEL</span>`
*
* Six tiers escalate cool → hot:
* - null value → `never` (neutral, infinity icon, "Never")
* - > 30 days → `normal` (neutral grey, "Until DATE")
* - 8–30 days → `caution` (soft amber, "N days")
* - 2–7 days → `soon` (soft orange, "N days")
* - 0–1 days → `urgent` (soft red, "Today" / "Tomorrow")
* - past deadline → `expired` (deeper red, warning icon, "Expired")
*
* The CSS lives in `components/expiryChip.css`.
* Caller is responsible for embedding the returned HTML safely — the
* label is taken from a controlled i18n key set (no user input).
*
* @param {string | number | Date | null | undefined} value
* @returns {string} HTML snippet
*/
function formatExpiryChip(value) {
// null/undefined is a valid input meaning "no deadline".
if (value === null || value === undefined) {
const label = i18n.t('expiryChip.never', 'Never expires');
return `<span class="expiry-chip expiry-chip--never"><i class="fas fa-infinity expiry-chip__icon"></i>${escapeHtml(label)}</span>`;
}
/** @type {Date} */
let date;
if (value instanceof Date) {
date = value;
} else if (typeof value === 'number') {
date = new Date(value < 1e12 ? value * 1000 : value);
} else {
date = new Date(value);
}
if (Number.isNaN(date.getTime())) return escapeHtml(String(value));
const daysUntil = Math.floor((date.getTime() - Date.now()) / 86_400_000);
let tier;
let icon;
let label;
if (daysUntil < 0) {
tier = 'expired';
icon = 'fa-exclamation-triangle';
label = i18n.t('expiryChip.expired', 'Expired');
} else if (daysUntil === 0) {
tier = 'urgent';
icon = 'fa-clock';
label = i18n.t('expiryChip.today', 'Expires today');
} else if (daysUntil === 1) {
tier = 'urgent';
icon = 'fa-clock';
label = i18n.t('expiryChip.tomorrow', 'Expires tomorrow');
} else if (daysUntil <= 7) {
tier = 'soon';
icon = 'fa-calendar';
const translated = i18n.t('expiryChip.inDays', { count: daysUntil });
label = translated === 'expiryChip.inDays' ? `Expires in ${daysUntil} days` : translated;
} else if (daysUntil <= 30) {
tier = 'caution';
icon = 'fa-calendar';
const translated = i18n.t('expiryChip.inDays', { count: daysUntil });
label = translated === 'expiryChip.inDays' ? `Expires in ${daysUntil} days` : translated;
} else {
// Far future — show absolute date so users see the exact deadline.
tier = 'normal';
icon = 'fa-calendar';
const fmt = formatExpiryDate(date);
const translated = i18n.t('expiryChip.onDate', { date: fmt });
label = translated === 'expiryChip.onDate' ? `Expires ${fmt}` : translated;
}
return `<span class="expiry-chip expiry-chip--${tier}"><i class="fas ${icon} expiry-chip__icon"></i>${escapeHtml(label)}</span>`;
}
/**
* Maps a file size in bytes to a coarse, human-readable bucket label.
*
@@ -201,6 +358,9 @@ export {
escapeHtml,
formatDateShort,
formatDateTime,
formatDaysRemaining,
formatExpiryChip,
formatExpiryDate,
formatFileSize,
formatQuotaSize,
isEmailValid,