First full release
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
/* global axios, removeAllChildNodes, bootstrap */
|
||||
/* global axios, removeAllChildNodes, bootstrap, i18next */
|
||||
|
||||
const fileUploadContainerEl = document.getElementById('upload-files-container')
|
||||
const fileUploadForm = document.forms['upload-files']
|
||||
const fileInputEl = fileUploadForm.querySelector('input[type="file"]')
|
||||
const filesListEl = document.getElementById('files-list')
|
||||
const dropArea = document.querySelector('.drag-area')
|
||||
const dragButton = dropArea.querySelector('button')
|
||||
const dragInput = dropArea.querySelector('input')
|
||||
const modalSpinner = new bootstrap.Modal(
|
||||
document.getElementById('modal-spinner')
|
||||
)
|
||||
const modalAlert = new bootstrap.Modal(document.getElementById('alert-modal'))
|
||||
let file
|
||||
let fileListElCount = filesListEl.childElementCount
|
||||
let isUploadInProgress = false
|
||||
let isDeletionInProgress = false
|
||||
const existingFileNames = [...filesListEl.children].map(
|
||||
fileListEl => fileListEl.querySelector('.file-name').textContent
|
||||
)
|
||||
|
||||
dragButton.onclick = () => {
|
||||
dragInput.click()
|
||||
@@ -68,96 +72,216 @@ fileInputEl.addEventListener('change', submitFiles)
|
||||
filesListEl.addEventListener('click', handleFileClick)
|
||||
|
||||
async function submitFiles() {
|
||||
// TODO Lock additional submits for the duration of this function execution?
|
||||
if (isUploadInProgress) return
|
||||
isUploadInProgress = true
|
||||
fileUploadContainerEl.hidden = true
|
||||
const MAX_FILE_SIZE = 10 ** 9 // 1 GB
|
||||
const filesToUpload = []
|
||||
const failedUploads = []
|
||||
|
||||
modalSpinner.toggle()
|
||||
const succeededFileListEls = []
|
||||
let didRemoveAnyFileListEls = false
|
||||
|
||||
for (const file of fileInputEl.files) {
|
||||
const filename = file.name
|
||||
|
||||
const indexOfSameNamed = existingFileNames.indexOf(filename)
|
||||
if (indexOfSameNamed !== -1) {
|
||||
filesListEl.children[indexOfSameNamed].remove()
|
||||
existingFileNames.splice(indexOfSameNamed, 1)
|
||||
didRemoveAnyFileListEls = true
|
||||
}
|
||||
|
||||
const fileListEl = createFileListEl(filename)
|
||||
filesListEl.appendChild(fileListEl)
|
||||
existingFileNames.push(filename)
|
||||
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
const failedUpload = {
|
||||
filename: file.name,
|
||||
filename,
|
||||
message: 'File too large. Must not be over 1 GB.'
|
||||
}
|
||||
failedUploads.push(failedUpload)
|
||||
updateFileListEl(fileListEl, { status: 'failed' })
|
||||
continue
|
||||
}
|
||||
|
||||
const payload = new FormData()
|
||||
payload.set(fileInputEl.name, file)
|
||||
filesToUpload.push([file, fileListEl])
|
||||
}
|
||||
|
||||
if (didRemoveAnyFileListEls) reindexFileListEls()
|
||||
|
||||
for (const [file, fileListEl] of filesToUpload) {
|
||||
try {
|
||||
await axios.put(apiEndpointBase, payload)
|
||||
const payload = new FormData()
|
||||
payload.set(fileInputEl.name, file)
|
||||
const { data: fileStats } = await axios.put(apiEndpointBase, payload, {
|
||||
onUploadProgress: displayUploadProgress(fileListEl)
|
||||
})
|
||||
succeededFileListEls.push(fileListEl)
|
||||
updateFileListEl(fileListEl, { status: 'success', fileStats })
|
||||
} catch (error) {
|
||||
const failedUpload = {
|
||||
filename: file.name,
|
||||
message: error.response.data
|
||||
}
|
||||
failedUploads.push(failedUpload)
|
||||
updateFileListEl(fileListEl, { status: 'failed' })
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: files } = await axios.get(apiEndpointBase)
|
||||
updateFilesList(files)
|
||||
} catch {
|
||||
alert('Pri posodobljanju seznama naloženih datotek je prišlo do napake.')
|
||||
}
|
||||
|
||||
fileInputEl.value = ''
|
||||
displayFailedUploads(failedUploads)
|
||||
modalSpinner.toggle()
|
||||
succeededFileListEls.forEach(el => updateFileListEl(el, { status: 'done' }))
|
||||
if (failedUploads.length) displayFailedUploads(failedUploads)
|
||||
isUploadInProgress = false
|
||||
fileUploadContainerEl.hidden = false
|
||||
}
|
||||
|
||||
function updateFilesList(files) {
|
||||
removeAllChildNodes(filesListEl)
|
||||
files.forEach(({ filename, size, timeModified }, index) => {
|
||||
const rowEl = document.createElement('tr')
|
||||
const tdId = document.createElement('td')
|
||||
const tdName = document.createElement('td')
|
||||
const tdSize = document.createElement('td')
|
||||
const tdDate = document.createElement('td')
|
||||
const tdDelete = document.createElement('td')
|
||||
tdId.textContent = index + 1
|
||||
tdName.textContent = filename
|
||||
tdName.className = 'filename'
|
||||
tdSize.textContent = size
|
||||
const formattedDate = new Date(timeModified).toLocaleDateString('sl-SL')
|
||||
tdDate.textContent = formattedDate
|
||||
const delBtn = document.createElement('button')
|
||||
delBtn.className = 'p-0 delete-file delete-btn-table'
|
||||
delBtn.type = 'button'
|
||||
const deleteImg = document.createElement('img')
|
||||
deleteImg.src = '/images/red-trash-icon.svg'
|
||||
deleteImg.alt = 'Izbriši'
|
||||
const delSpan = document.createElement('span')
|
||||
delSpan.className = 'ms-2'
|
||||
delSpan.textContent = 'Briši'
|
||||
delBtn.appendChild(deleteImg)
|
||||
delBtn.appendChild(delSpan)
|
||||
tdDelete.appendChild(delBtn)
|
||||
rowEl.append(tdId, tdName, tdSize, tdDate, tdDelete)
|
||||
filesListEl.appendChild(rowEl)
|
||||
})
|
||||
function createFileListEl(filename) {
|
||||
const indexEl = document.createElement('td')
|
||||
indexEl.className = 'file-index'
|
||||
indexEl.textContent = ++fileListElCount
|
||||
|
||||
const nameEl = document.createElement('td')
|
||||
nameEl.className = 'file-name'
|
||||
nameEl.textContent = filename
|
||||
|
||||
const sizeEl = document.createElement('td')
|
||||
sizeEl.className = 'file-size'
|
||||
|
||||
const dateEl = document.createElement('td')
|
||||
dateEl.className = 'file-date-modified'
|
||||
|
||||
const progressBarId = `progress-bar-${fileListElCount}`
|
||||
const progressLabelEl = document.createElement('label')
|
||||
progressLabelEl.className = 'me-1'
|
||||
progressLabelEl.for = progressBarId
|
||||
progressLabelEl.textContent = '0%'
|
||||
const progressBarEl = document.createElement('progress')
|
||||
progressBarEl.id = progressBarId
|
||||
progressBarEl.className = 'upload-progress'
|
||||
progressBarEl.value = 0
|
||||
progressBarEl.max = 100
|
||||
const lastEl = document.createElement('td')
|
||||
lastEl.className = 'file-last-cell'
|
||||
lastEl.append(progressLabelEl, progressBarEl)
|
||||
|
||||
const fileListEl = document.createElement('tr')
|
||||
fileListEl.append(indexEl, nameEl, sizeEl, dateEl, lastEl)
|
||||
|
||||
return fileListEl
|
||||
}
|
||||
|
||||
function updateFileListEl(fileListEl, { status, fileStats, index }) {
|
||||
if (status === 'success') {
|
||||
const nameEl = fileListEl.querySelector('.file-name')
|
||||
nameEl.textContent = fileStats.filename
|
||||
|
||||
const sizeEl = fileListEl.querySelector('.file-size')
|
||||
sizeEl.textContent = fileStats.size
|
||||
|
||||
const dateEl = fileListEl.querySelector('.file-date-modified')
|
||||
const formattedDate = new Date(fileStats.timeModified).toLocaleDateString(
|
||||
'sl-SL'
|
||||
)
|
||||
dateEl.textContent = formattedDate
|
||||
|
||||
const successImgEl = document.createElement('img')
|
||||
successImgEl.src = '/images/valid.svg'
|
||||
successImgEl.alt = i18next.t('Datoteka uspešno naložena')
|
||||
const lastEl = fileListEl.querySelector('.file-last-cell')
|
||||
removeAllChildNodes(lastEl)
|
||||
lastEl.append(successImgEl)
|
||||
} else if (status === 'done') {
|
||||
const deleteImgEl = document.createElement('img')
|
||||
deleteImgEl.src = '/images/red-trash-icon.svg'
|
||||
deleteImgEl.alt = ''
|
||||
const deleteSpanEl = document.createElement('span')
|
||||
deleteSpanEl.className = 'ms-2'
|
||||
deleteSpanEl.textContent = 'Briši'
|
||||
const deleteButtonEl = document.createElement('button')
|
||||
deleteButtonEl.className = 'p-0 delete-file delete-btn-table'
|
||||
deleteButtonEl.append(deleteImgEl, deleteSpanEl)
|
||||
const lastEl = fileListEl.querySelector('.file-last-cell')
|
||||
removeAllChildNodes(lastEl)
|
||||
lastEl.append(deleteButtonEl)
|
||||
} else if (status === 'reindex') {
|
||||
const indexEl = fileListEl.querySelector('.file-index')
|
||||
indexEl.textContent = index + 1
|
||||
} else if (status === 'failed') {
|
||||
const failImgEl = document.createElement('img')
|
||||
failImgEl.src = '/images/x_red.svg'
|
||||
failImgEl.alt = i18next.t('Napaka pri nalaganju datoteke')
|
||||
const lastEl = fileListEl.querySelector('.file-last-cell')
|
||||
removeAllChildNodes(lastEl)
|
||||
lastEl.append(failImgEl)
|
||||
}
|
||||
}
|
||||
|
||||
function displayFailedUploads(failedUploads) {
|
||||
const modalContentEl = modalAlert._element.querySelector(
|
||||
'.modal-alert-content'
|
||||
)
|
||||
modalContentEl.classList.remove('d-flex')
|
||||
|
||||
const descriptionEl = modalAlert._element.querySelector('#alert-text')
|
||||
descriptionEl.textContent = i18next.t(
|
||||
'Naslednje datoteke niso bile naložene zaradi navedenih razlogov:'
|
||||
)
|
||||
|
||||
modalContentEl.querySelector('ul')?.remove()
|
||||
|
||||
const listEl = document.createElement('ul')
|
||||
modalContentEl.append(listEl)
|
||||
|
||||
failedUploads.forEach(({ filename, message }) => {
|
||||
const alertText = modalAlert.querySelector('#alert-text')
|
||||
alertText.textContent = `NAPAKA - Ime datoteke: ${filename}, razlog: ${message}`
|
||||
modalAlert.toggle()
|
||||
const bulletEl = document.createElement('li')
|
||||
bulletEl.textContent = `${filename} - ${message}`
|
||||
listEl.appendChild(bulletEl)
|
||||
})
|
||||
|
||||
modalAlert.toggle()
|
||||
}
|
||||
|
||||
async function handleFileClick(e) {
|
||||
if (e.target.closest('.delete-file')) {
|
||||
if (isUploadInProgress) {
|
||||
alert(
|
||||
i18next.t('Brisanje je onemogočeno, doker se nalagajo nove datoteke.')
|
||||
)
|
||||
return
|
||||
}
|
||||
if (isDeletionInProgress) {
|
||||
alert(i18next.t('Brisanje je onemogočeno, saj je eno še v procesu.'))
|
||||
return
|
||||
}
|
||||
isDeletionInProgress = true
|
||||
const fileEl = e.target.closest('tr')
|
||||
const filename = fileEl.querySelector('.filename').textContent
|
||||
const filename = fileEl.querySelector('.file-name').textContent
|
||||
try {
|
||||
await axios.delete(`${apiEndpointBase}/${filename}`)
|
||||
fileEl.remove()
|
||||
existingFileNames.splice(existingFileNames.indexOf(filename), 1)
|
||||
reindexFileListEls()
|
||||
isDeletionInProgress = false
|
||||
} catch {
|
||||
alert('Pri brisanju datoteke je prišlo do napake.')
|
||||
alert(i18next.t('Pri brisanju datoteke je prišlo do napake.'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function displayUploadProgress(fileListEl) {
|
||||
const progressEl = fileListEl.querySelector('.upload-progress')
|
||||
return function ({ loaded, total }) {
|
||||
const percentCompleted = Math.round((loaded / total) * 100)
|
||||
progressEl.setAttribute('value', percentCompleted)
|
||||
progressEl.previousElementSibling.textContent = `${percentCompleted}%`
|
||||
}
|
||||
}
|
||||
|
||||
function reindexFileListEls() {
|
||||
fileListElCount = filesListEl.childElementCount
|
||||
for (let i = 0; i < fileListElCount; i++) {
|
||||
const fileListEl = filesListEl.children[i]
|
||||
updateFileListEl(fileListEl, { status: 'reindex', index: i })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user