Files
RenewCMS/web/src/components/ActivityExportDialog.vue
T

249 lines
7.3 KiB
Vue

<script setup lang="ts">
import { t } from '@/i18n'
import { computed } from 'vue'
import type { ExportStage } from '@/composables/useActivityExport'
import { EXPORT_RANGE_KEYS, exportRangeLabel, type ExportRange } from '@/lib/activity'
const props = defineProps<{
stage: ExportStage
canSubmit: boolean
fileName: string
}>()
const emit = defineEmits<{
submit: []
close: []
}>()
const open = defineModel<boolean>('open', { default: false })
const range = defineModel<ExportRange>('range', { default: 'all' })
const from = defineModel<string>('from', { default: '' })
const to = defineModel<string>('to', { default: '' })
const MODAL_CONTENT =
'divide-y-0 shadow-(--shadow-modal) ring-0 sm:max-w-md md:top-35 md:translate-y-0 max-md:!inset-x-0 max-md:!top-auto max-md:!bottom-0 max-md:!left-0 max-md:!w-auto max-md:!max-w-none max-md:!translate-x-0 max-md:!translate-y-0 max-md:before:absolute max-md:before:top-2 max-md:before:left-1/2 max-md:before:h-1 max-md:before:w-9 max-md:before:-translate-x-1/2 max-md:before:bg-(--ui-border-accented) max-md:pt-3'
const BUTTON_CLASS = 'h-11 justify-center px-3 text-sm md:h-8'
const OUTLINE_CLASS = `${BUTTON_CLASS} border border-accented bg-default text-highlighted ring-0`
const FIELD_LABEL_CLASS = 'font-medium text-highlighted'
const CLOSE_BUTTON = {
color: 'neutral',
variant: 'ghost',
class: 'size-11 justify-center p-0 text-muted md:size-7',
} as const
const preparing = computed(() => props.stage === 'preparing')
const rangeItems = computed(() =>
EXPORT_RANGE_KEYS.map((key) => ({ value: key, label: exportRangeLabel(key) })),
)
const showCustomFields = computed(() => range.value === 'custom')
function onOpenChange(value: boolean) {
if (!value) emit('close')
}
</script>
<template>
<UModal
:open="open"
:title="t('Export all activity')"
:description="t('Downloads every event as a CSV file. Filters on the page are ignored.')"
:dismissible="!preparing"
:close="preparing ? false : CLOSE_BUTTON"
:ui="{
content: MODAL_CONTENT,
close: 'end-6 top-8 md:top-5',
header: 'min-h-0 items-start px-6 pt-5 pb-0 sm:px-6',
title: 'pe-14 text-lg/6 font-semibold md:pe-10',
description: 'mt-1 pe-14 text-sm text-muted text-pretty md:pe-10',
body: 'px-6 pt-5 pb-0 sm:px-6 sm:pt-5 sm:pb-0',
footer: 'px-6 py-5 sm:px-6',
}"
@update:open="onOpenChange"
>
<template #body>
<div class="flex flex-col gap-4">
<template v-if="stage === 'configure'">
<UFormField :label="t('Format')" :ui="{ label: FIELD_LABEL_CLASS }">
<UInput
model-value="CSV"
readonly
:aria-label="t('Format')"
class="w-full"
:ui="{
base: 'h-8 rounded-none border border-accented bg-default text-sm text-highlighted ring-0',
}"
/>
</UFormField>
<UFormField
:label="t('Date range')"
:help="t('Default is all time. Filters on the page are ignored.')"
:ui="{ label: FIELD_LABEL_CLASS, help: 'mt-1.5 text-xs text-muted' }"
>
<USelect
v-model="range"
:items="rangeItems"
value-key="value"
icon="i-lucide-calendar"
color="neutral"
variant="outline"
:aria-label="t('Date range')"
class="w-full"
:ui="{
base: 'h-11 rounded-none border border-accented bg-default ps-8 pe-8 text-sm ring-0 md:h-8',
leadingIcon: 'size-3.5 text-muted',
trailingIcon: 'size-3.5 text-muted',
}"
/>
</UFormField>
<div v-if="showCustomFields" class="flex items-center gap-2">
<UInput
v-model="from"
type="date"
icon="i-lucide-calendar"
:aria-label="t('Start date')"
class="min-w-0 flex-1"
:ui="{ base: 'h-11 w-full min-w-0 text-lg md:h-8 md:text-sm' }"
/>
<UInput
v-model="to"
type="date"
icon="i-lucide-calendar"
:aria-label="t('End date')"
class="min-w-0 flex-1"
:ui="{ base: 'h-11 w-full min-w-0 text-lg md:h-8 md:text-sm' }"
/>
</div>
</template>
<div v-else-if="preparing" class="flex flex-col gap-3">
<span class="flex items-center gap-2 font-medium text-highlighted">
<UIcon name="i-lucide-loader-circle" class="size-4 animate-spin" />
{{ t('Preparing download…') }}
</span>
<UProgress size="xs" color="primary" />
<span class="text-muted text-pretty">
{{
t(
'The file starts downloading as soon as it is ready. Keep this window open.',
)
}}
</span>
</div>
<div v-else-if="stage === 'started'" class="flex items-start gap-3">
<UIcon
name="i-lucide-circle-check"
class="mt-0.5 size-5 shrink-0 text-success"
/>
<div class="flex flex-col gap-0.5">
<span class="text-md font-semibold text-highlighted">{{
t('Download started')
}}</span>
<span class="text-muted">{{
t(
"{0} is downloading. Check your browser's downloads if nothing appears.",
[fileName],
)
}}</span>
</div>
</div>
<UAlert
v-else-if="stage === 'failed'"
role="alert"
color="error"
variant="soft"
icon="i-lucide-circle-x"
:close="false"
:title="t('Couldn\'t prepare the download')"
:description="t('Nothing was downloaded. Try again in a moment.')"
/>
<UAlert
v-else-if="stage === 'denied'"
role="alert"
color="error"
variant="soft"
icon="i-lucide-lock"
:close="false"
:title="t('You can\'t export all activity')"
:description="
t(
'Only owners and admins can export all activity. Ask an owner if you need a copy.',
)
"
/>
<div
v-else-if="stage === 'empty'"
class="flex flex-col items-center gap-2.5 py-3 text-center"
>
<span class="flex size-11 items-center justify-center bg-elevated text-toned">
<UIcon name="i-lucide-file-x" class="size-5" />
</span>
<span class="text-md font-semibold text-highlighted">{{
t('Nothing to export')
}}</span>
<span class="max-w-75 text-muted text-pretty">{{
t('There is no activity in this date range. Choose a wider range.')
}}</span>
</div>
</div>
</template>
<template #footer>
<div class="flex w-full flex-col-reverse gap-2 md:flex-row md:justify-end">
<template v-if="stage === 'configure' || preparing || stage === 'failed'">
<UButton
color="neutral"
variant="outline"
block
:class="[OUTLINE_CLASS, 'md:w-auto']"
@click="emit('close')"
>
{{ t('Cancel') }}
</UButton>
<UButton
v-if="stage === 'configure'"
icon="i-lucide-download"
block
:class="[BUTTON_CLASS, 'md:w-auto']"
:disabled="!canSubmit"
@click="emit('submit')"
>
{{ t('Export CSV') }}
</UButton>
<UButton
v-else-if="preparing"
block
disabled
:class="[BUTTON_CLASS, 'md:w-auto']"
>
{{ t('Preparing…') }}
</UButton>
<UButton
v-else
icon="i-lucide-rotate-cw"
block
:class="[BUTTON_CLASS, 'md:w-auto']"
@click="emit('submit')"
>
{{ t('Retry') }}
</UButton>
</template>
<UButton
v-else
color="neutral"
variant="outline"
block
:class="[OUTLINE_CLASS, 'md:w-auto']"
@click="emit('close')"
>
{{ t('Close') }}
</UButton>
</div>
</template>
</UModal>
</template>