feat(list view): show the owner of the File or Folder in list view
This commit is contained in:
@@ -21,6 +21,7 @@ import { addressBook, SYSTEM_BOOK_ID } from '../model/addressBook.js';
|
||||
import { grants } from '../model/grants.js';
|
||||
import { systemUsers } from '../model/systemUsers.js';
|
||||
import { Modal } from './modal.js';
|
||||
import { _colorIndex, _initials } from './userVignette.js';
|
||||
|
||||
/** @import {FileItem, FolderItem, Grant, ContactItem, MemberEntry, LinkEntry, DraftLink, ShareRoleEnum} from '../core/types.js' */
|
||||
|
||||
@@ -73,17 +74,6 @@ function _buildMembers(grantList) {
|
||||
return members;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get initials for an avatar (up to 2 chars).
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
function _initials(name) {
|
||||
const parts = name.trim().split(/\s+/);
|
||||
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||
return name.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
// ── Component ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const shareModal = {
|
||||
@@ -291,7 +281,16 @@ const shareModal = {
|
||||
}
|
||||
debounce = setTimeout(async () => {
|
||||
const results = await addressBook.searchContacts(q, [SYSTEM_BOOK_ID]);
|
||||
this._renderSuggestions(dropdown, results.slice(0, 8), (contact) => {
|
||||
// Filter out the currently logged-in user — they cannot share with themselves
|
||||
const currentUserId = (() => {
|
||||
try {
|
||||
return /** @type {{id?:string}} */ (JSON.parse(localStorage.getItem('oxicloud_user') ?? '{}'))?.id ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
const filtered = currentUserId ? results.filter((c) => c.id !== currentUserId) : results;
|
||||
this._renderSuggestions(dropdown, filtered.slice(0, 8), (contact) => {
|
||||
this._stageUser(contact, input, dropdown, addBtn);
|
||||
});
|
||||
}, 200);
|
||||
@@ -332,13 +331,13 @@ const shareModal = {
|
||||
container.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
results.forEach((c, i) => {
|
||||
results.forEach((c) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'smd-suggestion-item';
|
||||
item.tabIndex = 0;
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = `smd-suggestion-avatar smd-avatar--${i % 5}`;
|
||||
avatar.className = `smd-suggestion-avatar uv-color-${_colorIndex(c.id)}`;
|
||||
const displayName = [c.first_name, c.last_name].filter(Boolean).join(' ') || c.full_name || c.id.slice(0, 8);
|
||||
avatar.textContent = _initials(displayName);
|
||||
|
||||
@@ -411,12 +410,12 @@ const shareModal = {
|
||||
*/
|
||||
_renderChipsInto(container) {
|
||||
container.replaceChildren();
|
||||
this._stagedUsers.forEach((c, i) => {
|
||||
this._stagedUsers.forEach((c) => {
|
||||
const chip = document.createElement('div');
|
||||
chip.className = 'smd-chip';
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = `smd-chip-avatar smd-avatar--${i % 5}`;
|
||||
avatar.className = `smd-chip-avatar uv-color-${_colorIndex(c.id)}`;
|
||||
const displayName = [c.first_name, c.last_name].filter(Boolean).join(' ') || c.full_name || c.id.slice(0, 8);
|
||||
avatar.textContent = _initials(displayName);
|
||||
|
||||
@@ -516,15 +515,15 @@ const shareModal = {
|
||||
|
||||
/**
|
||||
* @param {MemberEntry} entry
|
||||
* @param {number} idx
|
||||
* @param {number} _idx (unused — color is now derived deterministically from userId)
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
_buildMemberRow(entry, idx) {
|
||||
_buildMemberRow(entry, _idx) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'smd-member-row';
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = `smd-member-avatar smd-avatar--${idx % 5}`;
|
||||
avatar.className = `smd-member-avatar uv-color-${_colorIndex(entry.grant.subject.id)}`;
|
||||
|
||||
// Resolve display name async
|
||||
systemUsers.getDisplayName(entry.grant.subject.id).then((name) => {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* UserVignette — reusable user avatar + name inline component.
|
||||
*
|
||||
* Renders a coloured circle with initials (or photo when available) alongside
|
||||
* an asynchronously-resolved display name. Used in:
|
||||
* • Owner column (list view) via `ui.resolveOwnerCells()`
|
||||
* • ShareModal member rows / chips / suggestion items
|
||||
*
|
||||
* Usage:
|
||||
* import { createUserVignette } from './userVignette.js';
|
||||
* cell.replaceChildren(createUserVignette(userId, 'sm'));
|
||||
*/
|
||||
|
||||
import { systemUsers } from '../model/systemUsers.js';
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get initials for an avatar (1-2 characters).
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
export function _initials(name) {
|
||||
const parts = name.trim().split(/\s+/);
|
||||
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||
return name.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministic color index 0-4 derived from a userId string.
|
||||
* Same userId always maps to the same color across all components.
|
||||
* @param {string} userId
|
||||
* @returns {number}
|
||||
*/
|
||||
export function _colorIndex(userId) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < userId.length; i++) {
|
||||
hash = (hash * 31 + userId.charCodeAt(i)) | 0;
|
||||
}
|
||||
return Math.abs(hash) % 5;
|
||||
}
|
||||
|
||||
// ── Component ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @typedef {'xs'|'sm'|'md'|'lg'} VignetteSize
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a user vignette element: a coloured initials circle + async-resolved
|
||||
* display name span. The element is returned immediately with a short-UUID
|
||||
* placeholder; the name resolves in the background via `systemUsers`.
|
||||
*
|
||||
* @param {string} userId UUID of the user
|
||||
* @param {VignetteSize} [size='sm']
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function createUserVignette(userId, size = 'sm') {
|
||||
const colorIdx = _colorIndex(userId);
|
||||
|
||||
const wrapper = /** @type {HTMLElement} */ (document.createElement('span'));
|
||||
wrapper.className = `user-vignette user-vignette--${size}`;
|
||||
|
||||
const avatar = document.createElement('span');
|
||||
avatar.className = `user-vignette__avatar uv-color-${colorIdx}`;
|
||||
// Temporary placeholder: first two chars of UUID
|
||||
avatar.textContent = userId.slice(0, 2).toUpperCase();
|
||||
|
||||
const nameEl = document.createElement('span');
|
||||
nameEl.className = 'user-vignette__name';
|
||||
nameEl.textContent = `${userId.slice(0, 8)}…`;
|
||||
|
||||
wrapper.appendChild(avatar);
|
||||
wrapper.appendChild(nameEl);
|
||||
|
||||
// Resolve full name asynchronously and update both avatar initials and name
|
||||
systemUsers.getDisplayName(userId).then((name) => {
|
||||
avatar.textContent = _initials(name);
|
||||
nameEl.textContent = name;
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
Reference in New Issue
Block a user