Skip to content
  1. Tables
  2. Export menu

Export menu

The export button of a register. It opens on what is left of the quota this period, then one button per format. Nothing downloads here: the callback does.

import { ExportMenu } from '@/components/export-menu'

<ExportMenu used={3} quota={10} onExport={(format) => startExport(format)} />
<script setup lang="ts">
import ExportMenu from '@/components/export-menu.vue'
</script>

<template>
  <ExportMenu :used="3" :quota="10" @export="startExport" />
</template>

Installation

Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.

'use client'

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { ArrowDownTrayIcon } from '@heroicons/react/16/solid'
import type React from 'react'
import { Button } from './button'
import { Meter } from './meter'

export type ExportFormat = { value: string; label: string }

const defaultFormats: ExportFormat[] = [
  { value: 'csv', label: 'Download CSV' },
  { value: 'json', label: 'Download JSON' },
]

export function ExportMenu({
  used,
  quota,
  period = 'this month',
  formats = defaultFormats,
  onExport,
  label = 'Export',
  className,
}: {
  used: number
  quota: number
  period?: string
  formats?: ExportFormat[]
  onExport: (format: string) => void
  label?: React.ReactNode
  className?: string
}) {
  let exhausted = used >= quota

  return (
    <Popover>
      <PopoverButton as={Button} outline className={className}>
        <ArrowDownTrayIcon data-slot="icon" />
        {label}
      </PopoverButton>
      <PopoverPanel
        anchor={{ to: 'bottom end', gap: 8, padding: 8 }}
        className="z-50 w-64 rounded-xl border border-zinc-950/10 bg-white p-4 shadow-lg outline-none dark:border-white/10 dark:bg-zinc-800"
      >
        {({ close }) => (
          <>
            <p className="text-base/6 sm:text-sm/6 text-zinc-500 dark:text-zinc-400">
              <span className="font-medium text-zinc-950 tabular-nums dark:text-white">
                {used}/{quota}
              </span>{' '}
              exports {period}
            </p>
            <Meter className="mt-2" value={used} max={quota} label="Export quota" />
            {exhausted && (
              <p className="mt-2 text-base/6 sm:text-sm/6 text-zinc-500 dark:text-zinc-400">
                No exports left {period}.
              </p>
            )}
            <div className="mt-4 flex flex-col gap-2">
              {formats.map((format) => (
                <Button
                  key={format.value}
                  outline
                  disabled={exhausted}
                  onClick={() => {
                    onExport(format.value)
                    close()
                  }}
                >
                  {format.label}
                </Button>
              ))}
            </div>
          </>
        )}
      </PopoverPanel>
    </Popover>
  )
}

Also copy Button, Meter.

<script lang="ts">
export type ExportFormat = { value: string; label: string }
</script>

<script setup lang="ts">
import { ArrowDownTrayIcon } from '@heroicons/vue/16/solid'
import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui'
import { computed, ref } from 'vue'
import Button from './button.vue'
import Meter from './meter.vue'

defineOptions({ inheritAttrs: false })

const props = withDefaults(
  defineProps<{
    used: number
    quota: number
    period?: string
    formats?: ExportFormat[]
    label?: string
  }>(),
  {
    period: 'this month',
    formats: () => [
      { value: 'csv', label: 'Download CSV' },
      { value: 'json', label: 'Download JSON' },
    ],
    label: 'Export',
  }
)

const emit = defineEmits<{ export: [format: string] }>()

const open = ref(false)
const exhausted = computed(() => props.used >= props.quota)

function pick(format: string) {
  emit('export', format)
  open.value = false
}
</script>

<template>
  <PopoverRoot v-model:open="open">
    <PopoverTrigger as-child>
      <Button outline v-bind="$attrs">
        <ArrowDownTrayIcon data-slot="icon" />
        <slot name="label">{{ label }}</slot>
      </Button>
    </PopoverTrigger>
    <PopoverPortal>
      <PopoverContent
        align="end"
        :side-offset="8"
        :collision-padding="8"
        class="z-50 w-64 rounded-xl border border-zinc-950/10 bg-white p-4 shadow-lg outline-none dark:border-white/10 dark:bg-zinc-800"
      >
        <p class="text-base/6 sm:text-sm/6 text-zinc-500 dark:text-zinc-400">
          <span class="font-medium text-zinc-950 tabular-nums dark:text-white">{{ used }}/{{ quota }}</span>
          exports {{ period }}
        </p>
        <Meter class="mt-2" :value="used" :max="quota" label="Export quota" />
        <p v-if="exhausted" class="mt-2 text-base/6 sm:text-sm/6 text-zinc-500 dark:text-zinc-400">
          No exports left {{ period }}.
        </p>
        <div class="mt-4 flex flex-col gap-2">
          <Button v-for="format in formats" :key="format.value" outline :disabled="exhausted" @click="pick(format.value)">
            {{ format.label }}
          </Button>
        </div>
      </PopoverContent>
    </PopoverPortal>
  </PopoverRoot>
</template>

Also copy Button, Meter.

Component API

PropDefaultDescription
ExportMenubuilt on the Headless UI <Headless.Popover> component
used-Exports already made this period.
quota-Exports allowed per period. At the cap, the format buttons disable and a line says no exports are left.
period'this month'Named in the sentence: `3/10 exports this month`.
formatsCSV and JSONArray of `{ value, label }`, one button each.
onExport-Called with the value of the format picked. The popover closes.
label'Export'Label of the trigger button.
className-Classes of the trigger button.
PropDefaultDescription
ExportMenubuilt on the Reka UI <PopoverRoot> component
used-Exports already made this period.
quota-Exports allowed per period. At the cap, the format buttons disable.
period'this month'Named in the sentence: `3/10 exports this month`.
formatsCSV and JSONArray of `{ value, label }`, one button each.
@export-Emitted with the value of the format picked. The popover closes.
label'Export'Label of the trigger button. Use the `#label` slot for markup.

Examples

Basic example

Pass what the account used and what it may use. The meter below the count says the same thing at a glance:

import { ExportMenu } from '@/components/export-menu'

<ExportMenu used={3} quota={10} onExport={(format) => startExport(format)} />
<script setup lang="ts">
import ExportMenu from '@/components/export-menu.vue'
</script>

<template>
  <ExportMenu :used="3" :quota="10" @export="startExport" />
</template>

Quota reached

At the cap the buttons disable; the count says why:

<ExportMenu used={10} quota={10} onExport={startExport} />
<ExportMenu :used="10" :quota="10" @export="startExport" />

Formats and period

Name the period the quota resets on, and list the formats the backend produces:

<ExportMenu
  used={1}
  quota={5}
  period="this week"
  formats={[
    { value: 'csv', label: 'Download CSV' },
    { value: 'xlsx', label: 'Download Excel' },
    { value: 'pdf', label: 'Download PDF' },
  ]}
  onExport={startExport}
/>
<ExportMenu
  :used="1"
  :quota="5"
  period="this week"
  :formats="[
    { value: 'csv', label: 'Download CSV' },
    { value: 'xlsx', label: 'Download Excel' },
    { value: 'pdf', label: 'Download PDF' },
  ]"
  @export="startExport"
/>