feat(front): layout upload modal; use new components

This commit is contained in:
upsiflu 2025-03-19 13:18:04 +01:00
parent e05a5b9d7a
commit 6eb5c14176
4 changed files with 244 additions and 239 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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') }}