feat(activity): update summary cards to toggles and introduce export menu
This commit is contained in:
1 parent
73f4b539fe
commit
03b39e64f2
30 files changed
+2376
-238
No files matched your search
@@ -0,0 +1,248 @@
|
||||
<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>
|
||||
Reference in new issue
Block a user