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 };
|