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 label: string
isDangerous?: boolean isDangerous?: boolean
allowAll?: boolean allowAll?: boolean
confirmColor?: string confirmColor?: 'success' | 'danger'
confirmationMessage?: string confirmationMessage?: string
filterChackable?: (item: any) => boolean filterChackable?: (item: any) => boolean
} }
@ -61,6 +61,13 @@ const checkable = computed(() => {
.map(item => item[props.idField] as string) .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 => { const objects = computed(() => props.objectsData.results.map(object => {
return props.customObjects.find(custom => custom[props.idField] === object[props.idField]) return props.customObjects.find(custom => custom[props.idField] === object[props.idField])
?? object ?? object
@ -178,7 +185,7 @@ const gridColumns = computed(() => {
columns += 1 columns += 1
} }
return Array.from({ length: columns }, () => 'auto' as 'auto') return Array.from({ length: columns }, () => 'auto' as const)
}) })
</script> </script>
@ -209,14 +216,16 @@ const gridColumns = computed(() => {
v-if="actionUrl && actions.length > 0 || refreshable" v-if="actionUrl && actions.length > 0 || refreshable"
:style="{ gridColumn: `span ${gridColumns.length}`, height: '128px' }" :style="{ gridColumn: `span ${gridColumns.length}`, height: '128px' }"
> >
<Layout <Layout
v-if="actionUrl && actions.length > 0"
stack stack
no-gap no-gap
v-if="actionUrl && actions.length > 0"
> >
<label for="actions-select">{{ t('components.common.ActionTable.label.actions') }}</label> <label for="actions-select">{{ t('components.common.ActionTable.label.actions') }}</label>
<Layout flex class="ui form"> <Layout
flex
class="ui form"
>
<select <select
id="actions-select" id="actions-select"
v-model="currentActionName" v-model="currentActionName"
@ -331,8 +340,8 @@ const gridColumns = computed(() => {
<Spacer grow /> <Spacer grow />
<Layout <Layout
label
v-if="refreshable" v-if="refreshable"
label
class="right floated" class="right floated"
> >
<span v-if="needsRefresh"> <span v-if="needsRefresh">
@ -343,8 +352,8 @@ const gridColumns = computed(() => {
icon="bi-arrow-clockwise" icon="bi-arrow-clockwise"
:title="labels.refresh" :title="labels.refresh"
:aria-label="labels.refresh" :aria-label="labels.refresh"
@click="$emit('refresh')"
style="align-self: end;" style="align-self: end;"
@click="$emit('refresh')"
> >
{{ labels.refresh }} {{ labels.refresh }}
</Button> </Button>

View File

@ -25,6 +25,10 @@ import useSharedLabels from '~/composables/locale/useSharedLabels'
import Alert from '~/components/ui/Alert.vue' import Alert from '~/components/ui/Alert.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import Slider from '~/components/ui/Slider.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 { interface Events {
(e: 'uploads-finished', delta: number):void (e: 'uploads-finished', delta: number):void
@ -343,6 +347,9 @@ useEventListener(window, 'beforeunload', (event) => {
event.preventDefault() event.preventDefault()
return (event.returnValue = t('components.library.FileUpload.message.listener')) return (event.returnValue = t('components.library.FileUpload.message.listener'))
}) })
// collapse section
const section = ref(false)
</script> </script>
<template> <template>
@ -361,27 +368,8 @@ useEventListener(window, 'beforeunload', (event) => {
:options="options" :options="options"
:label="t('components.manage.library.UploadsTable.label.visibility')" :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> <Alert blue>
<h2 class="ui header">
{{ t('components.library.FileUpload.header.local') }}
</h2>
<p> <p>
{{ t('components.library.FileUpload.message.local.message') }} {{ t('components.library.FileUpload.message.local.message') }}
</p> </p>
@ -402,9 +390,55 @@ useEventListener(window, 'beforeunload', (event) => {
</ul> </ul>
</Alert> </Alert>
<h2 class="ui header"> <file-upload-widget
{{ t('components.library.FileUpload.header.server') }} ref="upload"
</h2> 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 <Alert
v-if="fsErrors.length > 0" v-if="fsErrors.length > 0"
red red
@ -421,32 +455,8 @@ useEventListener(window, 'beforeunload', (event) => {
</li> </li>
</ul> </ul>
</Alert> </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 <!-- Show list of processed files -->
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>
<library-files-table <library-files-table
:needs-refresh="needsRefresh" :needs-refresh="needsRefresh"
@ -455,111 +465,55 @@ useEventListener(window, 'beforeunload', (event) => {
:custom-objects="Object.values(uploads.objects)" :custom-objects="Object.values(uploads.objects)"
@fetch-start="needsRefresh = false" @fetch-start="needsRefresh = false"
/> />
<div class="ui top attached tabular menu">
<a <!-- Edit the metadata of uploaded files -->
href=""
:class="['item', {active: currentTab === 'uploads'}]" <Table
@click.prevent="currentTab = 'uploads'" :class="$style.table"
>
{{ 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" v-if="files.length > 0"
class="table-wrapper" :grid-template-columns="['1fr', 'auto', 'auto', 'auto']"
> >
<table class="ui unstackable table"> <template #header>
<thead> <b class="ten wide">
<tr>
<th class="ten wide">
{{ t('components.library.FileUpload.table.upload.header.filename') }} {{ t('components.library.FileUpload.table.upload.header.filename') }}
</th> </b>
<th> <b>
{{ t('components.library.FileUpload.table.upload.header.size') }} {{ t('components.library.FileUpload.table.upload.header.size') }}
</th> </b>
<th> <b>
{{ t('components.library.FileUpload.table.upload.header.status') }} {{ t('components.library.FileUpload.table.upload.header.status') }}
</th> </b>
<th> <b>
{{ t('components.library.FileUpload.table.upload.header.actions') }} {{ t('components.library.FileUpload.table.upload.header.actions') }}
</th> </b>
</tr> </template>
<tr v-if="retryableFiles.length > 1">
<th class="ten wide" /> <!-- Retry row -->
<th /> <template v-if="retryableFiles.length > 1">
<th /> <b> </b>
<th> <b />
<b />
<b>
<Button <Button
tiny auto
style="float: right;" primary
@click.prevent="retry(retryableFiles)" @click.prevent="retry(retryableFiles)"
> >
{{ t('components.library.FileUpload.button.retry') }} {{ t('components.library.FileUpload.button.retry') }}
</Button> </Button>
</th> </b>
</tr> </template>
</thead>
<tbody> <!-- Rows for each file -->
<tr <template
v-for="file in sortedFiles" v-for="file in sortedFiles"
:key="file.id" :key="file.id"
> >
<td :title="file.name"> <b :title="file.name">
{{ truncate(file.name ?? '', 60) }} {{ truncate(file.name ?? '', 60) }}
</td> </b>
<td>{{ humanSize(file.size ?? 0) }}</td> <b>{{ humanSize(file.size ?? 0) }}</b>
<td> <b>
<span <span
v-if="typeof file.error === 'string' && file.error" v-if="typeof file.error === 'string' && file.error"
class="ui tooltip" class="ui tooltip"
@ -595,13 +549,13 @@ useEventListener(window, 'beforeunload', (event) => {
{{ t('components.library.FileUpload.table.upload.status.pending') }} {{ t('components.library.FileUpload.table.upload.status.pending') }}
</span> </span>
</span> </span>
</td> </b>
<td> <b>
<template v-if="file.error"> <template v-if="file.error">
<Button <Button
v-if="retryableFiles.includes(file)" v-if="retryableFiles.includes(file)"
tiny square
style="float: right;" secondary
:title="labels.tooltips.retry" :title="labels.tooltips.retry"
icon="bi-arrow-clockwise" icon="bi-arrow-clockwise"
@click.prevent="retry([file])" @click.prevent="retry([file])"
@ -609,23 +563,66 @@ useEventListener(window, 'beforeunload', (event) => {
</template> </template>
<template v-else-if="!file.success"> <template v-else-if="!file.success">
<Button <Button
tiny square-small
style="float: right;" destructive
icon="bi-trash-fill" icon="bi-trash-fill"
@click.prevent="upload.remove(file)" @click.prevent="upload.remove(file)"
/> />
</template> </template>
</td> </b>
</tr> </template>
</tbody> </Table>
</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> </div>
</Section>
</template> </template>
<style scoped lang="scss"> <style module lang="scss">
.file-uploads { .file-uploads {
padding: 32px; padding: 32px;
border-radius: var(--fw-border-radius); border-radius: var(--fw-border-radius);
border: 2px dashed var(--border-color); border: 2px dashed var(--border-color);
} }
.table {
b:not(:first-child) { margin-left: 12px; }
}
</style> </style>

View File

@ -10,7 +10,7 @@ import { useStore } from '~/store'
import FileUpload from 'vue-upload-component' import FileUpload from 'vue-upload-component'
const props = defineProps<{ const props = defineProps<{
channel: components['schemas']['Channel']['uuid']; channel?: components['schemas']['Channel']['uuid'];
}>() }>()
const { get } = useCookies() const { get } = useCookies()
@ -94,6 +94,7 @@ export default { inheritAttrs: false }
<file-upload <file-upload
ref="upload" ref="upload"
v-bind="$attrs" v-bind="$attrs"
:class="$style.uploader"
:post-action="store.getters['instance/absoluteUrl']('/api/v2/uploads/')" :post-action="store.getters['instance/absoluteUrl']('/api/v2/uploads/')"
:multiple="true" :multiple="true"
:thread="1" :thread="1"
@ -106,3 +107,10 @@ export default { inheritAttrs: false }
<slot /> <slot />
</file-upload> </file-upload>
</template> </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 Input from '~/components/ui/Input.vue'
import Loader from '~/components/ui/Loader.vue' import Loader from '~/components/ui/Loader.vue'
// This is the 'processing' tab in the old upload process
interface Events { interface Events {
(e: 'fetch-start'): void (e: 'fetch-start'): void
} }
@ -141,17 +143,6 @@ const getImportStatusChoice = (importStatus: ImportStatus) => {
<template> <template>
<Layout form> <Layout form>
<div class="fields"> <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"> <div class="field">
<label for="import-status"> <label for="import-status">
{{ t('views.content.libraries.FilesTable.label.importStatus') }} {{ t('views.content.libraries.FilesTable.label.importStatus') }}