feat(front): layout upload modal; use new components
This commit is contained in:
parent
e05a5b9d7a
commit
6eb5c14176
|
@ -18,7 +18,7 @@ interface Action {
|
|||
label: string
|
||||
isDangerous?: boolean
|
||||
allowAll?: boolean
|
||||
confirmColor?: string
|
||||
confirmColor?: 'success' | 'danger'
|
||||
confirmationMessage?: string
|
||||
filterChackable?: (item: any) => boolean
|
||||
}
|
||||
|
@ -61,6 +61,13 @@ const checkable = computed(() => {
|
|||
.map(item => item[props.idField] as string)
|
||||
})
|
||||
|
||||
// objects is `any`.
|
||||
// Can we narrow down this type?
|
||||
// TODO: Search `action-table` globally and narrow all
|
||||
// `objectsData` props
|
||||
|
||||
// Type Custom = A | B | C
|
||||
|
||||
const objects = computed(() => props.objectsData.results.map(object => {
|
||||
return props.customObjects.find(custom => custom[props.idField] === object[props.idField])
|
||||
?? object
|
||||
|
@ -178,27 +185,27 @@ const gridColumns = computed(() => {
|
|||
columns += 1
|
||||
}
|
||||
|
||||
return Array.from({ length: columns }, () => 'auto' as 'auto')
|
||||
return Array.from({ length: columns }, () => 'auto' as const)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="table-wrapper component-action-table">
|
||||
<Table
|
||||
<Table
|
||||
v-if="objectsData.count > 0"
|
||||
:grid-template-columns="gridColumns"
|
||||
class="ui compact very basic unstackable table"
|
||||
>
|
||||
>
|
||||
<template #header>
|
||||
<label v-if="actions.length > 0">
|
||||
<div class="ui checkbox">
|
||||
<!-- TODO (wvffle): Check if we don't have to migrate to v-model -->
|
||||
<input
|
||||
type="checkbox"
|
||||
:aria-label="labels.selectAllItems"
|
||||
:disabled="checkable.length === 0"
|
||||
:checked="checkable.length > 0 && checked.length === checkable.length"
|
||||
@change="toggleCheckAll"
|
||||
type="checkbox"
|
||||
:aria-label="labels.selectAllItems"
|
||||
:disabled="checkable.length === 0"
|
||||
:checked="checkable.length > 0 && checked.length === checkable.length"
|
||||
@change="toggleCheckAll"
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
|
@ -209,14 +216,16 @@ const gridColumns = computed(() => {
|
|||
v-if="actionUrl && actions.length > 0 || refreshable"
|
||||
:style="{ gridColumn: `span ${gridColumns.length}`, height: '128px' }"
|
||||
>
|
||||
|
||||
<Layout
|
||||
v-if="actionUrl && actions.length > 0"
|
||||
stack
|
||||
no-gap
|
||||
v-if="actionUrl && actions.length > 0"
|
||||
>
|
||||
<label for="actions-select">{{ t('components.common.ActionTable.label.actions') }}</label>
|
||||
<Layout flex class="ui form">
|
||||
<Layout
|
||||
flex
|
||||
class="ui form"
|
||||
>
|
||||
<select
|
||||
id="actions-select"
|
||||
v-model="currentActionName"
|
||||
|
@ -331,8 +340,8 @@ const gridColumns = computed(() => {
|
|||
<Spacer grow />
|
||||
|
||||
<Layout
|
||||
label
|
||||
v-if="refreshable"
|
||||
label
|
||||
class="right floated"
|
||||
>
|
||||
<span v-if="needsRefresh">
|
||||
|
@ -343,10 +352,10 @@ const gridColumns = computed(() => {
|
|||
icon="bi-arrow-clockwise"
|
||||
:title="labels.refresh"
|
||||
:aria-label="labels.refresh"
|
||||
@click="$emit('refresh')"
|
||||
style="align-self: end;"
|
||||
@click="$emit('refresh')"
|
||||
>
|
||||
{{ labels.refresh }}
|
||||
{{ labels.refresh }}
|
||||
</Button>
|
||||
</Layout>
|
||||
</div>
|
||||
|
|
|
@ -25,6 +25,10 @@ import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|||
import Alert from '~/components/ui/Alert.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Slider from '~/components/ui/Slider.vue'
|
||||
import Section from '~/components/ui/Section.vue'
|
||||
import Pill from '~/components/ui/Pill.vue'
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Table from '~/components/ui/Table.vue'
|
||||
|
||||
interface Events {
|
||||
(e: 'uploads-finished', delta: number):void
|
||||
|
@ -343,6 +347,9 @@ useEventListener(window, 'beforeunload', (event) => {
|
|||
event.preventDefault()
|
||||
return (event.returnValue = t('components.library.FileUpload.message.listener'))
|
||||
})
|
||||
|
||||
// collapse section
|
||||
const section = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -361,27 +368,8 @@ useEventListener(window, 'beforeunload', (event) => {
|
|||
:options="options"
|
||||
:label="t('components.manage.library.UploadsTable.label.visibility')"
|
||||
/>
|
||||
<file-upload-widget
|
||||
ref="upload"
|
||||
v-model="files"
|
||||
:data="uploadData"
|
||||
@input-file="inputFile"
|
||||
>
|
||||
<Button
|
||||
primary
|
||||
icon="bi bi-upload"
|
||||
>
|
||||
{{ t('components.library.FileUpload.label.uploadWidget') }}
|
||||
</Button>
|
||||
<p>
|
||||
{{ t('components.library.FileUpload.label.extensions', {extensions: supportedExtensions.join(', ')}) }}
|
||||
</p>
|
||||
</file-upload-widget>
|
||||
|
||||
<Alert blue>
|
||||
<h2 class="ui header">
|
||||
{{ t('components.library.FileUpload.header.local') }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ t('components.library.FileUpload.message.local.message') }}
|
||||
</p>
|
||||
|
@ -402,9 +390,55 @@ useEventListener(window, 'beforeunload', (event) => {
|
|||
</ul>
|
||||
</Alert>
|
||||
|
||||
<h2 class="ui header">
|
||||
{{ t('components.library.FileUpload.header.server') }}
|
||||
</h2>
|
||||
<file-upload-widget
|
||||
ref="upload"
|
||||
v-model="files"
|
||||
:data="uploadData"
|
||||
@input-file="inputFile"
|
||||
>
|
||||
<Button
|
||||
primary
|
||||
icon="bi bi-upload"
|
||||
>
|
||||
{{ t('components.library.FileUpload.label.uploadWidget') }}
|
||||
</Button>
|
||||
<p>
|
||||
{{ t('components.library.FileUpload.label.extensions', {extensions: supportedExtensions.join(', ')}) }}
|
||||
</p>
|
||||
</file-upload-widget>
|
||||
|
||||
<!-- Show how many files are uploading and processing -->
|
||||
|
||||
<Layout
|
||||
v-if="files.length > 0"
|
||||
flex
|
||||
>
|
||||
<Layout
|
||||
flex
|
||||
gap-8
|
||||
>
|
||||
<label>{{ t('components.library.FileUpload.link.uploading') }}</label>
|
||||
<Pill
|
||||
v-bind="{
|
||||
'green': erroredFilesCount === 0,
|
||||
'red': erroredFilesCount > 0,
|
||||
'yellow': files.length > uploadedFilesCount + erroredFilesCount
|
||||
}"
|
||||
>
|
||||
{{ uploadedFilesCount + erroredFilesCount }} / {{ files.length }}
|
||||
</Pill>
|
||||
</Layout>
|
||||
<Layout
|
||||
flex
|
||||
gap-8
|
||||
>
|
||||
<label>{{ t('components.library.FileUpload.link.processing') }}</label>
|
||||
<Pill>
|
||||
{{ processedFilesCount }} / {{ processableFiles }}
|
||||
</Pill>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
<Alert
|
||||
v-if="fsErrors.length > 0"
|
||||
red
|
||||
|
@ -421,32 +455,8 @@ useEventListener(window, 'beforeunload', (event) => {
|
|||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
<fs-browser
|
||||
v-model="fsPath"
|
||||
:loading="isLoadingFs"
|
||||
:data="fsStatus"
|
||||
@import="importFs"
|
||||
/>
|
||||
<template v-if="fsStatus && fsStatus.import">
|
||||
<h3 class="ui header">
|
||||
{{ t('components.library.FileUpload.header.status') }}
|
||||
</h3>
|
||||
<p v-if="fsStatus.import.reference !== importReference">
|
||||
{{ t('components.library.FileUpload.description.previousImport') }}
|
||||
</p>
|
||||
<p v-else>
|
||||
{{ t('components.library.FileUpload.description.import') }}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
v-if="fsStatus.import.status === 'started' || fsStatus.import.status === 'pending'"
|
||||
secondary
|
||||
@click="cancelFsScan"
|
||||
>
|
||||
{{ t('components.library.FileUpload.button.cancel') }}
|
||||
</Button>
|
||||
<fs-logs :data="fsStatus.import" />
|
||||
</template>
|
||||
<!-- Show list of processed files -->
|
||||
|
||||
<library-files-table
|
||||
:needs-refresh="needsRefresh"
|
||||
|
@ -455,177 +465,164 @@ useEventListener(window, 'beforeunload', (event) => {
|
|||
:custom-objects="Object.values(uploads.objects)"
|
||||
@fetch-start="needsRefresh = false"
|
||||
/>
|
||||
<div class="ui top attached tabular menu">
|
||||
<a
|
||||
href=""
|
||||
:class="['item', {active: currentTab === 'uploads'}]"
|
||||
@click.prevent="currentTab = 'uploads'"
|
||||
>
|
||||
{{ t('components.library.FileUpload.link.uploading') }}
|
||||
<div
|
||||
v-if="files.length === 0"
|
||||
class="ui label"
|
||||
>
|
||||
{{ t('components.library.FileUpload.empty.noFiles') }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="files.length > uploadedFilesCount + erroredFilesCount"
|
||||
class="ui warning label"
|
||||
>
|
||||
{{ uploadedFilesCount + erroredFilesCount }}
|
||||
<span class="bi bi-slash-circle" />
|
||||
{{ files.length }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="['ui', {'success': erroredFilesCount === 0}, {'danger': erroredFilesCount > 0}, 'label']"
|
||||
>
|
||||
{{ uploadedFilesCount + erroredFilesCount }}
|
||||
<span class="bi bi-slash-circle" />
|
||||
{{ files.length }}
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href=""
|
||||
:class="['item', {active: currentTab === 'processing'}]"
|
||||
@click.prevent="currentTab = 'processing'"
|
||||
>
|
||||
{{ t('components.library.FileUpload.link.processing') }}
|
||||
<div
|
||||
v-if="processableFiles === 0"
|
||||
class="ui label"
|
||||
>
|
||||
{{ t('components.library.FileUpload.empty.noFiles') }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="processableFiles > processedFilesCount"
|
||||
class="ui warning label"
|
||||
>
|
||||
{{ processedFilesCount }}
|
||||
<span class="bi bi-slash-circle" />
|
||||
{{ processableFiles }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="['ui', {'success': uploads.errored === 0}, {'danger': uploads.errored > 0}, 'label']"
|
||||
>
|
||||
{{ processedFilesCount }}
|
||||
<span class="bi bi-slash-circle" />
|
||||
{{ processableFiles }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="files.length > 0"
|
||||
class="table-wrapper"
|
||||
>
|
||||
<table class="ui unstackable table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ten wide">
|
||||
{{ t('components.library.FileUpload.table.upload.header.filename') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.library.FileUpload.table.upload.header.size') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.library.FileUpload.table.upload.header.status') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.library.FileUpload.table.upload.header.actions') }}
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-if="retryableFiles.length > 1">
|
||||
<th class="ten wide" />
|
||||
<th />
|
||||
<th />
|
||||
<th>
|
||||
<Button
|
||||
tiny
|
||||
style="float: right;"
|
||||
@click.prevent="retry(retryableFiles)"
|
||||
>
|
||||
{{ t('components.library.FileUpload.button.retry') }}
|
||||
</Button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="file in sortedFiles"
|
||||
:key="file.id"
|
||||
>
|
||||
<td :title="file.name">
|
||||
{{ truncate(file.name ?? '', 60) }}
|
||||
</td>
|
||||
<td>{{ humanSize(file.size ?? 0) }}</td>
|
||||
<td>
|
||||
<span
|
||||
v-if="typeof file.error === 'string' && file.error"
|
||||
class="ui tooltip"
|
||||
:data-tooltip="labels.tooltips[file.error]"
|
||||
>
|
||||
<span class="ui danger icon label">
|
||||
<i class="bi bi-question-circle-fill" /> {{ file.error }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-else-if="file.success"
|
||||
class="ui success label"
|
||||
>
|
||||
<span key="1">
|
||||
{{ t('components.library.FileUpload.table.upload.status.uploaded') }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-else-if="file.active"
|
||||
class="ui warning label"
|
||||
>
|
||||
<span key="2">
|
||||
{{ t('components.library.FileUpload.table.upload.status.uploading') }}
|
||||
</span>
|
||||
|
||||
{{ t('components.library.FileUpload.table.upload.progress', {percent: parseFloat(file.progress ?? '0.00')}) }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="ui label"
|
||||
>
|
||||
<span key="3">
|
||||
{{ t('components.library.FileUpload.table.upload.status.pending') }}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="file.error">
|
||||
<Button
|
||||
v-if="retryableFiles.includes(file)"
|
||||
tiny
|
||||
style="float: right;"
|
||||
:title="labels.tooltips.retry"
|
||||
icon="bi-arrow-clockwise"
|
||||
@click.prevent="retry([file])"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="!file.success">
|
||||
<Button
|
||||
tiny
|
||||
style="float: right;"
|
||||
icon="bi-trash-fill"
|
||||
@click.prevent="upload.remove(file)"
|
||||
/>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Edit the metadata of uploaded files -->
|
||||
|
||||
<Table
|
||||
:class="$style.table"
|
||||
v-if="files.length > 0"
|
||||
:grid-template-columns="['1fr', 'auto', 'auto', 'auto']"
|
||||
>
|
||||
<template #header>
|
||||
<b class="ten wide">
|
||||
{{ t('components.library.FileUpload.table.upload.header.filename') }}
|
||||
</b>
|
||||
<b>
|
||||
{{ t('components.library.FileUpload.table.upload.header.size') }}
|
||||
</b>
|
||||
<b>
|
||||
{{ t('components.library.FileUpload.table.upload.header.status') }}
|
||||
</b>
|
||||
<b>
|
||||
{{ t('components.library.FileUpload.table.upload.header.actions') }}
|
||||
</b>
|
||||
</template>
|
||||
|
||||
<!-- Retry row -->
|
||||
<template v-if="retryableFiles.length > 1">
|
||||
<b> </b>
|
||||
<b />
|
||||
<b />
|
||||
<b>
|
||||
<Button
|
||||
auto
|
||||
primary
|
||||
@click.prevent="retry(retryableFiles)"
|
||||
>
|
||||
{{ t('components.library.FileUpload.button.retry') }}
|
||||
</Button>
|
||||
</b>
|
||||
</template>
|
||||
|
||||
<!-- Rows for each file -->
|
||||
<template
|
||||
v-for="file in sortedFiles"
|
||||
:key="file.id"
|
||||
>
|
||||
<b :title="file.name">
|
||||
{{ truncate(file.name ?? '', 60) }}
|
||||
</b>
|
||||
<b>{{ humanSize(file.size ?? 0) }}</b>
|
||||
<b>
|
||||
<span
|
||||
v-if="typeof file.error === 'string' && file.error"
|
||||
class="ui tooltip"
|
||||
:data-tooltip="labels.tooltips[file.error]"
|
||||
>
|
||||
<span class="ui danger icon label">
|
||||
<i class="bi bi-question-circle-fill" /> {{ file.error }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-else-if="file.success"
|
||||
class="ui success label"
|
||||
>
|
||||
<span key="1">
|
||||
{{ t('components.library.FileUpload.table.upload.status.uploaded') }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-else-if="file.active"
|
||||
class="ui warning label"
|
||||
>
|
||||
<span key="2">
|
||||
{{ t('components.library.FileUpload.table.upload.status.uploading') }}
|
||||
</span>
|
||||
|
||||
{{ t('components.library.FileUpload.table.upload.progress', {percent: parseFloat(file.progress ?? '0.00')}) }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="ui label"
|
||||
>
|
||||
<span key="3">
|
||||
{{ t('components.library.FileUpload.table.upload.status.pending') }}
|
||||
</span>
|
||||
</span>
|
||||
</b>
|
||||
<b>
|
||||
<template v-if="file.error">
|
||||
<Button
|
||||
v-if="retryableFiles.includes(file)"
|
||||
square
|
||||
secondary
|
||||
:title="labels.tooltips.retry"
|
||||
icon="bi-arrow-clockwise"
|
||||
@click.prevent="retry([file])"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="!file.success">
|
||||
<Button
|
||||
square-small
|
||||
destructive
|
||||
icon="bi-trash-fill"
|
||||
@click.prevent="upload.remove(file)"
|
||||
/>
|
||||
</template>
|
||||
</b>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<!-- Progressive disclosure: Import from server -->
|
||||
|
||||
<Section
|
||||
:h2="t('components.library.FileUpload.header.server')"
|
||||
align-left
|
||||
no-items
|
||||
v-bind="
|
||||
section
|
||||
? { collapse: () => { section = false } }
|
||||
: { expand: () => { section = true } }
|
||||
"
|
||||
>
|
||||
<div style="grid-column: 1 / -1">
|
||||
<fs-browser
|
||||
v-model="fsPath"
|
||||
:loading="isLoadingFs"
|
||||
:data="fsStatus"
|
||||
@import="importFs"
|
||||
/>
|
||||
<template v-if="fsStatus && fsStatus.import">
|
||||
<h3 class="ui header">
|
||||
{{ t('components.library.FileUpload.header.status') }}
|
||||
</h3>
|
||||
<p v-if="fsStatus.import.reference !== importReference">
|
||||
{{ t('components.library.FileUpload.description.previousImport') }}
|
||||
</p>
|
||||
<p v-else>
|
||||
{{ t('components.library.FileUpload.description.import') }}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
v-if="fsStatus.import.status === 'started' || fsStatus.import.status === 'pending'"
|
||||
secondary
|
||||
@click="cancelFsScan"
|
||||
>
|
||||
{{ t('components.library.FileUpload.button.cancel') }}
|
||||
</Button>
|
||||
<fs-logs :data="fsStatus.import" />
|
||||
</template>
|
||||
</div>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style module lang="scss">
|
||||
.file-uploads {
|
||||
padding: 32px;
|
||||
border-radius: var(--fw-border-radius);
|
||||
border: 2px dashed var(--border-color);
|
||||
}
|
||||
.table {
|
||||
b:not(:first-child) { margin-left: 12px; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,7 +10,7 @@ import { useStore } from '~/store'
|
|||
import FileUpload from 'vue-upload-component'
|
||||
|
||||
const props = defineProps<{
|
||||
channel: components['schemas']['Channel']['uuid'];
|
||||
channel?: components['schemas']['Channel']['uuid'];
|
||||
}>()
|
||||
|
||||
const { get } = useCookies()
|
||||
|
@ -94,6 +94,7 @@ export default { inheritAttrs: false }
|
|||
<file-upload
|
||||
ref="upload"
|
||||
v-bind="$attrs"
|
||||
:class="$style.uploader"
|
||||
:post-action="store.getters['instance/absoluteUrl']('/api/v2/uploads/')"
|
||||
:multiple="true"
|
||||
:thread="1"
|
||||
|
@ -106,3 +107,10 @@ export default { inheritAttrs: false }
|
|||
<slot />
|
||||
</file-upload>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.uploader label {
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -28,6 +28,8 @@ import Button from '~/components/ui/Button.vue'
|
|||
import Input from '~/components/ui/Input.vue'
|
||||
import Loader from '~/components/ui/Loader.vue'
|
||||
|
||||
// This is the 'processing' tab in the old upload process
|
||||
|
||||
interface Events {
|
||||
(e: 'fetch-start'): void
|
||||
}
|
||||
|
@ -141,17 +143,6 @@ const getImportStatusChoice = (importStatus: ImportStatus) => {
|
|||
<template>
|
||||
<Layout form>
|
||||
<div class="fields">
|
||||
<div class="ui six wide field">
|
||||
<form @submit.prevent="console.log(search);query = search">
|
||||
<Input
|
||||
id="files-search"
|
||||
v-model="search"
|
||||
search
|
||||
:label="t('views.content.libraries.FilesTable.label.search')"
|
||||
:placeholder="labels.searchPlaceholder"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="import-status">
|
||||
{{ t('views.content.libraries.FilesTable.label.importStatus') }}
|
||||
|
|
Loading…
Reference in New Issue