2026-05-29 02:16:15 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* OxiCloud – "My Shares" view.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Renders resources the current user has shared with others, using the
|
|
|
|
|
|
* cursor-paginated `GET /api/grants/outgoing/resources` endpoint.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Group-by modes exposed to the navigation toolbar:
|
|
|
|
|
|
* 'items' — one card per resource (sort_by=type) [default / None]
|
|
|
|
|
|
* 'sharedWith' — swimlanes per subject (sort_by=subject)
|
|
|
|
|
|
*
|
|
|
|
|
|
* The "None" option (key='') from the toolbar maps to the default 'items' mode.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { ui } from '../../app/ui.js';
|
2026-05-30 23:35:47 +02:00
|
|
|
|
import { collectGroupSubjectIds, MySharesList } from '../../components/mySharesList.js';
|
2026-05-29 02:16:15 +02:00
|
|
|
|
import { shareModal } from '../../components/shareModal.js';
|
|
|
|
|
|
import { i18n } from '../../core/i18n.js';
|
|
|
|
|
|
import * as viewPrefs from '../../core/viewPrefs.js';
|
|
|
|
|
|
import * as itemTooltip from '../../features/itemTooltip.js';
|
|
|
|
|
|
import { grants } from '../../model/grants.js';
|
2026-05-30 23:35:47 +02:00
|
|
|
|
import { groups } from '../../model/groups.js';
|
2026-05-29 02:16:15 +02:00
|
|
|
|
|
|
|
|
|
|
/** @import {FileItem, FolderItem} from '../../core/types.js' */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-30 11:15:46 +02:00
|
|
|
|
* @typedef {{ key: string, label: string, icon?: string, orderBy: string }} GroupByDef
|
2026-05-29 02:16:15 +02:00
|
|
|
|
* @typedef {'items'|'sharedWith'} ViewMode
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @type {{ [key: string]: { orderBy: string, viewMode: ViewMode } }}
|
|
|
|
|
|
*/
|
|
|
|
|
|
const MODE_MAP = {
|
|
|
|
|
|
'': { orderBy: 'type', viewMode: 'items' },
|
|
|
|
|
|
items: { orderBy: 'type', viewMode: 'items' },
|
|
|
|
|
|
sharedWith: { orderBy: 'subject', viewMode: 'sharedWith' }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** @type {GroupByDef[]} */
|
|
|
|
|
|
const GROUP_BY_DEFS = [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: '',
|
|
|
|
|
|
get label() {
|
|
|
|
|
|
return i18n.t('groupby.byFiles', 'By files');
|
|
|
|
|
|
},
|
2026-05-30 11:15:46 +02:00
|
|
|
|
icon: 'fas fa-layer-group',
|
2026-05-29 02:16:15 +02:00
|
|
|
|
orderBy: 'type'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'sharedWith',
|
|
|
|
|
|
get label() {
|
|
|
|
|
|
return i18n.t('groupby.sharedWith', 'Shared with');
|
|
|
|
|
|
},
|
2026-05-30 11:15:46 +02:00
|
|
|
|
icon: 'fas fa-layer-group',
|
2026-05-29 02:16:15 +02:00
|
|
|
|
orderBy: 'subject'
|
|
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/** ID of the "Load more" wrapper injected below `.files-container`. */
|
|
|
|
|
|
const LOAD_MORE_ID = 'ms-load-more-wrapper';
|
|
|
|
|
|
|
|
|
|
|
|
const mySharesView = {
|
|
|
|
|
|
// ── State ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/** @type {string|null} */
|
|
|
|
|
|
_nextCursor: null,
|
|
|
|
|
|
|
|
|
|
|
|
_loading: false,
|
|
|
|
|
|
|
|
|
|
|
|
/** @type {MySharesList|null} */
|
|
|
|
|
|
_component: null,
|
|
|
|
|
|
|
2026-05-30 23:35:47 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Cumulative id→GroupItem map of every group subject seen so far in this
|
|
|
|
|
|
* session. Each `_loadPage()` merges newly resolved entries so subsequent
|
|
|
|
|
|
* pages don't re-resolve already-known groups.
|
|
|
|
|
|
* @type {Record<string, import('../../core/types.js').GroupItem>}
|
|
|
|
|
|
*/
|
|
|
|
|
|
_knownGroupMeta: {},
|
|
|
|
|
|
|
2026-05-29 02:16:15 +02:00
|
|
|
|
/** @type {string} */
|
|
|
|
|
|
_groupBy: '',
|
|
|
|
|
|
|
|
|
|
|
|
/** @type {boolean} */
|
|
|
|
|
|
_reversed: false,
|
|
|
|
|
|
|
|
|
|
|
|
// ── Public API ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/** @returns {GroupByDef[]} */
|
|
|
|
|
|
get groupByDefs() {
|
|
|
|
|
|
return GROUP_BY_DEFS;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Change the active group-by dimension and reload from page 1.
|
|
|
|
|
|
* Called by navigation.js when the user picks a pill.
|
|
|
|
|
|
* Empty string '' maps to the default items mode.
|
|
|
|
|
|
* @param {string} key
|
|
|
|
|
|
*/
|
|
|
|
|
|
setGroupBy(key) {
|
|
|
|
|
|
if (this._groupBy === key) return;
|
|
|
|
|
|
this._groupBy = key;
|
|
|
|
|
|
viewPrefs.save('shared', this._groupBy, this._reversed, viewPrefs.load('shared').view);
|
|
|
|
|
|
this._nextCursor = null;
|
|
|
|
|
|
this._component?.clear();
|
|
|
|
|
|
this._loadPage();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Flip sort direction and reload from page 1.
|
|
|
|
|
|
* @param {boolean} reversed
|
|
|
|
|
|
*/
|
|
|
|
|
|
setDirection(reversed) {
|
|
|
|
|
|
if (this._reversed === reversed) return;
|
|
|
|
|
|
this._reversed = reversed;
|
|
|
|
|
|
viewPrefs.save('shared', this._groupBy, this._reversed, viewPrefs.load('shared').view);
|
|
|
|
|
|
this._nextCursor = null;
|
|
|
|
|
|
this._component?.clear();
|
|
|
|
|
|
this._loadPage();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async init() {
|
|
|
|
|
|
this._nextCursor = null;
|
|
|
|
|
|
this._loading = false;
|
|
|
|
|
|
const saved = viewPrefs.load('shared');
|
|
|
|
|
|
this._groupBy = saved.groupBy || '';
|
|
|
|
|
|
this._reversed = saved.reversed;
|
|
|
|
|
|
|
|
|
|
|
|
this._ensureLoadMoreButton();
|
|
|
|
|
|
|
|
|
|
|
|
ui.resetFilesList();
|
|
|
|
|
|
ui.updateBreadcrumb();
|
|
|
|
|
|
|
|
|
|
|
|
const filesList = document.getElementById('files-list');
|
|
|
|
|
|
if (filesList) {
|
|
|
|
|
|
if (!this._component) {
|
|
|
|
|
|
this._component = new MySharesList(filesList, {
|
|
|
|
|
|
onResourceOpen: (resource, resourceType) => {
|
|
|
|
|
|
if (resourceType === 'folder') {
|
|
|
|
|
|
ui.openItem(resource);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// File: navigate to the parent folder in Files section.
|
|
|
|
|
|
const file = /** @type {FileItem} */ (resource);
|
|
|
|
|
|
const parts = (file.path || '').split('/').filter(Boolean);
|
|
|
|
|
|
const parentName = parts.length >= 2 ? parts[parts.length - 2] : '';
|
|
|
|
|
|
ui.openItem(/** @type {FolderItem} */ ({ id: file.folder_id, name: parentName }));
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onShareEdit: (resource, resourceType) => {
|
2026-05-29 13:43:15 +02:00
|
|
|
|
shareModal.open(resource, /** @type {'file'|'folder'} */ (resourceType), () => {
|
|
|
|
|
|
this._nextCursor = null;
|
|
|
|
|
|
this._component?.clear();
|
|
|
|
|
|
this._loadPage();
|
|
|
|
|
|
});
|
2026-05-29 02:16:15 +02:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this._loadPage();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
hide() {
|
|
|
|
|
|
const w = document.getElementById(LOAD_MORE_ID);
|
|
|
|
|
|
if (w) w.classList.add('hidden');
|
|
|
|
|
|
const filesList = document.getElementById('files-list');
|
|
|
|
|
|
if (filesList) itemTooltip.destroy(filesList);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── Internal helpers ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
async _loadPage() {
|
|
|
|
|
|
if (this._loading) return;
|
|
|
|
|
|
this._loading = true;
|
|
|
|
|
|
|
|
|
|
|
|
const isFirstPage = this._nextCursor === null;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const mode = MODE_MAP[this._groupBy] ?? MODE_MAP[''];
|
|
|
|
|
|
|
|
|
|
|
|
const data = await grants.fetchMySharesPage({
|
|
|
|
|
|
limit: 50,
|
|
|
|
|
|
cursor: this._nextCursor ?? undefined,
|
|
|
|
|
|
orderBy: mode.orderBy,
|
|
|
|
|
|
reverse: this._reversed
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this._nextCursor = data.next_cursor ?? null;
|
|
|
|
|
|
|
|
|
|
|
|
if (data.items.length === 0 && isFirstPage) {
|
|
|
|
|
|
ui.showError(`
|
|
|
|
|
|
<i class="fas fa-share-alt empty-state-icon"></i>
|
|
|
|
|
|
<p>${i18n.t('myshares.emptyStateTitle', "You haven't shared anything yet")}</p>
|
|
|
|
|
|
<p>${i18n.t('myshares.emptyStateDesc', 'Items you share with others will appear here')}</p>
|
|
|
|
|
|
`);
|
|
|
|
|
|
this._setLoadMoreVisible(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-30 23:35:47 +02:00
|
|
|
|
// Resolve full GroupItem records for any group-subject grants on
|
|
|
|
|
|
// this page so lane headers and identity rows render the localised
|
|
|
|
|
|
// name + virtual-aware icon. Cheap — one search call shared across
|
|
|
|
|
|
// every group visible on the page.
|
|
|
|
|
|
if (this._component) {
|
|
|
|
|
|
const groupIds = collectGroupSubjectIds(data.items);
|
|
|
|
|
|
if (groupIds.size > 0) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const resolved = await groups.resolveGroups(groupIds);
|
|
|
|
|
|
const merged = { ...this._knownGroupMeta, ...resolved };
|
|
|
|
|
|
this._component.setGroupMeta(merged);
|
|
|
|
|
|
this._knownGroupMeta = merged;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn('mySharesView: failed to resolve group names', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 02:16:15 +02:00
|
|
|
|
if (isFirstPage) {
|
|
|
|
|
|
this._component?.render(data.items, mode.viewMode);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this._component?.append(data.items, mode.viewMode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const filesList = document.getElementById('files-list');
|
|
|
|
|
|
if (filesList) itemTooltip.init(filesList);
|
|
|
|
|
|
|
|
|
|
|
|
this._setLoadMoreVisible(!!this._nextCursor);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
ui.showError(`
|
|
|
|
|
|
<i class="fas fa-exclamation-circle empty-state-icon error"></i>
|
|
|
|
|
|
<p>${i18n.t('errors_loadFailed', 'Failed to load items')}</p>
|
|
|
|
|
|
`);
|
|
|
|
|
|
console.error('mySharesView: load error', err);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this._loading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── "Load more" button ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
_ensureLoadMoreButton() {
|
|
|
|
|
|
if (document.getElementById(LOAD_MORE_ID)) return;
|
|
|
|
|
|
|
|
|
|
|
|
const filesContainer = document.querySelector('.files-container');
|
|
|
|
|
|
if (!filesContainer) return;
|
|
|
|
|
|
|
|
|
|
|
|
const wrapper = document.createElement('div');
|
|
|
|
|
|
wrapper.id = LOAD_MORE_ID;
|
|
|
|
|
|
wrapper.className = 'ms-load-more-wrapper hidden';
|
|
|
|
|
|
|
|
|
|
|
|
const btn = document.createElement('button');
|
|
|
|
|
|
btn.id = 'ms-load-more';
|
|
|
|
|
|
btn.className = 'button secondary';
|
|
|
|
|
|
btn.textContent = i18n.t('myshares.loadMore', 'Load more');
|
|
|
|
|
|
btn.addEventListener('click', () => this._loadPage());
|
|
|
|
|
|
|
|
|
|
|
|
wrapper.appendChild(btn);
|
|
|
|
|
|
filesContainer.after(wrapper);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/** @param {boolean} visible */
|
|
|
|
|
|
_setLoadMoreVisible(visible) {
|
|
|
|
|
|
const w = document.getElementById(LOAD_MORE_ID);
|
|
|
|
|
|
if (w) w.classList.toggle('hidden', !visible);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export { mySharesView };
|