236 lines
8.1 KiB
JavaScript
236 lines
8.1 KiB
JavaScript
/**
|
||
* 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';
|
||
import { MySharesList } from '../../components/mySharesList.js';
|
||
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';
|
||
|
||
/** @import {FileItem, FolderItem} from '../../core/types.js' */
|
||
|
||
/**
|
||
* @typedef {{ key: string, label: string, orderBy: string }} GroupByDef
|
||
* @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');
|
||
},
|
||
orderBy: 'type'
|
||
},
|
||
{
|
||
key: 'sharedWith',
|
||
get label() {
|
||
return i18n.t('groupby.sharedWith', 'Shared with');
|
||
},
|
||
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,
|
||
|
||
/** @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) => {
|
||
shareModal.open(resource, /** @type {'file'|'folder'} */ (resourceType));
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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 };
|