class FileBrowserDialog extends EICDialogContent { actions = [ { label: 'Cancel', class: 'FBDCancelBtn', onclick: this.cancel.bind(this), severity: 'secondary', role: 'cancel' }, { label: 'Select path', onclick: this.add.bind(this), severity: 'primary', role: 'add', class: 'ffs-selectPath', disabled: false } ] constructor(...args) { super(...args) Object.assign(this, app.helpers.validators) this.dirSimpleClickerTO = null // Timeout for simple directory click } DOMContentLoaded(options) { this.ffs = options.ffs this.editable = options.editable this.canManage = options.canManage this.currentPath = options.startPath this.templates = options.templates this.mailings = options.mailings // Checks if a path has any subfolders or files this.ffs.hasChildren = (path) => { const hasFolder = this.ffs.getFolders().some(f => f.path.startsWith(path + '/') && f.path !== path) const hasFile = this.ffs.getFiles().some(file => file.fullPath.startsWith(path + '/')) return hasFolder || hasFile } // Cache the select path button after DOM has been updated setTimeout(() => { this.selectPathButton = this.find('.ffs-selectPath') }, 0) this._cacheDOMElements() this._attachEventListeners() // Initialize with the starting directory this.ffs.changeDir(this.currentPath) this.foldersGrid = new DataGrid(this.find('.ffsDialog-folders-list'), { headers: [{ label: 'Folders', type: 'markup', filter: 'text', sortable: true }], actions: [] }) this.fillFoldersGrid() this.validateInput() if (!this.canManage) { this.find('.ffsDialog-btn-manage').style.display = 'none' } } _cacheDOMElements() { this.browserSection = this.find('.ffsDialog-folders-browser') this.managerSection = this.find('.ffsDialog-folders-manager') this.folderInput = this.find('.ffsDialog-folder-name') this.folderList = this.find('.ffsDialog-folder-list') this.createBtn = this.find('.create-folder') } _attachEventListeners() { this.find('.ffsDialog-btn-manage').addEventListener('click', () => this.showManager()) this.find('.ffsDialog-btn-back').addEventListener('click', () => this.showBrowser()) this.createBtn.addEventListener('click', () => this.createFolder()) this.folderInput.addEventListener('input', () => this.validateInput()) } // Fill the folder grid with accessible folders fillFoldersGrid() { this.foldersGrid.clear() this.showPath(this.ffs.currentPath) const authorized = this.ffs.authorizedPaths || [] const rights = this.ffs.rights || [] // Search for rights related to the current path const currentRights = rights.find(r => r.path === this.ffs.currentPath) const canManageCurrent = currentRights && (currentRights.create || currentRights.delete) const manageBtn = this.find('.ffsDialog-btn-manage') if (manageBtn) { manageBtn.style.display = canManageCurrent ? '' : 'none' } for (let folder of this.ffs.getFolders()) { let fullPath = folder.path if (!fullPath.startsWith('/')) { fullPath = '/' + fullPath } const isAuthorized = authorized.includes(fullPath) const isParentOfAuthorized = authorized.some(authPath => authPath.startsWith(fullPath + '/')) if (folder.name === '..') { const parentPath = fullPath const parentRights = rights.find(r => r.path === parentPath) if (parentPath === '/') { if (!parentRights || !parentRights.enter) continue } } if ( folder.name === '..' || isAuthorized || isParentOfAuthorized ) { const displayName = folder.name === '..' ? ' go back up' : folder.name const icon = folder.name === '..' ? 'icon-back' : 'icon-folder' const row = this.foldersGrid.addRow(folder.name, [`${displayName}`]) row.dataset.type = 'folder' row.addEventListener('click', () => this.onEnterDir(folder)) } } } // Display the current path as a breadcrumb showPath(path) { this.currentPath = path const dirs = path.split('/').filter(Boolean) const chips = dirs.map((dir, idx) => { const subPath = '/' + dirs.slice(0, idx + 1).join('/') return `${dir}` }).join('') this.find('.current-path').innerHTML = chips } onEnterDir(folder) { clearTimeout(this.dirSimpleClickerTO) this.ffs.changeDir(folder.path) this.fillFoldersGrid() } add() { this.commit(this.currentPath) } // Show the folder management section (create/delete) showManager() { this.browserSection.classList.add('ffsDialog-hidden') this.managerSection.classList.remove('ffsDialog-hidden') this.find('.ffsDialog-btn-manage').classList.add('ffsDialog-hidden') this.find('.ffsDialog-btn-back').classList.remove('ffsDialog-hidden') if (this.selectPathButton) this.selectPathButton.style.display = 'none' this.manageFolders() } showBrowser() { this.browserSection.classList.remove('ffsDialog-hidden') this.managerSection.classList.add('ffsDialog-hidden') this.find('.ffsDialog-btn-manage').classList.remove('ffsDialog-hidden') this.find('.ffsDialog-btn-back').classList.add('ffsDialog-hidden') if (this.selectPathButton) this.selectPathButton.style.display = '' this.fillFoldersGrid() } // Validate the folder name input validateInput() { const value = this.folderInput.value.trim() const invalidChars = /[\/\\:\*\?"<>|]/ const errorText = invalidChars.test(value) ? 'Folder name contains invalid characters.' : '' this.createBtn.disabled = !!errorText let errorElem = this.find('.folder-error') if (!errorElem) { errorElem = document.createElement('div') errorElem.className = 'folder-error' errorElem.style.color = 'red' errorElem.style.fontSize = '0.8em' errorElem.style.marginTop = '0.3em' this.folderInput.insertAdjacentElement('afterend', errorElem) } errorElem.textContent = errorText } // Create a folder in the current path async createFolder() { const dirName = this.folderInput.value.trim() if (!dirName) return const originalHTML = this.createBtn.innerHTML this.createBtn.innerHTML = `` this.createBtn.disabled = true try { if (this.templates) { await this.templates.makeDir(this.currentPath, dirName) } if (this.mailings) { await this.mailings.makeDir(this.currentPath, dirName) } ui.growl.append(`Folder "${dirName}" created successfully!`, 'success') this.folderInput.value = '' this.validateInput() this.createBtn.innerHTML = originalHTML this.createBtn.disabled = false const cancelBtn = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent.trim() === 'Cancel') cancelBtn?.click() } catch (e) { console.error(e) ui.growl.append(`Error creating folder "${dirName}"`, 'error') } } async deleteFolder(path, name, yesBtn, noBtn) { const originalText = yesBtn.innerHTML yesBtn.innerHTML = `` yesBtn.disabled = true noBtn.disabled = true try { if (this.templates) { await this.templates.removeDir(1, path) } if (this.mailings) { await this.mailings.removeDir(2, path) } ui.growl.append(`Folder "${name}" deleted!`, 'success') const cancelBtn = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent.trim() === 'Cancel') cancelBtn?.click() } catch (err) { console.error(err) yesBtn.innerHTML = originalText yesBtn.disabled = false noBtn.disabled = false ui.growl.append(`Error deleting folder "${name}"`, 'error') } } // Display the list of folders in the management section, with delete buttons if allowed manageFolders() { this.folderList.innerHTML = '' const folders = this.ffs.getFolders().filter(f => f.name !== '..') const authorizedPaths = this.ffs.authorizedPaths || [] const pathKeys = Object.keys(this.ffs.path2dir) const foldersWithFiles = this.ffs.foldersWithFiles || [] for (const f of folders) { const hasFolderChild = pathKeys.some(path => path !== f.path && path.startsWith(f.path + '/')) const hasFileChild = foldersWithFiles.some(path => path === f.path || path.startsWith(f.path + '/')) const isAuthorized = authorizedPaths.includes(f.path) const canDelete = !hasFolderChild && !hasFileChild && isAuthorized const li = document.createElement('li') li.innerHTML = ` ${f.name} ${canDelete ? `