unclean SPARC
This commit is contained in:
Executable
+8
@@ -0,0 +1,8 @@
|
||||
<header eicapptoolbar>
|
||||
<button eicbutton icon="icon-menu" xxxlarge rounded class="app-menu" role="button" aria-label="Button" aria-enabled="true"></button>
|
||||
<a class="eu-logo" href="/" title="EIC Home"></a>
|
||||
<h1>European Innovation Council</h1>
|
||||
</header>
|
||||
<menu eicmenu lookup class="app-menu"></menu>
|
||||
<div class="app-workspace"></div>
|
||||
<div class="app-content-thesaurus"></div>
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Application main view. Acts as a container and manager for regular views
|
||||
*
|
||||
* @category MyEic
|
||||
* @subcategory Views
|
||||
*
|
||||
* @extends EICDialogContent
|
||||
*/
|
||||
class EICAppTemplate extends EICDomContent {
|
||||
|
||||
DOMContentLoaded() {
|
||||
ui.init({ariaEnabled: true});
|
||||
ui.eicfy(this.el);
|
||||
|
||||
window.addEventListener('resize', this.onGlobalResize.bind(this), true);
|
||||
|
||||
this.menu = new Menu(this.find('menu.app-menu'));
|
||||
|
||||
// todo : replace with something more dynamic
|
||||
this.loadAppMenu('global/app-menu-map.json');
|
||||
}
|
||||
|
||||
onGlobalResize() { EICController.resize(); }
|
||||
|
||||
loadAppMenu(source) {
|
||||
app.Assets.loadJson({ name: source})
|
||||
.then(this.setupAppMenu.bind(this));
|
||||
}
|
||||
|
||||
setupAppMenu(data) {
|
||||
let menu = [];
|
||||
|
||||
for(let section of data) {
|
||||
|
||||
let apps = section.items.filter(item => {
|
||||
if(item.access.includes("*")) return(true)
|
||||
let intersect = app.User.roles.filter(r => (item.access.includes(r)) || (item.access.includes("*")) )
|
||||
return(intersect.length > 0)
|
||||
});
|
||||
|
||||
if(apps.length > 0) {
|
||||
let entry = Object.assign({}, section);
|
||||
entry.items = apps;
|
||||
menu.push(entry)
|
||||
}
|
||||
}
|
||||
this.menu.clear();
|
||||
this.menu.parse(menu);
|
||||
|
||||
const button = new Button(this.find('button.app-menu'));
|
||||
button.click = this.toggleAppMenu.bind(this);
|
||||
|
||||
this.menu.collapsed = app.User.getPreference('global.menu.collapsed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables/disables collapsable app menu
|
||||
* @param {*} event
|
||||
*/
|
||||
toggleAppMenu() {
|
||||
this.menu.collapsed = ! this.menu.collapsed ;
|
||||
app.User.setPreference('global.menu.collapsed', this.menu.collapsed);
|
||||
}
|
||||
|
||||
initSession() {
|
||||
this.session = ui.create(`<div class="eic-session connected">
|
||||
<div eicdropdown>
|
||||
<button eicuser><span></span></button>
|
||||
<menu eicmenu></menu>
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
this.userMenu = new DropDown(this.session.querySelector('[eicdropdown]'));
|
||||
|
||||
this.userMenu.menu.parse([
|
||||
{
|
||||
label: "My profile",
|
||||
icon: "icon-user",
|
||||
severity: 'success',
|
||||
onclick: this.onProfile.bind(this)
|
||||
},
|
||||
{
|
||||
label: "Log out",
|
||||
icon: "icon-logoff",
|
||||
severity: 'danger',
|
||||
onclick: this.onLogout.bind(this)
|
||||
}
|
||||
]);
|
||||
|
||||
this.find('header').appendChild(this.session);
|
||||
|
||||
this.userIcon = new UserIcon(this.session.querySelector('[eicuser]'));
|
||||
this.userIcon.showStatus = true;
|
||||
this.userIcon.uuid = app.User.identity.uuid;
|
||||
this.userIcon.fullname = app.User.identity.firstname + ' ' + app.User.identity.lastname
|
||||
this.userIcon.online = app.MessageBus.connected;
|
||||
|
||||
app.events.channel.addEventListener('MessageBus.Connected', this.onBusConnected.bind(this))
|
||||
app.events.channel.addEventListener('MessageBus.Closed', this.onBusClosed.bind(this))
|
||||
|
||||
return this.session;
|
||||
}
|
||||
|
||||
async onLogout(event) {
|
||||
event.stopPropagation(); event.preventDefault();
|
||||
this.userMenu.collapse();
|
||||
app.User.logout();
|
||||
}
|
||||
|
||||
onProfile(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.userMenu.collapse();
|
||||
app.Router.route('/common/myprofile');
|
||||
}
|
||||
|
||||
onBusConnected() { this.userIcon.online = true; }
|
||||
|
||||
onBusClosed() { this.userIcon.online = false; }
|
||||
}
|
||||
|
||||
app.registerClass('EICAppTemplate', EICAppTemplate);
|
||||
@@ -0,0 +1,60 @@
|
||||
<style>
|
||||
.path-browser section{
|
||||
height: 35vh !important;
|
||||
width: 400px;
|
||||
}
|
||||
.path-browser .ffsDialog-folders-list .cell i.ffs.icon-folder{
|
||||
color: var(--eicui-base-color-warning);
|
||||
margin-right: .5em;
|
||||
}
|
||||
.path-browser .ffsDialog-folders-list footer{ display:none; }
|
||||
.path-browser div.current-path{ height: calc(var(--eicui-base-font-size-2xs)*2); }
|
||||
|
||||
.ffsDialog-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ffsDialog-folders-manager .ffsDialog-folder-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.ffsDialog-folder-controls input {
|
||||
flex: 1;
|
||||
}
|
||||
.ffsConfirmBox {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
font-size: 0.85em;
|
||||
color: var(--eicui-base-color-danger);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<article eiccard class="path-browser">
|
||||
<header>
|
||||
<label>Current path:</label>
|
||||
<div class="current-path"></div>
|
||||
</header>
|
||||
|
||||
<section class="ffsDialog-folders-browser">
|
||||
<div class="ffsDialog-folders-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="ffsDialog-folders-manager ffsDialog-hidden">
|
||||
<div class="ffsDialog-folder-controls">
|
||||
<input type="text" class="ffsDialog-folder-name eicui-input" placeholder="New folder name" />
|
||||
<button eicbutton xsmall rounded primary class="create-folder">Create</button>
|
||||
</div>
|
||||
<ul class="ffsDialog-folder-list" style="list-style:none;padding:0;"></ul>
|
||||
</section>
|
||||
|
||||
<footer style="margin-top:1em;">
|
||||
<button eicbutton xsmall rounded primary class="ffsDialog-btn-manage">Manage Folders</button>
|
||||
<button eicbutton xsmall rounded primary btn-back class="ffsDialog-btn-back ffsDialog-hidden" icon="icon-back"><i class="icon-back">Back</i></button>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
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 === '..' ? ' <span style="font-style: italic;">go back up</span>' : folder.name
|
||||
const icon = folder.name === '..' ? 'icon-back' : 'icon-folder'
|
||||
const row = this.foldersGrid.addRow(folder.name, [`<i class="ffs ${icon}"></i>${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 `<span eicchip xxsmall info data-path="${subPath}">${dir}</span>`
|
||||
}).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 = `<i class="icon-spinner spin"></i>`
|
||||
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 = `<i class="icon-spinner spin"></i>`
|
||||
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 = `
|
||||
<i class="ffs icon-folder" style="margin-right: 0.5em"></i>
|
||||
<span>${f.name}</span>
|
||||
${canDelete ? `
|
||||
<button class="ffsDelBtn" eicbutton xsmall rounded danger style="margin-left:1em">
|
||||
<i class="icon-bin"></i>
|
||||
</button>
|
||||
<div class="ffsConfirmBox ffsDialog-hidden" style="margin-top: 0.5em; display: flex; gap: 0.5em; align-items: center;">
|
||||
<span>Confirm deletion?</span>
|
||||
<button class="ffsConfirmYes" eicbutton xsmall rounded danger data-path="${f.path}" data-name="${f.name}">Yes</button>
|
||||
<button class="ffsConfirmNo" eicbutton xsmall rounded secondary>No</button>
|
||||
</div>` : ''
|
||||
}
|
||||
`
|
||||
this.folderList.appendChild(li)
|
||||
|
||||
if (canDelete) {
|
||||
const delBtn = li.querySelector('.ffsDelBtn')
|
||||
const confirmBox = li.querySelector('.ffsConfirmBox')
|
||||
const yesBtn = li.querySelector('.ffsConfirmYes')
|
||||
const noBtn = li.querySelector('.ffsConfirmNo')
|
||||
|
||||
delBtn.addEventListener('click', () => {
|
||||
delBtn.style.display = 'none'
|
||||
confirmBox.classList.remove('ffsDialog-hidden')
|
||||
})
|
||||
|
||||
noBtn.addEventListener('click', () => {
|
||||
confirmBox.classList.add('ffsDialog-hidden')
|
||||
delBtn.style.display = ''
|
||||
})
|
||||
|
||||
yesBtn.addEventListener('click', () => {
|
||||
const path = yesBtn.getAttribute('data-path')
|
||||
const name = yesBtn.getAttribute('data-name')
|
||||
this.deleteFolder(path, name, yesBtn, noBtn)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.registerClass('FileBrowserDialog', FileBrowserDialog)
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="confirm-dialog">
|
||||
<section>
|
||||
<alert eicalert ${severity} ${muted}>
|
||||
${message}
|
||||
</alert>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @category MyEic
|
||||
* @subcategory Views
|
||||
*
|
||||
* @extends EICDialogContent
|
||||
*/
|
||||
class ConfirmDialog extends EICDialogContent {
|
||||
|
||||
actions = [
|
||||
{
|
||||
label: 'Cancel',
|
||||
onclick: this.cancel.bind(this),
|
||||
severity: 'secondary',
|
||||
role: 'cancel'
|
||||
}
|
||||
]
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.options = options;
|
||||
if(this.options.cancelLabel) this.actions[0].label = this.options.cancelLabel
|
||||
if(this.options.cancelSeverity) this.actions[0].severity = this.options.cancelSeverity
|
||||
if(this.options.okLabel != '') {
|
||||
this.actions.push({
|
||||
label: this.options.okLabel || 'OK',
|
||||
onclick: this.comply.bind(this),
|
||||
severity: options.okSeverity||options.severity,
|
||||
role: 'comply'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DOMContentLoaded() {
|
||||
ui.eicfy(this.el);
|
||||
if(typeof(this.options.contentLoaded)=='function') this.options.contentLoaded(this.el)
|
||||
if(this.options.promptsClass) this.form = new Form(this.find('.'+this.options.promptsClass))
|
||||
}
|
||||
|
||||
comply() {
|
||||
let data = true
|
||||
if(this.options.promptsClass){
|
||||
if(!this.form.validate()) return
|
||||
data = this.form.value
|
||||
}
|
||||
|
||||
this.actions.find(o => o.role == 'comply').button.loading = true;
|
||||
this.actions.find(o => o.role == 'cancel').button.disabled = true;
|
||||
if(this.options.okPromise){
|
||||
this.options.okPromise(data).then( this.onSuccess.bind(this,data), this.onFailed.bind(this) )
|
||||
} else {
|
||||
this.onSuccess(data)
|
||||
}
|
||||
}
|
||||
|
||||
onSuccess(data) {
|
||||
this.actions.find(o => o.role == 'comply').button.loading = false;
|
||||
this.actions.find(o => o.role == 'cancel').button.disabled = false;
|
||||
this.commit(data);
|
||||
}
|
||||
|
||||
onFailed() {
|
||||
this.actions.find(o => o.role == 'comply').button.loading = false;
|
||||
this.actions.find(o => o.role == 'cancel').button.disabled = false;
|
||||
this.abort();
|
||||
}
|
||||
}
|
||||
|
||||
app.registerClass('ConfirmDialog',ConfirmDialog);
|
||||
Reference in New Issue
Block a user