{
  "id": "BE03",
  "title": "Serial edit in a dialog",
  "topic": "bulk-editing",
  "use": "Edit a selection one record after the other, each with a value of its own. This is not bulk editing but serial single editing — the posting text of twelve documents is twelve different texts. Setting one field for all of them is BE02.",
  "decisions": [
    "Filtering assembles the set, this pattern works it off: without it each record means opening and closing the edit area and finding the place in the list again. The run turns twelve round trips into one — in once, on twelve times, out once.",
    "A dialog, not a surface beside the list: the run is a step of its own, and the dialog shows what the user needs — which record, which one of how many, and how far along.",
    "Progress stands at the top as a determinate bar with a count, because the extent is known and someone halfway through must see how much is left.",
    "The footer has exactly one accent button, first; the other buttons state their emphasis, and a named way out stands among them. Labels say what happens — Save document and continue, Skip document, Stop editing — and on the last record the accent button says Save document and close.",
    "The bare arrow keys stay with the fields, where they change an option or a radio value. Alt + Arrow Down saves and goes to the next record; Alt + Arrow Up goes back without saving.",
    "A record that cannot be changed says so before the user types: its fields are read-only, the notification names the reason, and the accent button reads Skip document.",
    "Stopping asks only when the open record holds an unsaved change: every record saved so far stays saved, so there is nothing else to lose."
  ],
  "try": [
    {
      "do": "Choose Edit documents",
      "then": "twelve documents are already selected, the dialog shows the first one, and the progress at the top reads 0 of 12 edited"
    },
    {
      "do": "Change a posting text and choose Save document and continue",
      "then": "the value stands in the row, the next document is in the dialog, and the bar moves on"
    },
    {
      "do": "Press Alt and Arrow Down",
      "then": "the same path by keyboard; the arrow keys alone stay with the fields"
    },
    {
      "do": "Choose the arrow for the previous document",
      "then": "it points up, and the previous document is back"
    },
    {
      "do": "Select a finalized document as well and start the run",
      "then": "the starting selection holds none, so the first run goes smoothly; one added turns up with read-only fields, the reason in the notification, and Skip document on the accent button"
    },
    {
      "do": "Look at the footer on the last document",
      "then": "it reads Save document and close — no continue where nothing follows"
    },
    {
      "do": "Stop in the middle of the run",
      "then": "a warning asks only when the open document holds an unsaved change; the documents saved before stay saved, and a toast says how many are changed and how many unchanged"
    },
    {
      "do": "Search for “Depreciation” and choose Edit documents",
      "then": "the run still covers all thirteen selected documents, though the list shows none of them"
    }
  ],
  "revision": 2,
  "changes": [
    {
      "revision": 2,
      "change": "Why Edit documents and Clear selection are disabled is an ed-tooltip on each, outside the toolbar, not the buttons' title; it shows only while the button is disabled."
    }
  ],
  "reviewed": true,
  "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=\"be03-grid\" label=\"Incoming documents\" with-search selectable selection-mode=\"multiple\" data-ed-preload=\"ed-format-date\">\n  <ed-button slot=\"toolbar-actions\" id=\"be03-edit\">\n    <ed-icon slot=\"start\" name=\"edit\"></ed-icon>\n    Edit documents\n  </ed-button>\n  <ed-button slot=\"toolbar-actions\" id=\"be03-clear\" appearance=\"plain\">Clear selection</ed-button>\n</ed-data-grid>\n<ed-tooltip id=\"be03-edit-reason\" for=\"be03-edit\" disabled>Select at least one document in the list, and the button becomes available.</ed-tooltip>\n<ed-tooltip id=\"be03-clear-reason\" for=\"be03-clear\" disabled>Nothing is selected — there is nothing to clear.</ed-tooltip>\n\n<ed-dialog id=\"be03-dialog\" light-dismiss style=\"--ed-dialog-width: 44rem\">\n  <span slot=\"label\" class=\"ed-cluster ed-gap-3 ed-align-items-center\">\n    <span id=\"be03-title\"></span>\n    <ed-badge id=\"be03-status\"></ed-badge>\n  </span>\n  <ed-button slot=\"header-actions\" id=\"be03-previous\" appearance=\"plain\" density=\"compact\" aria-label=\"Show the previous document\">\n    <ed-icon name=\"up\"></ed-icon>\n  </ed-button>\n  <ed-button slot=\"header-actions\" id=\"be03-next\" appearance=\"plain\" density=\"compact\" aria-label=\"Show the next document\">\n    <ed-icon name=\"down\"></ed-icon>\n  </ed-button>\n  <ed-tooltip id=\"be03-previous-tip\" for=\"be03-previous\"></ed-tooltip>\n  <ed-tooltip id=\"be03-next-tip\" for=\"be03-next\"></ed-tooltip>\n\n  <div class=\"ed-stack ed-gap-5\">\n    <ed-progress-bar id=\"be03-progress\"></ed-progress-bar>\n    <ed-notification id=\"be03-locked\" variant=\"warning\" hidden>\n      This document is finalized\n      <span slot=\"description\">It cannot be changed. Skip document moves on to the next one.</span>\n    </ed-notification>\n    <ed-input name=\"text\" label=\"Posting text\" autofocus></ed-input>\n    <div class=\"ed-grid ed-gap-4\" style=\"--ed-grid-min-column-size: 18rem\">\n      <ed-select name=\"type\" label=\"Document type\">\n        <ed-option value=\"outgoing\">Outgoing invoice</ed-option>\n        <ed-option value=\"incoming\">Incoming invoice</ed-option>\n        <ed-option value=\"cash\">Cash receipt</ed-option>\n        <ed-option value=\"reclassification\">Reclassification</ed-option>\n        <ed-option value=\"bank\">Bank document</ed-option>\n      </ed-select>\n      <ed-select name=\"clerk\" label=\"Clerk\">\n        <ed-option value=\"SB\">Sophie Brandt</ed-option>\n        <ed-option value=\"TR\">Thorsten Reinke</ed-option>\n        <ed-option value=\"AJ\">Andrea Jansen</ed-option>\n        <ed-option value=\"MH\">Marie-Luise Hoffmann-Grote</ed-option>\n      </ed-select>\n      <ed-input name=\"account\" label=\"Account\" inputmode=\"numeric\" maxlength=\"4\"></ed-input>\n      <ed-input name=\"costCenter\" label=\"Cost center\" inputmode=\"numeric\" maxlength=\"4\"></ed-input>\n    </div>\n  </div>\n\n  <ed-button slot=\"footer\" id=\"be03-save\">Save document and continue</ed-button>\n  <ed-button slot=\"footer\" id=\"be03-skip\" appearance=\"outlined\">Skip document</ed-button>\n  <ed-button slot=\"footer\" id=\"be03-stop\" appearance=\"plain\">Stop editing</ed-button>\n</ed-dialog>\n\n<ed-alert-dialog id=\"be03-discard\" light-dismiss type=\"warning\" label=\"Discard your changes?\" confirm-text=\"Discard changes\" cancel-text=\"Keep editing\">\n  <span id=\"be03-discard-text\"></span>\n</ed-alert-dialog>\n\n<ed-toast-stack id=\"be03-toasts\"></ed-toast-stack>\n\n<!-- Demo only, not part of the pattern. The product supplies documents, the batch as { id, date, text, type, account, amount, costCenter, clerk, status } with the date as YYYY-MM-DD and type, clerk and status as codes; clerks, the names behind the clerk codes; and selectedAtStart, the ids selected when the list opens — none, unless the product carries a selection over from where the user came. -->\n\n<script type=\"module\">\n  const types = { outgoing: 'Outgoing invoice', incoming: 'Incoming invoice', cash: 'Cash receipt', reclassification: 'Reclassification', bank: 'Bank document' };\n  const statuses = { open: ['Open', 'neutral'], checked: ['Checked', 'info'], finalized: ['Finalized', 'success'], error: ['Error', 'danger'] };\n  const currency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' });\n  const count = (n, noun) => `${n} ${noun}${n === 1 ? '' : 's'}`;\n  const day = value =>\n    Object.assign(document.createElement('ed-format-date'), { date: value, month: 'short', day: 'numeric', year: 'numeric' });\n  const badge = status =>\n    Object.assign(document.createElement('ed-badge'), { variant: statuses[status][1], textContent: statuses[status][0] });\n\n  const grid = document.getElementById('be03-grid');\n  const edit = document.getElementById('be03-edit');\n  const clear = document.getElementById('be03-clear');\n  const editReason = document.getElementById('be03-edit-reason');\n  const clearReason = document.getElementById('be03-clear-reason');\n  const dialog = document.getElementById('be03-dialog');\n  const title = document.getElementById('be03-title');\n  const status = document.getElementById('be03-status');\n  const progress = document.getElementById('be03-progress');\n  const locked = document.getElementById('be03-locked');\n  const previous = document.getElementById('be03-previous');\n  const next = document.getElementById('be03-next');\n  const previousTip = document.getElementById('be03-previous-tip');\n  const nextTip = document.getElementById('be03-next-tip');\n  const save = document.getElementById('be03-save');\n  const skip = document.getElementById('be03-skip');\n  const stop = document.getElementById('be03-stop');\n  const discard = document.getElementById('be03-discard');\n  const discardText = document.getElementById('be03-discard-text');\n  const toasts = document.getElementById('be03-toasts');\n  const fields = [...dialog.querySelectorAll('ed-input[name], ed-select[name]')];\n\n  grid.columns = [\n    { field: 'id', label: 'Document', rowheader: true, pinned: 'left', width: 150 },\n    { field: 'date', label: 'Date', width: 120, sortFn: 'datetime', formatter: day },\n    { field: 'text', label: 'Posting text', flex: 2, minWidth: 192 },\n    { id: 'type', label: 'Document type', width: 176, filterable: true, filterType: 'set', value: doc => types[doc.type] ?? '' },\n    { field: 'account', label: 'Account', width: 90, align: 'end' },\n    { field: 'amount', label: 'Amount', width: 144, align: 'end', formatter: value => currency.format(value) },\n    { field: 'costCenter', label: 'Cost center', width: 128, filterable: true, filterType: 'set' },\n    { id: 'clerk', label: 'Clerk', width: 220, filterable: true, filterType: 'set', value: doc => clerks[doc.clerk] ?? '' },\n    {\n      field: 'status',\n      label: 'Status',\n      width: 140,\n      pinned: 'right',\n      filterable: true,\n      filterType: 'set',\n      filterOptions: Object.entries(statuses).map(([value, [label]]) => ({ value, label })),\n      formatter: badge\n    }\n  ];\n  grid.data = documents;\n\n  let selected = selectedAtStart;\n  grid.selectedKeys = selected;\n\n  let queue = [];\n  let position = 0;\n  let leaving = false;\n  const saved = new Set();\n  const skipped = new Set();\n\n  function updateToolbar() {\n    edit.disabled = selected.length === 0;\n    editReason.disabled = !edit.disabled;\n    clear.disabled = selected.length === 0;\n    clearReason.disabled = !clear.disabled;\n  }\n\n  function show() {\n    const doc = queue[position];\n    const isLocked = doc.status === 'finalized';\n    const isFirst = position === 0;\n    const isLast = position === queue.length - 1;\n\n    title.textContent = `${doc.id} · ${position + 1} of ${queue.length}`;\n    status.textContent = statuses[doc.status][0];\n    status.variant = statuses[doc.status][1];\n    const done = saved.size + skipped.size;\n    progress.label = `${done} of ${queue.length} edited`;\n    progress.value = (done / queue.length) * 100;\n    locked.hidden = !isLocked;\n    locked.open = isLocked;\n    for (const field of fields) {\n      field.value = doc[field.name];\n      field.readonly = isLocked;\n    }\n\n    const skipLabel = isLast ? 'Skip document and close' : 'Skip document';\n    save.textContent = isLocked ? skipLabel : isLast ? 'Save document and close' : 'Save document and continue';\n    skip.textContent = skipLabel;\n    // A hidden or disabled button drops its focus to the page, so it moves on first: from Skip to the\n    // accent button, which skips a finalized document, and from one arrow to the other.\n    if (isLocked && document.activeElement === skip) save.focus();\n    skip.hidden = isLocked;\n\n    if (isFirst && document.activeElement === previous && !isLast) next.focus();\n    if (isLast && document.activeElement === next && !isFirst) previous.focus();\n    previous.disabled = isFirst;\n    next.disabled = isLast;\n    previousTip.textContent = isFirst\n      ? 'This is the first document of the selection.'\n      : 'Show the previous document without saving (Alt + Arrow Up).';\n    // Alt + Arrow Down saves and continues, as the accent button does, so this arrow names no shortcut.\n    nextTip.textContent = isLast ? 'This is the last document of the selection.' : 'Show the next document without saving.';\n  }\n\n  function advance() {\n    if (position < queue.length - 1) {\n      position += 1;\n      show();\n      return;\n    }\n    leaving = true;\n    dialog.open = false;\n    toasts.create({\n      variant: skipped.size ? 'warning' : 'success',\n      title: `${count(saved.size, 'document')} changed${skipped.size ? `, ${skipped.size} skipped` : ''}`,\n      description: skipped.size ? 'Skipped are the documents that cannot be changed and the ones you passed over.' : undefined,\n      duration: 6000\n    });\n  }\n\n  function saveAndContinue() {\n    const doc = queue[position];\n    if (doc.status === 'finalized') {\n      skipped.add(doc.id);\n    } else {\n      for (const field of fields) doc[field.name] = field.value;\n      saved.add(doc.id);\n      skipped.delete(doc.id);\n      grid.data = documents;\n    }\n    advance();\n  }\n\n  const isChanged = () => {\n    const doc = queue[position];\n    return doc.status !== 'finalized' && fields.some(field => String(field.value ?? '') !== String(doc[field.name] ?? ''));\n  };\n\n  // The one question over the dialog: asked only where a change in the open document would be lost.\n  let pending = null;\n  function unlessLost(action) {\n    if (!isChanged()) return action();\n    pending = action;\n    discardText.textContent = `The change to ${queue[position].id} is not saved yet.`;\n    discard.open = true;\n  }\n\n  // The arrows browse without saving.\n  function browse(step) {\n    unlessLost(() => {\n      position += step;\n      show();\n    });\n  }\n\n  grid.addEventListener('ed-data-grid-row-select', event => {\n    selected = event.detail.selectedKeys;\n    updateToolbar();\n  });\n\n  clear.addEventListener('click', () => {\n    selected = [];\n    grid.selectedKeys = [];\n    updateToolbar();\n  });\n\n  edit.addEventListener('click', () => {\n    // The run works off the whole selection: the selected rows in the list's order, then those the\n    // search or a filter hides right now, which stay selected.\n    const keys = new Set(selected);\n    const inView = grid.getProcessedRows().filter(doc => keys.has(doc.id));\n    const shown = new Set(inView.map(doc => doc.id));\n    queue = [...inView, ...documents.filter(doc => keys.has(doc.id) && !shown.has(doc.id))];\n    position = 0;\n    leaving = false;\n    saved.clear();\n    skipped.clear();\n    show();\n    dialog.open = true;\n  });\n\n  save.addEventListener('click', saveAndContinue);\n\n  skip.addEventListener('click', () => {\n    if (!saved.has(queue[position].id)) skipped.add(queue[position].id);\n    advance();\n  });\n\n  previous.addEventListener('click', () => browse(-1));\n  next.addEventListener('click', () => browse(1));\n\n  // Alt with an arrow is the shortcut; the bare arrow keys stay with the fields. A held key saves once.\n  dialog.addEventListener('keydown', event => {\n    if (!event.altKey || event.repeat || event.defaultPrevented) return;\n    if (event.key === 'ArrowDown') {\n      event.preventDefault();\n      saveAndContinue();\n    } else if (event.key === 'ArrowUp' && position > 0) {\n      event.preventDefault();\n      browse(-1);\n    }\n  });\n\n  stop.addEventListener('click', () => {\n    dialog.open = false;\n  });\n\n  // Every saved document stays saved, so stopping asks only when the open document holds a change that\n  // would be lost. The popups inside the dialog fire ed-hide and ed-after-hide as well.\n  dialog.addEventListener('ed-hide', event => {\n    if (event.target !== dialog || leaving || !isChanged()) return;\n    event.preventDefault();\n    unlessLost(() => {\n      show();\n      dialog.open = false;\n    });\n  });\n\n  discard.addEventListener('ed-alert-dialog-confirm', () => {\n    const action = pending;\n    pending = null;\n    action?.();\n  });\n\n  dialog.addEventListener('ed-after-hide', event => {\n    if (event.target !== dialog || leaving || (saved.size === 0 && skipped.size === 0)) return;\n    toasts.create({\n      variant: 'warning',\n      title: 'Editing stopped',\n      description: `${saved.size} changed, ${queue.length - saved.size} unchanged.`,\n      duration: 8000\n    });\n  });\n\n  updateToolbar();\n</script>",
  "page": "https://test-apps.eurodata.de/a/eurodata-ui/docs/patterns/bulk-editing/be03-serial-edit-in-a-dialog/",
  "topicPage": "https://test-apps.eurodata.de/a/eurodata-ui/docs/patterns/bulk-editing/"
}
