Skip to content
20 changes: 20 additions & 0 deletions css/apps/files.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1449,6 +1449,26 @@ table.files-filestable {
}
}

// Pending shares page: disable name link so files cannot be opened before accepting the share
body.nmc-pendingshares-view {
.files-list__row-name-link {
pointer-events: none;
cursor: default;
}

.files-list:not(.files-list--grid) .files-list__row-icon .material-design-icon svg {
width: 44px !important;
height: 36px !important;
}

.files-list__row-actions-batch-move-copy,
.files-list__row-actions-batch-download,
.files-list__row-actions-batch-delete,
.files-list__row-actions-batch-cancel_select {
display: none;
}
}

#body-user,
#body-public {

Expand Down
5 changes: 5 additions & 0 deletions css/components/ncactions.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
}
}

// Hide text label on action menu toggle buttons that show the three-dot icon
.action-item__menutoggle:has(.dots-horizontal-icon) .button-vue__text {
display: none;
}

#body-user,
#body-settings,
#body-public {
Expand Down
118 changes: 89 additions & 29 deletions src/nmcfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,27 +119,25 @@ if (document.readyState === 'loading') {
setupLinkBubbleFix()
}

const PENDING_SHARES_BODY_CLASS = 'nmc-pendingshares-view'

function isPendingSharesPage(): boolean {
return window.location.pathname.includes('/pendingshares')
}

function syncPendingSharesBodyClass(): void {
document.body.classList.toggle(PENDING_SHARES_BODY_CLASS, isPendingSharesPage())
}

/**
* Prevent row clicks and sidebar opening on pending share rows.
* Pending share rows are identified by the presence of the accept-share action button.
*/
function blockPendingShareRowClick(row: HTMLElement): void {
if (row.dataset.pendingShareBlocked === 'true') return

const nameLink = row.querySelector<HTMLElement>('.files-list__row-name-link')
if (nameLink) {
nameLink.addEventListener('click', (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
}, true)
}

// Also block row-level clicks (opens sidebar) on non-interactive cells
row.addEventListener('click', (event: MouseEvent) => {
const target = event.target as HTMLElement
const isCheckbox = target.closest('.files-list__row-checkbox')
const isActionButton = target.closest('.files-list__row-actions')
if (!isCheckbox && !isActionButton) {
if (!target.closest('.files-list__row-checkbox') && !target.closest('.files-list__row-actions')) {
event.preventDefault()
event.stopPropagation()
}
Expand All @@ -148,36 +146,98 @@ function blockPendingShareRowClick(row: HTMLElement): void {
row.dataset.pendingShareBlocked = 'true'
}

function setupPendingShareRowClickBlock(): void {
/**
* Filter menu to show only Accept/Reject share if it belongs to a pending share row.
*/
function filterPendingSharePopper(popper: Element): void {
const menu = popper.querySelector<HTMLElement>('ul[role="menu"]')
if (!menu || !menu.querySelector('.files-list__row-action-accept-share')) return

menu.querySelectorAll<HTMLElement>('li.action, li.action-separator').forEach(item => {
const keep = item.classList.contains('files-list__row-action-accept-share')
|| item.classList.contains('files-list__row-action-reject-share')
item.style.display = keep ? '' : 'none'
})
}

/**
* Patch history.pushState and history.replaceState to dispatch a custom
* 'locationchange' event, enabling detection of SPA navigation.
*/
function patchHistoryForNavigation(): void {
const dispatch = () => window.dispatchEvent(new Event('locationchange'))
const originalPush = history.pushState.bind(history)
const originalReplace = history.replaceState.bind(history)
history.pushState = (...args) => { originalPush(...args); dispatch() }
history.replaceState = (...args) => { originalReplace(...args); dispatch() }
}

/**
* Set up all pending share behaviours: row click blocking and popper menu filtering.
* A single MutationObserver handles both DOM concerns.
*/
function setupPendingShare(): void {
// Sync body class on init and on SPA navigation (pushState + popstate)
patchHistoryForNavigation()
syncPendingSharesBodyClass()
window.addEventListener('popstate', syncPendingSharesBodyClass)
window.addEventListener('locationchange', syncPendingSharesBodyClass)

// Handle rows already in the DOM on init
if (isPendingSharesPage()) {
document.querySelectorAll<HTMLElement>('tr[data-cy-files-list-row]').forEach(row => {
blockPendingShareRowClick(row)
})
}

// Single observer: watches for new rows (childList) and popper visibility changes (attributes)
const observer = new MutationObserver((mutations: MutationRecord[]) => {
for (const mutation of mutations) {
for (const node of Array.from(mutation.addedNodes)) {
if (!(node instanceof Element)) continue
const rows: HTMLElement[] = node.matches('tr[data-cy-files-list-row]')
? [node as HTMLElement]
: Array.from(node.querySelectorAll<HTMLElement>('tr[data-cy-files-list-row]'))
for (const row of rows) {
if (row.querySelector('.files-list__row-action-accept-share')) {
if (mutation.type === 'childList') {
if (!isPendingSharesPage()) continue
for (const node of Array.from(mutation.addedNodes)) {
if (!(node instanceof Element)) continue
const rows = node.matches('tr[data-cy-files-list-row]')
? [node as HTMLElement]
: Array.from(node.querySelectorAll<HTMLElement>('tr[data-cy-files-list-row]'))
for (const row of rows) {
blockPendingShareRowClick(row)
}
}
} else if (
mutation.type === 'attributes'
&& mutation.target instanceof Element
&& mutation.target.classList.contains('v-popper__popper')
&& mutation.target.classList.contains('v-popper__popper--shown')
) {
requestAnimationFrame(() => filterPendingSharePopper(mutation.target as Element))
}
}
})
observer.observe(document.body, { childList: true, subtree: true })
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class'],
})

// Handle rows already present in the DOM
document.querySelectorAll<HTMLElement>('tr[data-cy-files-list-row]').forEach(row => {
if (row.querySelector('.files-list__row-action-accept-share')) {
blockPendingShareRowClick(row)
// Click capture: retry across frames until the popper is open
document.addEventListener('click', (event: MouseEvent) => {
if (!(event.target as Element).closest('button.action-item__menutoggle')) return

const tryFilter = (remaining: number): void => {
const popper = document.querySelector('.v-popper__popper.v-popper__popper--shown')
if (popper) { filterPendingSharePopper(popper); return }
if (remaining > 0) requestAnimationFrame(() => tryFilter(remaining - 1))
}
})
requestAnimationFrame(() => tryFilter(10))
}, true)
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupPendingShareRowClickBlock)
document.addEventListener('DOMContentLoaded', setupPendingShare)
} else {
setupPendingShareRowClickBlock()
setupPendingShare()
}

window.addEventListener('DOMContentLoaded', function() {
Expand Down
Loading