feat: implement full breadcrumb path navigation in Files tab
- Add breadcrumbPath array to app state for tracking folder hierarchy - Rewrite updateBreadcrumb() to render full path: Home > folder > subfolder - Each breadcrumb segment is clickable to navigate back to that level - Current folder shown in bold (non-clickable), parent folders as links - Reset breadcrumb path on tab switch, home navigation, and initial load - Update navigateFolder/selectFolder to push to breadcrumb path - Enhanced breadcrumb CSS with hover effects and dark mode support
This commit is contained in:
@@ -205,17 +205,20 @@ async function resolveHomeFolder() {
|
||||
app.userHomeFolderId = home.id;
|
||||
app.userHomeFolderName = home.name;
|
||||
app.currentPath = home.id;
|
||||
window.ui.updateBreadcrumb(home.name);
|
||||
app.breadcrumbPath = [];
|
||||
window.ui.updateBreadcrumb();
|
||||
console.log(`Home folder resolved: ${home.name} (${home.id})`);
|
||||
} else {
|
||||
console.warn('No root folders found for user');
|
||||
app.currentPath = '';
|
||||
window.ui.updateBreadcrumb('');
|
||||
app.breadcrumbPath = [];
|
||||
window.ui.updateBreadcrumb();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error resolving home folder:', error);
|
||||
app.currentPath = '';
|
||||
window.ui.updateBreadcrumb('');
|
||||
app.breadcrumbPath = [];
|
||||
window.ui.updateBreadcrumb();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ async function loadFiles(options = {}) {
|
||||
if (app.userHomeFolderId) {
|
||||
url = `/api/folders/${app.userHomeFolderId}/listing?t=${timestamp}`;
|
||||
app.currentPath = app.userHomeFolderId;
|
||||
window.ui.updateBreadcrumb(app.userHomeFolderName || 'Home');
|
||||
app.breadcrumbPath = [];
|
||||
window.ui.updateBreadcrumb();
|
||||
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
|
||||
} else {
|
||||
url = `/api/folders?t=${timestamp}`;
|
||||
|
||||
+10
-1
@@ -52,6 +52,14 @@ const ACTIONS_BAR_TEMPLATES = {
|
||||
<span data-i18n="trash.empty_trash">Empty trash</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="view-toggle">
|
||||
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
|
||||
<i class="fas fa-th"></i>
|
||||
</button>
|
||||
<button class="toggle-btn" id="list-view-btn" title="List view">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
favorites: `
|
||||
<div class="action-buttons"></div>
|
||||
@@ -537,8 +545,9 @@ window.setActionsBarMode = setActionsBarMode;
|
||||
|
||||
// Set up global selectFolder function for navigation
|
||||
window.selectFolder = (id, name) => {
|
||||
app.breadcrumbPath.push({ id, name });
|
||||
app.currentPath = id;
|
||||
ui.updateBreadcrumb(name);
|
||||
ui.updateBreadcrumb();
|
||||
window.loadFiles();
|
||||
};
|
||||
|
||||
|
||||
@@ -76,7 +76,8 @@ function switchToFilesView() {
|
||||
|
||||
if (window.app.userHomeFolderId) {
|
||||
window.app.currentPath = window.app.userHomeFolderId;
|
||||
window.ui.updateBreadcrumb(window.app.userHomeFolderName || 'Home');
|
||||
window.app.breadcrumbPath = [];
|
||||
window.ui.updateBreadcrumb();
|
||||
} else {
|
||||
window.app.currentPath = '';
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ window.app = {
|
||||
isSearchMode: false,
|
||||
shareDialogItem: null,
|
||||
shareDialogItemType: null,
|
||||
notificationShareUrl: null
|
||||
notificationShareUrl: null,
|
||||
breadcrumbPath: [] // Array of {id, name} tracking folder navigation hierarchy
|
||||
};
|
||||
|
||||
window.appElements = {
|
||||
|
||||
+51
-51
@@ -472,75 +472,72 @@ const ui = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Update breadcrumb navigation
|
||||
* @param {string} folderName - Name of the current folder
|
||||
* Update breadcrumb navigation from the breadcrumbPath array.
|
||||
* Renders: Home > folder1 > folder2 > ...
|
||||
* Each segment is clickable to navigate back to that level.
|
||||
*/
|
||||
updateBreadcrumb(folderName) {
|
||||
updateBreadcrumb() {
|
||||
const breadcrumb = document.querySelector('.breadcrumb');
|
||||
breadcrumb.innerHTML = '';
|
||||
|
||||
// Get user info to help determine home folder
|
||||
const USER_DATA_KEY = 'oxicloud_user';
|
||||
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
||||
const username = userData.username || '';
|
||||
|
||||
// Create the home item - for users, this is their personal folder
|
||||
const homeItem = document.createElement('span');
|
||||
homeItem.className = 'breadcrumb-item';
|
||||
|
||||
|
||||
const self = this;
|
||||
const path = window.app.breadcrumbPath; // [{id, name}, ...]
|
||||
|
||||
// Helper function to safely get translation text
|
||||
const getTranslatedText = (key, defaultValue) => {
|
||||
if (!window.i18n || !window.i18n.t) return defaultValue;
|
||||
return window.i18n.t(key);
|
||||
};
|
||||
|
||||
// First determine if the current view is the user's home folder
|
||||
const isUserHomeFolder = username && window.app.userHomeFolderName &&
|
||||
window.app.userHomeFolderName.includes(username) &&
|
||||
folderName === window.app.userHomeFolderName;
|
||||
|
||||
// Set appropriate text for home item
|
||||
if (isUserHomeFolder) {
|
||||
// If the current folder is the user's home folder, label it as "Home"
|
||||
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
|
||||
} else if (folderName && folderName.startsWith('My Folder')) {
|
||||
// If viewing a root folder but not the user's home folder, use its full name
|
||||
homeItem.textContent = folderName;
|
||||
} else {
|
||||
// Default - use "Home" label
|
||||
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
|
||||
|
||||
// For searching, we might have a custom breadcrumb text
|
||||
if (folderName && folderName.startsWith('Search:')) {
|
||||
// We're in search mode - don't add click handler
|
||||
breadcrumb.appendChild(homeItem);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add click handler - but only if we have a user home folder to return to
|
||||
if (window.app.userHomeFolderId) {
|
||||
|
||||
// -- Home item (always present) --
|
||||
const homeItem = document.createElement('span');
|
||||
homeItem.className = 'breadcrumb-item';
|
||||
homeItem.textContent = getTranslatedText('breadcrumb.home', 'Home');
|
||||
|
||||
// If we have deeper segments, Home is clickable
|
||||
if (path.length > 0 && window.app.userHomeFolderId) {
|
||||
homeItem.classList.add('breadcrumb-link');
|
||||
homeItem.addEventListener('click', () => {
|
||||
window.app.breadcrumbPath = [];
|
||||
window.app.currentPath = window.app.userHomeFolderId;
|
||||
this.updateBreadcrumb(window.app.userHomeFolderName || 'Home');
|
||||
self.updateBreadcrumb();
|
||||
window.loadFiles();
|
||||
});
|
||||
} else {
|
||||
homeItem.classList.add('breadcrumb-current');
|
||||
}
|
||||
|
||||
breadcrumb.appendChild(homeItem);
|
||||
|
||||
// If we have a subfolder, add it to the breadcrumb
|
||||
if (folderName && !folderName.startsWith('Mi Carpeta') && !folderName.startsWith('Search:')) {
|
||||
// -- Intermediate + current segments --
|
||||
path.forEach((segment, index) => {
|
||||
const isLast = index === path.length - 1;
|
||||
|
||||
// Separator
|
||||
const separator = document.createElement('span');
|
||||
separator.className = 'breadcrumb-separator';
|
||||
separator.textContent = '>';
|
||||
breadcrumb.appendChild(separator);
|
||||
|
||||
const folderItem = document.createElement('span');
|
||||
folderItem.className = 'breadcrumb-item';
|
||||
folderItem.textContent = folderName;
|
||||
breadcrumb.appendChild(folderItem);
|
||||
}
|
||||
// Segment item
|
||||
const item = document.createElement('span');
|
||||
item.className = 'breadcrumb-item';
|
||||
item.textContent = segment.name;
|
||||
|
||||
if (!isLast) {
|
||||
// Intermediate segment: clickable – truncate path to this level
|
||||
item.classList.add('breadcrumb-link');
|
||||
item.addEventListener('click', () => {
|
||||
window.app.breadcrumbPath = path.slice(0, index + 1);
|
||||
window.app.currentPath = segment.id;
|
||||
self.updateBreadcrumb();
|
||||
window.loadFiles();
|
||||
});
|
||||
} else {
|
||||
// Last segment: current location, not clickable
|
||||
item.classList.add('breadcrumb-current');
|
||||
}
|
||||
breadcrumb.appendChild(item);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -733,8 +730,11 @@ const ui = {
|
||||
};
|
||||
|
||||
const navigateFolder = (card) => {
|
||||
window.app.currentPath = card.dataset.folderId;
|
||||
self.updateBreadcrumb(card.dataset.folderName);
|
||||
const folderId = card.dataset.folderId;
|
||||
const folderName = card.dataset.folderName;
|
||||
window.app.breadcrumbPath.push({ id: folderId, name: folderName });
|
||||
window.app.currentPath = folderId;
|
||||
self.updateBreadcrumb();
|
||||
window.loadFiles();
|
||||
};
|
||||
|
||||
|
||||
@@ -200,6 +200,9 @@ function replaceIconsInElement(container) {
|
||||
// Copy inline style if present
|
||||
if (el.style.cssText) svg.style.cssText = el.style.cssText;
|
||||
|
||||
// Preserve id attribute if present
|
||||
if (el.id) svg.id = el.id;
|
||||
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
path.setAttribute("fill", "currentColor");
|
||||
path.setAttribute("d", d);
|
||||
|
||||
+12
-2
@@ -87,8 +87,18 @@ const Modal = {
|
||||
cancelText = null
|
||||
} = options;
|
||||
|
||||
// Set modal content
|
||||
this.icon.className = `fas ${icon}`;
|
||||
// Set modal content - update the icon
|
||||
const iconContainer = document.querySelector('.modal-icon');
|
||||
if (iconContainer) {
|
||||
// Replace contents with a fresh <i> that icons.js will convert
|
||||
iconContainer.innerHTML = `<i id="modal-icon" class="fas ${icon}"></i>`;
|
||||
this.icon = document.getElementById('modal-icon');
|
||||
// Let icons.js convert it to SVG
|
||||
if (window.replaceIconsInElement) {
|
||||
window.replaceIconsInElement(iconContainer);
|
||||
this.icon = document.getElementById('modal-icon');
|
||||
}
|
||||
}
|
||||
this.title.textContent = title;
|
||||
this.label.textContent = label;
|
||||
this.input.placeholder = placeholder;
|
||||
|
||||
@@ -69,24 +69,35 @@ const sharedView = {
|
||||
container.innerHTML = `
|
||||
<div class="shared-header">
|
||||
<div class="shared-filters">
|
||||
<select id="filter-type" class="shared-filter-select">
|
||||
<option value="all" data-i18n="shared_filterAll">All</option>
|
||||
<option value="file" data-i18n="shared_filterFiles">Files</option>
|
||||
<option value="folder" data-i18n="shared_filterFolders">Folders</option>
|
||||
</select>
|
||||
<select id="sort-by" class="shared-filter-select">
|
||||
<option value="date" data-i18n="shared_sortByDate">Sort by date</option>
|
||||
<option value="name" data-i18n="shared_sortByName">Sort by name</option>
|
||||
<option value="expiration" data-i18n="shared_sortByExpiration">Sort by expiration</option>
|
||||
</select>
|
||||
<div class="shared-custom-select" id="filter-type-wrapper">
|
||||
<button class="shared-select-toggle" id="filter-type-toggle">
|
||||
<span class="shared-select-label" data-i18n="shared_filterAll">All</span>
|
||||
<i class="fas fa-chevron-down shared-select-arrow"></i>
|
||||
</button>
|
||||
<div class="shared-select-dropdown" id="filter-type-dropdown">
|
||||
<div class="shared-select-option active" data-value="all" data-i18n="shared_filterAll">All</div>
|
||||
<div class="shared-select-option" data-value="file" data-i18n="shared_filterFiles">Files</div>
|
||||
<div class="shared-select-option" data-value="folder" data-i18n="shared_filterFolders">Folders</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shared-custom-select" id="sort-by-wrapper">
|
||||
<button class="shared-select-toggle" id="sort-by-toggle">
|
||||
<span class="shared-select-label" data-i18n="shared_sortByDate">Sort by date</span>
|
||||
<i class="fas fa-chevron-down shared-select-arrow"></i>
|
||||
</button>
|
||||
<div class="shared-select-dropdown" id="sort-by-dropdown">
|
||||
<div class="shared-select-option active" data-value="date" data-i18n="shared_sortByDate">Sort by date</div>
|
||||
<div class="shared-select-option" data-value="name" data-i18n="shared_sortByName">Sort by name</div>
|
||||
<div class="shared-select-option" data-value="expiration" data-i18n="shared_sortByExpiration">Sort by expiration</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="empty-shared-state" class="empty-state" style="display:none;">
|
||||
<div class="empty-state-icon">📤</div>
|
||||
<h3 data-i18n="shared_emptyStateTitle">No shared items</h3>
|
||||
<i class="fas fa-share-alt" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
|
||||
<p data-i18n="shared_emptyStateTitle">No shared items</p>
|
||||
<p data-i18n="shared_emptyStateDesc">Items you share will appear here</p>
|
||||
<button id="empty-go-to-files" class="button primary" data-i18n="shared_goToFiles">Go to Files</button>
|
||||
</div>
|
||||
|
||||
<div class="shared-list-container" style="display:none;">
|
||||
@@ -183,13 +194,17 @@ const sharedView = {
|
||||
|
||||
// Attach event listeners
|
||||
attachEventListeners() {
|
||||
const filterType = document.getElementById('filter-type');
|
||||
const sortBy = document.getElementById('sort-by');
|
||||
const emptyGoToFiles = document.getElementById('empty-go-to-files');
|
||||
// Custom dropdown logic for filter-type
|
||||
this._initCustomSelect('filter-type-wrapper', 'filter-type-toggle', 'filter-type-dropdown');
|
||||
// Custom dropdown logic for sort-by
|
||||
this._initCustomSelect('sort-by-wrapper', 'sort-by-toggle', 'sort-by-dropdown');
|
||||
|
||||
if (filterType) filterType.addEventListener('change', () => this.filterAndSortItems());
|
||||
if (sortBy) sortBy.addEventListener('change', () => this.filterAndSortItems());
|
||||
if (emptyGoToFiles) emptyGoToFiles.addEventListener('click', () => window.switchToFilesView());
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
document.querySelectorAll('.shared-custom-select.open').forEach(sel => {
|
||||
if (!sel.contains(e.target)) sel.classList.remove('open');
|
||||
});
|
||||
});
|
||||
|
||||
// Share dialog (sharedView-specific IDs)
|
||||
const shareDialog = document.getElementById('shared-view-edit-dialog');
|
||||
@@ -226,13 +241,46 @@ const sharedView = {
|
||||
}
|
||||
},
|
||||
|
||||
// Initialize a custom select dropdown
|
||||
_initCustomSelect(wrapperId, toggleId, dropdownId) {
|
||||
const wrapper = document.getElementById(wrapperId);
|
||||
const toggle = document.getElementById(toggleId);
|
||||
const dropdown = document.getElementById(dropdownId);
|
||||
if (!wrapper || !toggle || !dropdown) return;
|
||||
|
||||
toggle.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// Close other open selects
|
||||
document.querySelectorAll('.shared-custom-select.open').forEach(sel => {
|
||||
if (sel !== wrapper) sel.classList.remove('open');
|
||||
});
|
||||
wrapper.classList.toggle('open');
|
||||
});
|
||||
|
||||
dropdown.querySelectorAll('.shared-select-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// Update active state
|
||||
dropdown.querySelectorAll('.shared-select-option').forEach(o => o.classList.remove('active'));
|
||||
option.classList.add('active');
|
||||
// Update label
|
||||
const label = toggle.querySelector('.shared-select-label');
|
||||
if (label) label.textContent = option.textContent;
|
||||
// Close dropdown
|
||||
wrapper.classList.remove('open');
|
||||
// Trigger filter
|
||||
this.filterAndSortItems();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// Filter and sort items
|
||||
filterAndSortItems() {
|
||||
const filterType = document.getElementById('filter-type');
|
||||
const sortBy = document.getElementById('sort-by');
|
||||
const filterTypeActive = document.querySelector('#filter-type-dropdown .shared-select-option.active');
|
||||
const sortByActive = document.querySelector('#sort-by-dropdown .shared-select-option.active');
|
||||
|
||||
const type = filterType ? filterType.value : 'all';
|
||||
const sort = sortBy ? sortBy.value : 'date';
|
||||
const type = filterTypeActive ? filterTypeActive.dataset.value : 'all';
|
||||
const sort = sortByActive ? sortByActive.dataset.value : 'date';
|
||||
|
||||
// Use the top-bar search if available, otherwise no filter
|
||||
const topSearch = document.getElementById('shared-search');
|
||||
|
||||
Reference in New Issue
Block a user