feat(ui): 1 modal to manage shares (users & public share)
fix(share): ensure Authz parent is created/updated on publicShare create/update fix(ShareModal): do not show Token (public) grants in People section
This commit is contained in:
@@ -7,11 +7,12 @@ import { resolveHomeFolder } from '../../app/authSession.js';
|
||||
import { loadFiles } from '../../app/filesView.js';
|
||||
import { switchToFilesSection } from '../../app/navigation.js';
|
||||
import { app } from '../../app/state.js';
|
||||
import { showConfirmDialog, ui } from '../../app/ui.js';
|
||||
import { ui } from '../../app/ui.js';
|
||||
import { Modal } from '../../components/modal.js';
|
||||
import { shareModal } from '../../components/shareModal.js';
|
||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||
import { escapeHtml } from '../../core/formatters.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { Modal } from '../../components/modal.js';
|
||||
import { favorites } from '../library/favorites.js';
|
||||
import { musicView } from '../library/music.js';
|
||||
import { fileSharing } from '../sharing/fileSharing.js';
|
||||
@@ -157,7 +158,7 @@ const contextMenus = {
|
||||
document.getElementById('share-folder-option').addEventListener('click', () => {
|
||||
const folder = app.contextMenuTargetFolder;
|
||||
if (folder) {
|
||||
this.showShareDialog(folder, 'folder');
|
||||
shareModal.open(folder, 'folder');
|
||||
}
|
||||
ui.closeContextMenu();
|
||||
});
|
||||
@@ -279,7 +280,7 @@ const contextMenus = {
|
||||
document.getElementById('share-file-option').addEventListener('click', () => {
|
||||
const file = app.contextMenuTargetFile;
|
||||
if (file) {
|
||||
this.showShareDialog(file, 'file');
|
||||
shareModal.open(file, 'file');
|
||||
}
|
||||
ui.closeFileContextMenu();
|
||||
});
|
||||
@@ -722,229 +723,6 @@ const contextMenus = {
|
||||
await this.loadMoveDialogFolders(app.userHomeFolderId || null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show share dialog for files or folders
|
||||
* @param {FileItem | FolderItem} item - File or folder object
|
||||
* @param {ItemTypeEnum} itemType
|
||||
*/
|
||||
async showShareDialog(item, itemType) {
|
||||
try {
|
||||
const shareDialog = document.getElementById('share-dialog');
|
||||
if (!shareDialog) {
|
||||
console.error('Share dialog element not found in DOM');
|
||||
ui.showNotification('Error', 'Share dialog not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update dialog title — use the <span> inside header to preserve <i> icon
|
||||
const dialogHeader = shareDialog.querySelector('.share-dialog-header');
|
||||
if (dialogHeader) {
|
||||
const headerSpan = dialogHeader.querySelector('span');
|
||||
const titleText = itemType === 'file' ? i18n.t('dialogs.share_file') : i18n.t('dialogs.share_folder');
|
||||
if (headerSpan) {
|
||||
headerSpan.textContent = titleText;
|
||||
} else {
|
||||
dialogHeader.textContent = titleText;
|
||||
}
|
||||
}
|
||||
|
||||
const itemName = document.getElementById('shared-item-name');
|
||||
if (itemName) itemName.textContent = item.name;
|
||||
|
||||
// Reset form
|
||||
const pwField = /** @type HTMLInputElement */ (document.getElementById('share-password'));
|
||||
const expField = /** @type HTMLInputElement */ (document.getElementById('share-expiration'));
|
||||
if (pwField) pwField.value = '';
|
||||
if (expField) expField.value = '';
|
||||
const permRead = /** @type HTMLInputElement */ (document.getElementById('share-permission-read'));
|
||||
const permWrite = /** @type HTMLInputElement */ (document.getElementById('share-permission-write'));
|
||||
const permReshare = /** @type HTMLInputElement */ (document.getElementById('share-permission-reshare'));
|
||||
if (permRead) permRead.checked = true;
|
||||
if (permWrite) permWrite.checked = false;
|
||||
if (permReshare) permReshare.checked = false;
|
||||
|
||||
// Store the current item and type for use when creating the share
|
||||
app.shareDialogItem = item;
|
||||
app.shareDialogItemType = itemType;
|
||||
|
||||
// Check if item already has shares (async API call)
|
||||
const existingShares = await fileSharing.getSharedLinksForItem(item.id, itemType);
|
||||
const existingSharesContainer = document.getElementById('existing-shares-container');
|
||||
|
||||
// Clear existing shares container
|
||||
existingSharesContainer.innerHTML = '';
|
||||
|
||||
if (existingShares.length > 0) {
|
||||
document.getElementById('existing-shares-section').classList.remove('hidden');
|
||||
|
||||
// Create elements for each existing share
|
||||
existingShares.forEach((share) => {
|
||||
const shareEl = document.createElement('div');
|
||||
shareEl.className = 'existing-share-item';
|
||||
|
||||
const expiresText = share.expires_at ? `Expires: ${fileSharing.formatExpirationDate(share.expires_at)}` : 'No expiration';
|
||||
|
||||
// Share URL
|
||||
const urlDiv = document.createElement('div');
|
||||
urlDiv.className = 'share-url';
|
||||
urlDiv.textContent = share.url;
|
||||
shareEl.appendChild(urlDiv);
|
||||
|
||||
// Share info
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'share-info';
|
||||
if (share.has_password) {
|
||||
const protectedSpan = document.createElement('span');
|
||||
protectedSpan.className = 'share-protected';
|
||||
protectedSpan.innerHTML = '<i class="fas fa-lock"></i> Password protected';
|
||||
infoDiv.appendChild(protectedSpan);
|
||||
}
|
||||
const expirationSpan = document.createElement('span');
|
||||
expirationSpan.className = 'share-expiration';
|
||||
expirationSpan.textContent = expiresText;
|
||||
infoDiv.appendChild(expirationSpan);
|
||||
shareEl.appendChild(infoDiv);
|
||||
|
||||
// Share actions
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'share-actions';
|
||||
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'btn btn-small copy-link-btn';
|
||||
copyBtn.dataset.shareUrl = share.url;
|
||||
copyBtn.innerHTML = '<i class="fas fa-copy"></i> Copy';
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'btn btn-small btn-danger delete-link-btn';
|
||||
deleteBtn.dataset.shareId = share.id;
|
||||
deleteBtn.innerHTML = '<i class="fas fa-trash"></i> Delete';
|
||||
actionsDiv.appendChild(deleteBtn);
|
||||
|
||||
shareEl.appendChild(actionsDiv);
|
||||
|
||||
existingSharesContainer.appendChild(shareEl);
|
||||
});
|
||||
|
||||
// Add event listeners for copy and delete buttons
|
||||
document.querySelectorAll('.copy-link-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const url = btn.getAttribute('data-share-url');
|
||||
fileSharing.copyLinkToClipboard(url);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.delete-link-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const shareId = btn.getAttribute('data-share-id');
|
||||
|
||||
showConfirmDialog({
|
||||
title: i18n.t('dialogs.confirm_delete_share'),
|
||||
message: i18n.t('dialogs.confirm_delete_share_msg'),
|
||||
confirmText: i18n.t('actions.delete')
|
||||
}).then(async (confirmed) => {
|
||||
if (confirmed) {
|
||||
await fileSharing.removeSharedLink(shareId);
|
||||
btn.closest('.existing-share-item').remove();
|
||||
if (existingSharesContainer.children.length === 0) {
|
||||
document.getElementById('existing-shares-section').classList.add('hidden');
|
||||
ui.setSharedVisualState(item.id, itemType, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
document.getElementById('existing-shares-section').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Hide new-share section from previous use
|
||||
const newShareSection = document.getElementById('new-share-section');
|
||||
if (newShareSection) newShareSection.classList.add('hidden');
|
||||
|
||||
// Show dialog
|
||||
shareDialog.classList.remove('hidden');
|
||||
console.log('Share dialog opened for', itemType, item.name);
|
||||
} catch (error) {
|
||||
console.error('Error opening share dialog:', error);
|
||||
ui.showNotification('Error', 'Could not open share dialog');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a shared link with the configured options
|
||||
*/
|
||||
async createSharedLink() {
|
||||
if (!app.shareDialogItem || !app.shareDialogItemType) {
|
||||
ui.showNotification('Error', 'Could not share the item');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get values from form
|
||||
const password = /** @type HTMLInputElement */ (document.getElementById('share-password')).value;
|
||||
const expirationDate = /** @type HTMLInputElement */ (document.getElementById('share-expiration')).value;
|
||||
const permissionRead = /** @type HTMLInputElement */ (document.getElementById('share-permission-read')).checked;
|
||||
const permissionWrite = /** @type HTMLInputElement */ (document.getElementById('share-permission-write')).checked;
|
||||
const permissionReshare = /** @type HTMLInputElement */ (document.getElementById('share-permission-reshare')).checked;
|
||||
|
||||
const item = app.shareDialogItem;
|
||||
const itemType = app.shareDialogItemType;
|
||||
|
||||
// Build DTO for backend API
|
||||
const createDto = {
|
||||
item_id: item.id,
|
||||
item_name: item.name || null,
|
||||
item_type: itemType,
|
||||
password: password || null,
|
||||
expires_at: expirationDate ? Math.floor(new Date(expirationDate).getTime() / 1000) : null,
|
||||
permissions: {
|
||||
read: permissionRead,
|
||||
write: permissionWrite,
|
||||
reshare: permissionReshare
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...getCsrfHeaders()
|
||||
};
|
||||
|
||||
const response = await fetch('/api/shares', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(createDto)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errBody = await response.json().catch(() => ({}));
|
||||
throw new Error(errBody.error || `Server error ${response.status}`);
|
||||
}
|
||||
|
||||
const shareInfo = await response.json();
|
||||
|
||||
// Update UI with new share
|
||||
const shareUrl = /** @type HTMLInputElement */ (document.getElementById('generated-share-url'));
|
||||
if (shareUrl) {
|
||||
shareUrl.value = shareInfo.url;
|
||||
document.getElementById('new-share-section').classList.remove('hidden');
|
||||
shareUrl.focus();
|
||||
shareUrl.select();
|
||||
}
|
||||
|
||||
// Update Item's shared badge
|
||||
ui.setSharedVisualState(item.id, itemType, true);
|
||||
|
||||
// Show success message
|
||||
ui.showNotification(i18n.t('notifications.link_created'), i18n.t('notifications.share_success'));
|
||||
} catch (error) {
|
||||
console.error('Error creating shared link:', error);
|
||||
ui.showNotification('Error', /** @type {Error} */ (error).message || 'Could not create shared link');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show email notification dialog
|
||||
* @param {string} shareUrl - URL to share
|
||||
@@ -991,16 +769,6 @@ const contextMenus = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Close share dialog
|
||||
*/
|
||||
closeShareDialog() {
|
||||
const dialog = document.getElementById('share-dialog');
|
||||
if (dialog) dialog.classList.add('hidden');
|
||||
app.shareDialogItem = null;
|
||||
app.shareDialogItemType = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Close notification dialog
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { app } from '../../app/state.js';
|
||||
import { Modal } from '../../components/modal.js';
|
||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||
import { formatFileSize } from '../../core/formatters.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { oxiIcon } from '../../core/icons.js';
|
||||
import { Modal } from '../../components/modal.js';
|
||||
import { notifications } from '../../core/notifications.js';
|
||||
|
||||
/** @import {FileItem, Musicshare, Playlist, PlaylistItem} from '../../core/types.js' */
|
||||
|
||||
Reference in New Issue
Block a user