{
  "id": "BE06",
  "title": "Export dialog",
  "topic": "bulk-editing",
  "use": "Export with scope and format. A dialog says what goes out, in which format and how many records that is, and only then the file is made. Take it wherever what is exported varies — the view, a selection, everything. Where the same file goes out every time, a button with a clear label is enough; where the same output is needed every period, it is a saved report (SC06), not an export.",
  "decisions": [
    "Export opens a dialog that asks first for the scope, then for the format, before any file exists: the file leaves the application, and whoever first sees its scope in the file has already passed the mistake on.",
    "Every scope names its count, and the view names what narrows it — '22 documents · Filter: Document type, Status', never a bare 'Selection': without the number someone exports 84 rows when they meant the 22 filtered.",
    "All documents says how many of them the view does not show: the export of everything is the one nobody meant.",
    "The accent button carries the count of the chosen scope — Export 22 documents: it is the last thing read before the file is made.",
    "The dialog opens on This view, even when rows are selected: what the screen shows is what the user has in mind, and the other scopes are one choice away, each with its count.",
    "Format and character set are selects, though they hold three and two values, against the select's rule for two to four choices: the formats a product offers grow (PDF, DATEV), and the character set is a setting the recipient dictates, which must not draw the eye from the scope.",
    "The format exported with last is preselected, with its character set: a file usually goes to the same recipient, who expects it the same way each time.",
    "The columns are the ones the table shows, and the dialog names them read-only with their number — All visible (7): they are chosen in the table's View menu, where they can be seen, and a second choice in the dialog would be two truths about the same columns.",
    "What does not come along is said before the export — the attachments and the hidden columns: a file without them looks complete.",
    "The file name is suggested with the date and the filter in it — documents_2026-09-30_fuel-receipt_open.csv: once the file has left the application, its name is all that says what it holds.",
    "Cancel closes without asking, even after a file name was typed: the dialog changes no record, so dropping its entries only cancels the export, and opening it again suggests the name anew."
  ],
  "try": [
    {
      "do": "Press Filter",
      "then": "two filters are set, Document type and Status, and the count below the bar reads 22 of 84 entries"
    },
    {
      "do": "Choose Export",
      "then": "the dialog asks for the scope first, and each scope names its count — This view 22 documents with the filters it carries, All documents 84 including the 62 not in this view; Selected rows is unavailable, since nothing is selected"
    },
    {
      "do": "Look at the accent button and the file name",
      "then": "the button reads Export 22 documents, and the file name carries the date and the two filter values"
    },
    {
      "do": "Choose All documents",
      "then": "the button reads Export 84 documents, and the file name says all instead of the filter values"
    },
    {
      "do": "Cancel, tick three rows and choose Export again",
      "then": "Selected rows names 3 documents, and choosing it makes the button read Export 3 documents"
    },
    {
      "do": "Choose Excel as the format",
      "then": "the character set becomes unavailable, since it applies to CSV only, and the file name ends in .xlsx"
    },
    {
      "do": "Look above the footer",
      "then": "Not included names the attachments and the two hidden columns, Cost center and Clerk, and Columns reads All visible (7)"
    },
    {
      "do": "Press the accent button",
      "then": "the dialog closes, and a toast names how many documents went into which file"
    },
    {
      "do": "Choose Export once more",
      "then": "Excel is preselected, the format exported with last, and the scope is This view again"
    },
    {
      "do": "Cancel, show Cost center from the View menu and choose Export again",
      "then": "Columns reads All visible (8), and Not included names the attachments and Clerk alone"
    },
    {
      "do": "Type a file name of your own and press Cancel",
      "then": "the dialog closes without asking, and opening it again suggests the name anew"
    },
    {
      "do": "Type a file name again and click beside the dialog",
      "then": "the dialog closes without asking, as Cancel does"
    }
  ],
  "revision": 1,
  "changes": [],
  "reviewed": false,
  "source": "<!-- Demo only, not part of the pattern. The table takes the height the screen leaves it; here it is set to fit the frame. -->\n\n<ed-data-grid id=\"be06-grid\" label=\"Documents\" selectable selection-mode=\"multiple\" with-search with-view-menu data-ed-preload=\"ed-format-date\">\n  <ed-button slot=\"toolbar-actions\" id=\"be06-open\">\n    <ed-icon slot=\"start\" name=\"export\"></ed-icon>\n    Export\n  </ed-button>\n  <ed-button slot=\"toolbar-view\" id=\"be06-filter\" appearance=\"plain\">\n    <ed-icon slot=\"start\" name=\"filter\"></ed-icon>\n    <span id=\"be06-filter-label\">Filter</span>\n  </ed-button>\n</ed-data-grid>\n\n<ed-dialog id=\"be06-dialog\" light-dismiss label=\"Export documents\" style=\"--ed-dialog-width: 44rem\">\n  <div class=\"ed-stack ed-gap-5\">\n    <ed-radio-group id=\"be06-scope\" label=\"Scope\" appearance=\"card\" value=\"view\">\n      <ed-radio value=\"view\">\n        This view\n        <span slot=\"description\" id=\"be06-scope-view\"></span>\n      </ed-radio>\n      <ed-radio value=\"selected\">\n        Selected rows\n        <span slot=\"description\" id=\"be06-scope-selected\"></span>\n      </ed-radio>\n      <ed-radio value=\"all\">\n        All documents\n        <span slot=\"description\" id=\"be06-scope-all\"></span>\n      </ed-radio>\n    </ed-radio-group>\n\n    <div class=\"ed-grid ed-gap-4\" style=\"--ed-grid-min-column-size: 11rem\">\n      <ed-select id=\"be06-format\" label=\"Format\">\n        <ed-option value=\"csv-semicolon\">CSV (semicolon)</ed-option>\n        <ed-option value=\"csv-comma\">CSV (comma)</ed-option>\n        <ed-option value=\"xlsx\">Excel</ed-option>\n      </ed-select>\n      <ed-select id=\"be06-charset\" label=\"Character set\">\n        <ed-option value=\"utf-8\">UTF-8</ed-option>\n        <ed-option value=\"iso-8859-15\">ISO 8859-15</ed-option>\n      </ed-select>\n      <ed-input id=\"be06-columns\" label=\"Columns\" readonly></ed-input>\n    </div>\n    <ed-input id=\"be06-file\" label=\"File name\" required></ed-input>\n\n    <p id=\"be06-left-out\" class=\"ed-caption-s\"></p>\n  </div>\n\n  <ed-button slot=\"footer\" id=\"be06-export\">Export documents</ed-button>\n  <ed-button slot=\"footer\" appearance=\"outlined\" data-dialog=\"close\">Cancel</ed-button>\n</ed-dialog>\n\n<ed-toast-stack id=\"be06-toasts\"></ed-toast-stack>\n\n<!-- Demo only, not part of the pattern. The product supplies documents, the list as { id, date, partner, text, type, status, amount, costCenter, clerk } with the date as YYYY-MM-DD; and TODAY, the current date as YYYY-MM-DD, which the suggested file name carries. -->\n\n<!-- Demo only, not part of the pattern. The Filter button opens the product's filter surface, the drawer of FI01 or the panel of FI02, whose fields set grid.filters; here it toggles exampleFilters, one filter on Document type and one on Status, so the view holds fewer documents than the table. -->\n\n<!-- Demo only, not part of the pattern. The product supplies exportDocuments(request), which has the file made and handed to the browser to save, and resolves once it is. The request names the scope by what defines it — { scope: 'view', filters, searchTerm }, { scope: 'selected', keys } or { scope: 'all' } — with format, charset (null for Excel), columns, the ids of the visible columns in their order, and fileName; here no file is made, the call only takes the time. And lastExportSettings() and saveExportSettings(settings), the { format, charset } this user exported with last, kept for the next visit. What the screen does when an export fails is not part of this pattern. -->\n\n<script type=\"module\">\n  const day = value =>\n    Object.assign(document.createElement('ed-format-date'), { date: value, month: 'short', day: 'numeric', year: 'numeric' });\n  const euro = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' });\n  const statusVariant = { Open: 'neutral', Checked: 'info', Finalized: 'success' };\n  const EXTENSIONS = { 'csv-semicolon': '.csv', 'csv-comma': '.csv', xlsx: '.xlsx' };\n  const count = n => `${n} document${n === 1 ? '' : 's'}`;\n  const list = items => (items.length < 2 ? items.join('') : `${items.slice(0, -1).join(', ')} and ${items.at(-1)}`);\n  const slug = text => text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n\n  const grid = document.getElementById('be06-grid');\n  const open = document.getElementById('be06-open');\n  const filterButton = document.getElementById('be06-filter');\n  const filterLabel = document.getElementById('be06-filter-label');\n  const dialog = document.getElementById('be06-dialog');\n  const scope = document.getElementById('be06-scope');\n  const scopes = Object.fromEntries([...scope.querySelectorAll('ed-radio')].map(radio => [radio.getAttribute('value'), radio]));\n  const scopeText = {\n    view: document.getElementById('be06-scope-view'),\n    selected: document.getElementById('be06-scope-selected'),\n    all: document.getElementById('be06-scope-all')\n  };\n  const format = document.getElementById('be06-format');\n  const charset = document.getElementById('be06-charset');\n  const columnsField = document.getElementById('be06-columns');\n  const fileField = document.getElementById('be06-file');\n  const leftOut = document.getElementById('be06-left-out');\n  const apply = document.getElementById('be06-export');\n  const toasts = document.getElementById('be06-toasts');\n\n  grid.columns = [\n    { field: 'id', label: 'Document', rowheader: true, pinned: 'left', width: 140 },\n    { field: 'date', label: 'Date', width: 130, sortFn: 'datetime', searchable: false, formatter: day },\n    { field: 'partner', label: 'Partner', flex: 1, minWidth: 210 },\n    { field: 'text', label: 'Posting text', flex: 1, minWidth: 200 },\n    { field: 'type', label: 'Document type', width: 170, filterable: true, filterType: 'set' },\n    {\n      field: 'status',\n      label: 'Status',\n      width: 130,\n      filterable: true,\n      filterType: 'set',\n      formatter: value => Object.assign(document.createElement('ed-badge'), { variant: statusVariant[value], textContent: value })\n    },\n    { field: 'amount', label: 'Amount', width: 120, align: 'end', searchable: false, formatter: value => euro.format(value) },\n    { field: 'costCenter', label: 'Cost center', width: 120, hidden: true },\n    { field: 'clerk', label: 'Clerk', width: 160, hidden: true }\n  ];\n  grid.data = documents;\n\n  function showFilterCount() {\n    filterLabel.textContent = grid.filters.length ? `Filter (${grid.filters.length})` : 'Filter';\n  }\n\n  filterButton.addEventListener('click', () => {\n    grid.filters = grid.filters.length ? [] : exampleFilters;\n    showFilterCount();\n  });\n  grid.addEventListener('ed-data-grid-filter-change', showFilterCount);\n\n  // What the dialog was opened on: the counts per scope and the columns the table shows. The dialog\n  // covers the table, so none of it changes while it is open.\n  let counts = {};\n  let columns = [];\n  // Set once the user enters a name of their own; the suggestion then no longer follows the scope.\n  let edited = false;\n  let running = false;\n  // Each opening counts up, so an export that outlasts its dialog leaves the next one open.\n  let session = 0;\n\n  const labelOf = id => grid.columns.find(column => column.field === id)?.label ?? id;\n  const isCsv = () => format.value !== 'xlsx';\n\n  // The view is named by what narrows it, the filtered columns and the search.\n  function describeView() {\n    const term = grid.searchTerm.trim();\n    const parts = [count(counts.view)];\n    if (grid.filters.length) parts.push(`Filter: ${grid.filters.map(filter => labelOf(filter.id)).join(', ')}`);\n    if (term) parts.push(`Search: “${term}”`);\n    if (parts.length === 1) parts.push('no filter set');\n    return parts.join(' · ');\n  }\n\n  function suggestedName() {\n    const parts = ['documents', TODAY];\n    if (scope.value === 'all') parts.push('all');\n    else if (scope.value === 'selected') parts.push('selection');\n    else {\n      parts.push(...grid.filters.flatMap(filter => [filter.value].flat()).map(value => slug(String(value))));\n      if (grid.searchTerm.trim()) parts.push(slug(grid.searchTerm));\n    }\n    return parts.filter(Boolean).join('_') + EXTENSIONS[format.value];\n  }\n\n  function updateCharset() {\n    charset.disabled = !isCsv();\n    charset.hint = isCsv() ? '' : 'Only for CSV files';\n  }\n\n  function updateScope() {\n    const n = counts[scope.value] ?? 0;\n    apply.textContent = `Export ${count(n)}`;\n    apply.disabled = n === 0;\n    if (!edited) fileField.value = suggestedName();\n  }\n\n  open.addEventListener('click', () => {\n    session += 1;\n    const state = grid.getState();\n    const declared = grid.columns.map(column => column.field);\n    const order = state.columnOrder.length ? state.columnOrder.filter(id => declared.includes(id)) : declared;\n    columns = order.filter(id => !state.hidden.includes(id));\n    const hidden = declared.filter(id => state.hidden.includes(id)).map(labelOf);\n\n    counts = { view: grid.getProcessedRows().length, selected: grid.selectedRows.length, all: documents.length };\n    scopeText.view.textContent = describeView();\n    scopeText.selected.textContent = counts.selected ? count(counts.selected) : 'No row is selected';\n    const outside = counts.all - counts.view;\n    scopeText.all.textContent = outside ? `${count(counts.all)} · including the ${outside} not in this view` : count(counts.all);\n    scopes.view.disabled = counts.view === 0;\n    scopes.selected.disabled = counts.selected === 0;\n    scope.value = counts.view ? 'view' : 'all';\n\n    const settings = lastExportSettings();\n    format.value = settings.format;\n    charset.value = settings.charset;\n    updateCharset();\n    columnsField.value = `All visible (${columns.length})`;\n    leftOut.textContent = hidden.length\n      ? `Not included: the attachments, and the hidden ${hidden.length === 1 ? 'column' : 'columns'} ${list(hidden)}.`\n      : 'Not included: the attachments.';\n\n    edited = false;\n    fileField.error = null;\n    updateScope();\n    dialog.open = true;\n  });\n\n  scope.addEventListener('change', updateScope);\n\n  // A name of the user's own keeps its stem; the extension follows the format.\n  format.addEventListener('change', () => {\n    fileField.value = fileField.value.replace(/\\.(csv|xlsx)$/i, '') + EXTENSIONS[format.value];\n    updateCharset();\n  });\n\n  fileField.addEventListener('input', () => {\n    edited = true;\n  });\n\n  apply.addEventListener('click', async () => {\n    if (running || apply.disabled || !fileField.reportValidity()) return;\n    const own = session;\n    const n = counts[scope.value];\n    const target =\n      scope.value === 'view'\n        ? { scope: 'view', filters: grid.filters, searchTerm: grid.searchTerm }\n        : scope.value === 'selected'\n          ? { scope: 'selected', keys: [...grid.selectedKeys] }\n          : { scope: 'all' };\n    const request = { ...target, format: format.value, charset: isCsv() ? charset.value : null, columns, fileName: fileField.value.trim() };\n    // Taken before the wait: the fields stay open to change while the file is made, and what is\n    // remembered is what went out.\n    const settings = { format: format.value, charset: charset.value };\n\n    running = true;\n    apply.loading = true;\n    await exportDocuments(request);\n    saveExportSettings(settings);\n    running = false;\n    apply.loading = false;\n    if (own === session) dialog.open = false;\n    toasts.create({ variant: 'success', title: `${count(n)} exported`, description: request.fileName });\n  });\n</script>",
  "page": "https://test-apps.eurodata.de/a/eurodata-ui/docs/patterns/bulk-editing/be06-export-dialog/",
  "topicPage": "https://test-apps.eurodata.de/a/eurodata-ui/docs/patterns/bulk-editing/"
}
