249 lines
7.3 KiB
Vue
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>
|