644 lines
25 KiB
JavaScript
644 lines
25 KiB
JavaScript
/**
|
|
* MySharesList — row-per-grant list for the My Shares view.
|
|
*
|
|
* Both view modes emit one row per grant. Lane headers are emitted on
|
|
* grouping-key change — the server guarantees ORDER BY group key first.
|
|
*
|
|
* Modes:
|
|
* 'items' — lane = resource; row identity = subject
|
|
* 'sharedWith' — lane = user | 'links:public' | 'links:password'; row identity = resource
|
|
*/
|
|
|
|
import { formatExpiryChip } from '../core/formatters.js';
|
|
import { i18n } from '../core/i18n.js';
|
|
import { fileSharing } from '../features/sharing/fileSharing.js';
|
|
import { grants } from '../model/grants.js';
|
|
import { buildExpiryChip } from '../utils/expiryChip.js';
|
|
import { buildPasswordChip } from '../utils/passwordChip.js';
|
|
import { groupDisplayName, groupIconClass } from './groupDisplay.js';
|
|
import { createGroupVignette } from './groupVignette.js';
|
|
import { buildLinkChip } from './linkChip.js';
|
|
import { buildResourceIcon } from './resourceIcon.js';
|
|
import { buildRoleChip, roleLabel } from './roleChip.js';
|
|
import { createUserVignette } from './userVignette.js';
|
|
|
|
/**
|
|
* @import {OutgoingResourceItem, OutgoingResourceGrant, FileItem, FolderItem} from '../core/types.js'
|
|
* @typedef {'items'|'sharedWith'} ViewMode
|
|
* @typedef {'never'|'active'|'soon'|'expired'} ExpiryState
|
|
*/
|
|
|
|
const SOON_DAYS = 30;
|
|
|
|
/**
|
|
* @param {string|null|undefined} expiresAt
|
|
* @returns {ExpiryState}
|
|
*/
|
|
function _expiryState(expiresAt) {
|
|
if (!expiresAt) return 'never';
|
|
const ms = new Date(expiresAt).getTime() - Date.now();
|
|
if (ms < 0) return 'expired';
|
|
if (ms <= SOON_DAYS * 86_400_000) return 'soon';
|
|
return 'active';
|
|
}
|
|
|
|
/**
|
|
* Extract the unique group subject IDs across all grants in a page.
|
|
* Callers feed the result to `groups.resolveGroups(...)` so rows can render
|
|
* the group's display name instead of its UUID.
|
|
*
|
|
* @param {OutgoingResourceItem[]} items
|
|
* @returns {Set<string>}
|
|
*/
|
|
function collectGroupSubjectIds(items) {
|
|
const out = new Set();
|
|
for (const item of items) {
|
|
for (const g of item.grants) {
|
|
if (g.subject_type === 'group') out.add(g.subject_id);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export { collectGroupSubjectIds };
|
|
|
|
class MySharesList {
|
|
/**
|
|
* @param {HTMLElement} container
|
|
* @param {{
|
|
* onResourceOpen: (resource: FileItem|FolderItem, resourceType: string) => void,
|
|
* onShareEdit: (resource: FileItem|FolderItem, resourceType: string) => void,
|
|
* }} config
|
|
*/
|
|
constructor(container, config) {
|
|
this._container = container;
|
|
this._config = config;
|
|
/** @type {string|null} */
|
|
this._lastSwimKey = null;
|
|
/** @type {HTMLElement|null} */
|
|
this._lastSwimEl = null;
|
|
/**
|
|
* Cached map of group subject UUID → full GroupItem. Populated by
|
|
* the view via `setGroupMeta()` before each `render()` / `append()`
|
|
* so group lane headers and identity rows render with the localised
|
|
* name + virtual-aware icon.
|
|
* @type {Record<string, import('../core/types.js').GroupItem>}
|
|
*/
|
|
this._groupMeta = {};
|
|
}
|
|
|
|
/**
|
|
* Provide a resolved id→GroupItem map for group subjects expected in
|
|
* the next render / append call. Replaces (does not merge) any previous
|
|
* map.
|
|
* @param {Record<string, import('../core/types.js').GroupItem>} map
|
|
*/
|
|
setGroupMeta(map) {
|
|
this._groupMeta = map;
|
|
}
|
|
|
|
/**
|
|
* Best-effort display name for a group subject. Falls back to the UUID
|
|
* when no entry has been resolved yet — better than nothing while the
|
|
* resolve query is in flight.
|
|
* @param {string} groupId
|
|
* @returns {string}
|
|
*/
|
|
_groupName(groupId) {
|
|
const g = this._groupMeta[groupId];
|
|
return g ? groupDisplayName(g) : groupId;
|
|
}
|
|
|
|
/**
|
|
* Icon class for a group subject. Falls back to the regular group icon
|
|
* if the entry hasn't been resolved yet.
|
|
* @param {string} groupId
|
|
* @returns {string}
|
|
*/
|
|
_groupIcon(groupId) {
|
|
const g = this._groupMeta[groupId];
|
|
return g ? groupIconClass(g) : 'fa-user-group';
|
|
}
|
|
|
|
clear() {
|
|
this._container.innerHTML = '';
|
|
this._lastSwimKey = null;
|
|
this._lastSwimEl = null;
|
|
}
|
|
|
|
/**
|
|
* Full re-render (page 1).
|
|
* @param {OutgoingResourceItem[]} items
|
|
* @param {ViewMode} viewMode
|
|
*/
|
|
render(items, viewMode) {
|
|
this.clear();
|
|
this._ingest(items, viewMode);
|
|
}
|
|
|
|
/**
|
|
* Cursor append (page 2+).
|
|
* @param {OutgoingResourceItem[]} items
|
|
* @param {ViewMode} viewMode
|
|
*/
|
|
append(items, viewMode) {
|
|
this._ingest(items, viewMode);
|
|
}
|
|
|
|
// ── Core ingest ───────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* @param {OutgoingResourceItem[]} items
|
|
* @param {ViewMode} viewMode
|
|
*/
|
|
_ingest(items, viewMode) {
|
|
for (const item of items) {
|
|
if (viewMode === 'items') {
|
|
this._ingestItemsMode(item);
|
|
} else {
|
|
this._ingestSharedWithMode(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Items mode — one lane per resource, one grant row per grant.
|
|
* @param {OutgoingResourceItem} item
|
|
*/
|
|
_ingestItemsMode(item) {
|
|
const swimKey = `resource:${item.resource.id}`;
|
|
const laneBody = this._ensureLane(swimKey, () => this._buildResourceLaneHeader(item));
|
|
for (const grant of item.grants) {
|
|
laneBody.appendChild(this._buildGrantRow(grant, item, 'items'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* SharedWith mode — one lane per user or per link bucket, one row per grant.
|
|
* @param {OutgoingResourceItem} item
|
|
*/
|
|
_ingestSharedWithMode(item) {
|
|
for (const grant of item.grants) {
|
|
let swimKey;
|
|
if (grant.subject_type === 'user') {
|
|
swimKey = `user:${grant.subject_id}`;
|
|
} else if (grant.subject_type === 'group') {
|
|
swimKey = `group:${grant.subject_id}`;
|
|
} else if (grant.has_password) {
|
|
swimKey = 'links:password';
|
|
} else {
|
|
swimKey = 'links:public';
|
|
}
|
|
const laneBody = this._ensureLane(swimKey, () => this._buildSubjectLaneHeader(swimKey, grant));
|
|
laneBody.appendChild(this._buildGrantRow(grant, item, 'sharedWith'));
|
|
}
|
|
}
|
|
|
|
// ── Lane management ───────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Return the existing lane body when swimKey matches, else create a new lane.
|
|
* @param {string} swimKey
|
|
* @param {() => HTMLElement} buildHeader
|
|
* @returns {HTMLElement}
|
|
*/
|
|
_ensureLane(swimKey, buildHeader) {
|
|
if (swimKey === this._lastSwimKey && this._lastSwimEl) return this._lastSwimEl;
|
|
|
|
const lane = document.createElement('div');
|
|
lane.className = 'ms-lane';
|
|
lane.dataset.swimKey = swimKey;
|
|
|
|
const header = document.createElement('div');
|
|
header.className = 'ms-lane__header';
|
|
header.appendChild(buildHeader());
|
|
lane.appendChild(header);
|
|
|
|
const body = document.createElement('div');
|
|
body.className = 'ms-lane__body';
|
|
lane.appendChild(body);
|
|
|
|
this._container.appendChild(lane);
|
|
this._lastSwimKey = swimKey;
|
|
this._lastSwimEl = body;
|
|
return body;
|
|
}
|
|
|
|
/**
|
|
* Lane header for items mode: resource icon + name link + Edit sharing button.
|
|
* @param {OutgoingResourceItem} item
|
|
* @returns {HTMLElement}
|
|
*/
|
|
_buildResourceLaneHeader(item) {
|
|
const row = document.createElement('div');
|
|
row.className = 'ms-resource-row';
|
|
if (item.resource.path) row.dataset.path = item.resource.path;
|
|
if (item.resource.owner_id) row.dataset.ownerId = item.resource.owner_id;
|
|
|
|
row.appendChild(buildResourceIcon(item.resource, item.resource_type));
|
|
|
|
const nameLink = document.createElement('a');
|
|
nameLink.className = 'ms-resource-row__name';
|
|
nameLink.href = '#';
|
|
nameLink.textContent = item.resource.name;
|
|
nameLink.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
this._config.onResourceOpen(item.resource, item.resource_type);
|
|
});
|
|
row.appendChild(nameLink);
|
|
|
|
const editBtn = document.createElement('button');
|
|
editBtn.className = 'ms-resource-row__edit button ghost';
|
|
editBtn.innerHTML = `<i class="fas fa-pencil-alt"></i> ${i18n.t('myshares.editSharing', 'Edit sharing')}`;
|
|
editBtn.addEventListener('click', () => this._config.onShareEdit(item.resource, item.resource_type));
|
|
row.appendChild(editBtn);
|
|
|
|
return row;
|
|
}
|
|
|
|
/**
|
|
* Lane header for sharedWith mode: user vignette or link bucket label.
|
|
* @param {string} swimKey
|
|
* @param {OutgoingResourceGrant} grant
|
|
* @returns {HTMLElement}
|
|
*/
|
|
_buildSubjectLaneHeader(swimKey, grant) {
|
|
if (swimKey.startsWith('user:')) {
|
|
return createUserVignette(grant.subject_id, 'list');
|
|
}
|
|
if (swimKey.startsWith('group:')) {
|
|
return createGroupVignette(this._groupName(grant.subject_id), 'list', {
|
|
icon: this._groupIcon(grant.subject_id)
|
|
});
|
|
}
|
|
const el = document.createElement('div');
|
|
el.className = 'ms-link-lane-label';
|
|
const icon = document.createElement('i');
|
|
if (swimKey === 'links:password') {
|
|
icon.className = 'fas fa-lock ms-link-lane-label__icon';
|
|
el.appendChild(icon);
|
|
el.appendChild(document.createTextNode(` ${i18n.t('myshares.passwordLinks', 'Password-protected links')}`));
|
|
} else {
|
|
icon.className = 'fas fa-link ms-link-lane-label__icon';
|
|
el.appendChild(icon);
|
|
el.appendChild(document.createTextNode(` ${i18n.t('myshares.publicLinks', 'Public links')}`));
|
|
}
|
|
return el;
|
|
}
|
|
|
|
// ── Grant row ─────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* One grant row: identity + role pill + expiry chip + ⋯ button.
|
|
* @param {OutgoingResourceGrant} grant
|
|
* @param {OutgoingResourceItem} item
|
|
* @param {ViewMode} viewMode
|
|
* @returns {HTMLElement}
|
|
*/
|
|
_buildGrantRow(grant, item, viewMode) {
|
|
const row = document.createElement('div');
|
|
row.className = 'ms-grant-row';
|
|
if (_expiryState(grant.expires_at ?? null) === 'expired') {
|
|
row.classList.add('ms-grant-row--expired');
|
|
}
|
|
// In sharedWith mode each grant row represents a (resource → subject)
|
|
// pair, so stamp the resource hierarchy info for the hover tooltip.
|
|
// In items mode the row represents a subject — no resource attrs.
|
|
if (viewMode === 'sharedWith') {
|
|
if (item.resource.path) row.dataset.path = item.resource.path;
|
|
if (item.resource.owner_id) row.dataset.ownerId = item.resource.owner_id;
|
|
}
|
|
|
|
row.appendChild(this._buildIdentity(grant, item, viewMode));
|
|
row.appendChild(this._buildRolePill(grant.role));
|
|
row.appendChild(this._buildExpiryChip(grant.expires_at ?? null));
|
|
row.appendChild(this._buildKebabBtn(grant, item, row));
|
|
|
|
return row;
|
|
}
|
|
|
|
/**
|
|
* Identity: user vignette or link icon + name; tokens in sharedWith mode add → resource.
|
|
* @param {OutgoingResourceGrant} grant
|
|
* @param {OutgoingResourceItem} item
|
|
* @param {ViewMode} viewMode
|
|
* @returns {HTMLElement}
|
|
*/
|
|
_buildIdentity(grant, item, viewMode) {
|
|
const el = document.createElement('div');
|
|
el.className = 'ms-grant-row__identity';
|
|
|
|
if ((grant.subject_type === 'user' || grant.subject_type === 'group') && viewMode === 'sharedWith') {
|
|
// Lane header is already the subject — show the resource instead.
|
|
el.appendChild(buildResourceIcon(item.resource, item.resource_type));
|
|
const nameLink = document.createElement('a');
|
|
nameLink.className = 'ms-identity__resource-name';
|
|
nameLink.href = '#';
|
|
nameLink.textContent = item.resource.name;
|
|
nameLink.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
this._config.onResourceOpen(item.resource, item.resource_type);
|
|
});
|
|
el.appendChild(nameLink);
|
|
} else if (grant.subject_type === 'user') {
|
|
el.appendChild(createUserVignette(grant.subject_id, 'xs'));
|
|
} else if (grant.subject_type === 'group') {
|
|
el.appendChild(
|
|
createGroupVignette(this._groupName(grant.subject_id), 'xs', {
|
|
icon: this._groupIcon(grant.subject_id)
|
|
})
|
|
);
|
|
} else {
|
|
// Token — link chip handles icon + label + copy-on-click
|
|
el.appendChild(buildLinkChip(grant));
|
|
|
|
if (viewMode === 'sharedWith') {
|
|
const arrow = document.createElement('span');
|
|
arrow.className = 'ms-link-identity__arrow';
|
|
arrow.textContent = '→';
|
|
el.appendChild(arrow);
|
|
|
|
const resLink = document.createElement('a');
|
|
resLink.className = 'ms-link-identity__resource';
|
|
resLink.href = '#';
|
|
resLink.appendChild(buildResourceIcon(item.resource, item.resource_type));
|
|
resLink.appendChild(document.createTextNode(` ${item.resource.name}`));
|
|
resLink.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
this._config.onResourceOpen(item.resource, item.resource_type);
|
|
});
|
|
el.appendChild(resLink);
|
|
}
|
|
}
|
|
|
|
return el;
|
|
}
|
|
|
|
/** @param {string} role @returns {HTMLElement} */
|
|
_buildRolePill(role) {
|
|
return buildRoleChip(role);
|
|
}
|
|
|
|
/**
|
|
* Build the expiry chip as a DOM element.
|
|
*
|
|
* Delegates label/tier/icon decisions to the shared `formatExpiryChip`
|
|
* helper (used by Trash too) so all expiration chips look identical
|
|
* across the app and stay in sync as the design evolves.
|
|
*
|
|
* @param {string|null} expiresAt
|
|
* @returns {HTMLElement}
|
|
*/
|
|
_buildExpiryChip(expiresAt) {
|
|
const tpl = document.createElement('template');
|
|
tpl.innerHTML = formatExpiryChip(expiresAt);
|
|
return /** @type {HTMLElement} */ (tpl.content.firstElementChild);
|
|
}
|
|
|
|
// ── Kebab menu ────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* @param {OutgoingResourceGrant} grant
|
|
* @param {OutgoingResourceItem} item
|
|
* @param {HTMLElement} rowEl
|
|
* @returns {HTMLButtonElement}
|
|
*/
|
|
_buildKebabBtn(grant, item, rowEl) {
|
|
const btn = /** @type {HTMLButtonElement} */ (document.createElement('button'));
|
|
btn.className = 'ms-kebab-btn ms-btn-icon';
|
|
btn.setAttribute('aria-label', i18n.t('myshares.manageAccess', 'Manage access'));
|
|
btn.innerHTML = '<i class="fas fa-ellipsis-v"></i>';
|
|
btn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
this._openGrantMenu(btn, grant, item, rowEl);
|
|
});
|
|
return btn;
|
|
}
|
|
|
|
/**
|
|
* Build and show a dynamic context menu positioned below the trigger button.
|
|
* @param {HTMLButtonElement} btn
|
|
* @param {OutgoingResourceGrant} grant
|
|
* @param {OutgoingResourceItem} item
|
|
* @param {HTMLElement} rowEl
|
|
*/
|
|
_openGrantMenu(btn, grant, item, rowEl) {
|
|
document.querySelector('.ms-grant-menu')?.remove();
|
|
|
|
const menu = document.createElement('div');
|
|
menu.className = 'context-menu ms-grant-menu';
|
|
|
|
// Current expiry as YYYY-MM-DD (or null)
|
|
const initialExpiry = grant.expires_at ? String(grant.expires_at).slice(0, 10) : null;
|
|
|
|
if (grant.subject_type === 'user' || grant.subject_type === 'group') {
|
|
for (const role of /** @type {('admin'|'editor'|'viewer')[]} */ (['admin', 'editor', 'viewer'])) {
|
|
const isCurrent = grant.role === role;
|
|
const mi = this._menuItem(isCurrent ? 'fas fa-check' : '', roleLabel(role), false, async () => {
|
|
menu.remove();
|
|
if (isCurrent) return;
|
|
await grants.updateRole({
|
|
subject: { type: grant.subject_type, id: grant.subject_id },
|
|
resource: { type: item.resource_type, id: item.resource.id },
|
|
role
|
|
});
|
|
const pill = rowEl.querySelector('.role-chip');
|
|
if (pill) pill.replaceWith(buildRoleChip(role));
|
|
grant.role = role;
|
|
});
|
|
if (isCurrent) mi.classList.add('ms-menu-item--current');
|
|
menu.appendChild(mi);
|
|
}
|
|
menu.appendChild(this._menuSeparator());
|
|
menu.appendChild(this._menuExpiryRow(grant, item, rowEl, initialExpiry));
|
|
menu.appendChild(this._menuSeparator());
|
|
const removeIcon = grant.subject_type === 'group' ? 'fas fa-user-group' : 'fas fa-user-times';
|
|
menu.appendChild(
|
|
this._menuItem(removeIcon, i18n.t('myshares.removeAccess', 'Remove access'), true, async () => {
|
|
menu.remove();
|
|
await grants.revokeGrant(grant.grant_id);
|
|
this._removeRowAndCleanLane(rowEl);
|
|
})
|
|
);
|
|
} else {
|
|
menu.appendChild(
|
|
this._menuItem('fas fa-copy', i18n.t('myshares.copyLink', 'Copy link'), false, async () => {
|
|
menu.remove();
|
|
const share = await fileSharing.getShareById(grant.subject_id);
|
|
await fileSharing.copyLinkToClipboard(share.url);
|
|
})
|
|
);
|
|
menu.appendChild(this._menuSeparator());
|
|
menu.appendChild(this._menuExpiryRow(grant, item, rowEl, initialExpiry));
|
|
menu.appendChild(this._menuPasswordRow(grant, rowEl));
|
|
menu.appendChild(this._menuSeparator());
|
|
menu.appendChild(
|
|
this._menuItem('fas fa-trash', i18n.t('myshares.deleteLink', 'Delete link'), true, async () => {
|
|
menu.remove();
|
|
await fileSharing.removeSharedLink(grant.subject_id);
|
|
this._removeRowAndCleanLane(rowEl);
|
|
})
|
|
);
|
|
}
|
|
|
|
document.body.appendChild(menu);
|
|
|
|
// Position below the trigger, right-aligned to it, clamped to viewport
|
|
const rect = btn.getBoundingClientRect();
|
|
const mw = menu.offsetWidth || 200;
|
|
const left = Math.min(rect.right - mw, window.innerWidth - mw - 8);
|
|
menu.style.position = 'absolute';
|
|
menu.style.top = `${rect.bottom + window.scrollY + 4}px`;
|
|
menu.style.left = `${Math.max(8, left)}px`;
|
|
|
|
const close = (/** @type {Event} */ e) => {
|
|
if (e.type === 'keydown' && /** @type {KeyboardEvent} */ (e).key !== 'Escape') return;
|
|
// Keep menu open when interacting with elements inside it (e.g. the date input)
|
|
if (e.type === 'click' && menu.contains(/** @type {Node} */ (e.target))) return;
|
|
menu.remove();
|
|
document.removeEventListener('click', close, true);
|
|
document.removeEventListener('keydown', close, true);
|
|
};
|
|
setTimeout(() => {
|
|
document.addEventListener('click', close, true);
|
|
document.addEventListener('keydown', close, true);
|
|
}, 0);
|
|
}
|
|
|
|
/**
|
|
* Non-closing expiry row embedded in the context menu.
|
|
* Uses the shared smd-expiry-chip; saves on blur/Enter.
|
|
* @param {OutgoingResourceGrant} grant
|
|
* @param {OutgoingResourceItem} item
|
|
* @param {HTMLElement} rowEl
|
|
* @param {string|null} initialExpiry YYYY-MM-DD or null
|
|
* @returns {HTMLElement}
|
|
*/
|
|
_menuExpiryRow(grant, item, rowEl, initialExpiry) {
|
|
const row = document.createElement('div');
|
|
row.className = 'ms-menu-expiry-row';
|
|
|
|
const label = document.createElement('span');
|
|
label.className = 'ms-menu-expiry-label';
|
|
label.textContent = i18n.t('share.expiry', 'Expiry');
|
|
row.appendChild(label);
|
|
|
|
const chip = buildExpiryChip(initialExpiry, async (dateStr) => {
|
|
const expiresIso = dateStr ? new Date(`${dateStr}T00:00:00Z`).toISOString() : null;
|
|
try {
|
|
await grants.updateRole({
|
|
subject: { type: grant.subject_type, id: grant.subject_id },
|
|
resource: { type: item.resource_type, id: item.resource.id },
|
|
role: grant.role,
|
|
expires_at: expiresIso
|
|
});
|
|
grant.expires_at = expiresIso;
|
|
// Replace the display chip in the grant row. The chip class
|
|
// is `expiry-chip` (emitted by `formatExpiryChip` in
|
|
// core/formatters.js) — NOT `ms-expiry-chip`. The earlier
|
|
// selector silently no-op'd, which is why the row never
|
|
// refreshed after an expiry change.
|
|
const displayChip = rowEl.querySelector('.expiry-chip');
|
|
if (displayChip) {
|
|
const newChip = this._buildExpiryChip(expiresIso);
|
|
displayChip.replaceWith(newChip);
|
|
}
|
|
// Keep the row's expired styling in sync (0.6 opacity).
|
|
rowEl.classList.toggle('ms-grant-row--expired', _expiryState(expiresIso) === 'expired');
|
|
} catch (err) {
|
|
console.error('mySharesList: setExpiry failed', err);
|
|
}
|
|
});
|
|
row.appendChild(chip);
|
|
|
|
return row;
|
|
}
|
|
|
|
/**
|
|
* Non-closing password row embedded in the link context menu.
|
|
* Saves immediately on confirm (blur / Enter).
|
|
* @param {OutgoingResourceGrant} grant
|
|
* @param {HTMLElement} rowEl
|
|
* @returns {HTMLElement}
|
|
*/
|
|
_menuPasswordRow(grant, rowEl) {
|
|
const row = document.createElement('div');
|
|
row.className = 'ms-menu-expiry-row';
|
|
|
|
const label = document.createElement('span');
|
|
label.className = 'ms-menu-expiry-label';
|
|
label.textContent = i18n.t('share.password', 'Password');
|
|
row.appendChild(label);
|
|
|
|
const chip = buildPasswordChip(grant.has_password, async (newPassword) => {
|
|
try {
|
|
await fileSharing.updateSharedLink(grant.subject_id, {
|
|
password: newPassword || null
|
|
});
|
|
grant.has_password = !!newPassword;
|
|
// Update the lock icon on the link chip in the row
|
|
const linkChipEl = rowEl.querySelector('.link-chip');
|
|
if (linkChipEl) {
|
|
linkChipEl.classList.toggle('link-chip--locked', grant.has_password);
|
|
const iconEl = linkChipEl.querySelector('.link-chip__icon');
|
|
if (iconEl) {
|
|
iconEl.className = grant.has_password ? 'fas fa-lock link-chip__icon' : 'fas fa-link link-chip__icon';
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('mySharesList: setPassword failed', err);
|
|
}
|
|
});
|
|
row.appendChild(chip);
|
|
|
|
return row;
|
|
}
|
|
|
|
/**
|
|
* @param {string} iconClass
|
|
* @param {string} label
|
|
* @param {boolean} danger
|
|
* @param {() => void} onClick
|
|
* @returns {HTMLElement}
|
|
*/
|
|
_menuItem(iconClass, label, danger, onClick) {
|
|
const el = document.createElement('div');
|
|
el.className = danger ? 'context-menu-item context-menu-item-danger' : 'context-menu-item';
|
|
el.setAttribute('role', 'menuitem');
|
|
if (iconClass) {
|
|
el.innerHTML = `<i class="${iconClass}"></i> `;
|
|
}
|
|
el.appendChild(document.createTextNode(label));
|
|
el.addEventListener('click', /** @type {EventListener} */ (onClick));
|
|
return el;
|
|
}
|
|
|
|
/** @returns {HTMLElement} */
|
|
_menuSeparator() {
|
|
const el = document.createElement('div');
|
|
el.className = 'context-menu-separator';
|
|
return el;
|
|
}
|
|
|
|
/**
|
|
* Remove the row; if the lane body is now empty, remove the whole lane.
|
|
* @param {HTMLElement} rowEl
|
|
*/
|
|
_removeRowAndCleanLane(rowEl) {
|
|
const laneBody = rowEl.closest('.ms-lane__body');
|
|
rowEl.remove();
|
|
if (laneBody instanceof HTMLElement && laneBody.children.length === 0) {
|
|
const lane = laneBody.closest('.ms-lane');
|
|
if (lane instanceof HTMLElement) {
|
|
if (lane.dataset.swimKey === this._lastSwimKey) {
|
|
this._lastSwimKey = null;
|
|
this._lastSwimEl = null;
|
|
}
|
|
lane.remove();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export { MySharesList };
|