{
  "id": "ST02",
  "title": "Diff view",
  "topic": "status",
  "use": "Two versions of the same record side by side, field by field. What differs is marked, what is the same stays plain, and the toolbar names the two versions compared. Use it when the question is what has changed in one record between two points in time — often before taking a value back. Where different records are compared, the question is which one, and that is a comparison table; a third version breaks the side-by-side.",
  "decisions": [
    "Both versions stand side by side in one table, one row per field: each value stands next to the one it is compared with, and the two sides scroll together.",
    "The table is an ed-data-grid, not a plain table: Only differences filters its rows and Restore acts on them in place, and the grid's usage rules give both to the grid. The switch sets the grid's own filter, on a hidden column, so the header carries no second filter control beside it.",
    "Only what differs is marked, with a Changed badge in a column of its own rather than with color alone: the difference is shown, not searched for, and a mark on every row would mark nothing.",
    "The toolbar names the two versions compared, each in a select whose value carries its date: which two versions stand side by side is the first thing to know, and the user picks them. The earlier version stands first and the current one second, because the question is what has changed since.",
    "All fields is the default, and Only differences hides the unchanged fields while the count in the toolbar names how many are hidden: both versions stay visible in full, and the few changes can still be pulled out of many fields.",
    "Restore stands in the changed row and takes back one field, never the whole version: who compares two versions often wants one value back, not all of them.",
    "After a restore the row is no longer changed and the count follows; the focus moves on to the next Restore — the one before where it was the last — or to the count when none is left, because the button pressed goes with the change.",
    "Restore stands only while the second version is the current one: it writes the earlier value into the record as it is now, and a past version does not change.",
    "Where Only differences leaves no row, an empty state says the two versions agree and offers Show all fields: an empty table would read as missing data."
  ],
  "try": [
    {
      "do": "Look at the table",
      "then": "Weekly hours, Employment type, Cost center and Vacation entitlement carry Changed and Restore, the other six fields carry nothing, and the count above the table reads 4 differences · 6 fields unchanged"
    },
    {
      "do": "Switch to Only differences",
      "then": "the six unchanged fields drop out, and the count says they are hidden"
    },
    {
      "do": "Press Restore in the row Cost center",
      "then": "the row is no longer changed and leaves the list, the count reads 3 differences, and the focus moves on to Restore in the next changed row, Vacation entitlement"
    },
    {
      "do": "Restore the other three",
      "then": "the empty state says the two versions agree, and the focus is on the count"
    },
    {
      "do": "Press Show all fields",
      "then": "all ten fields are back and none is marked — Cost center reads 4200 Fleet South on both sides again — and the focus is on the count"
    },
    {
      "do": "Choose As of Mar 1, 2019 as the first version",
      "then": "the first column shows the version from the entry date, and Tax class and Child allowances are marked"
    },
    {
      "do": "Choose As of Aug 12, 2026 as the second version",
      "then": "the two rows stay marked, but Restore is gone — only the current version can be changed"
    }
  ],
  "revision": 1,
  "changes": [],
  "reviewed": false,
  "source": "<div class=\"ed-stack ed-gap-4\">\n  <!-- Demo only, not part of the pattern. The screen's title comes from the screen around the pattern; SC01 places it. -->\n\n  <ed-toolbar label=\"Versions of Timo Brandt\">\n    <ed-select id=\"st02-first\" slot=\"view\" aria-label=\"First version\" style=\"--ed-select-width: 14rem\"></ed-select>\n    <ed-icon slot=\"view\" name=\"exchange\"></ed-icon>\n    <ed-select id=\"st02-second\" slot=\"view\" aria-label=\"Second version\" style=\"--ed-select-width: 14rem\"></ed-select>\n    <ed-radio-group id=\"st02-scope\" slot=\"view\" appearance=\"button\" aria-label=\"Scope\" value=\"all\">\n      <ed-radio value=\"all\">All fields</ed-radio>\n      <ed-radio value=\"differences\">Only differences</ed-radio>\n    </ed-radio-group>\n\n    <span id=\"st02-count\" slot=\"meta\" role=\"status\" tabindex=\"-1\"></span>\n  </ed-toolbar>\n\n  <ed-data-grid id=\"st02-grid\" label=\"Comparison of the two versions\" row-key=\"key\" without-toolbar hide-column-menu>\n    <ed-empty-state slot=\"no-results\" size=\"small\" icon=\"done\">\n      <span slot=\"label\">No differences</span>\n      <span id=\"st02-agree\"></span>\n      <ed-button id=\"st02-show-all\" slot=\"button\" appearance=\"outlined\">Show all fields</ed-button>\n    </ed-empty-state>\n  </ed-data-grid>\n</div>\n\n<!-- Demo only, not part of the pattern. The product supplies fields, the record's fields in their order as { key, label }, and versions, its saved versions newest first as { id, label, current, values }: label names the version as the selects show it, current marks the record as it is now, and values holds each field's value as shown, by key. A restore writes into the values of the current version; a product saves it there. -->\n\n<script type=\"module\">\n  const first = document.getElementById('st02-first');\n  const second = document.getElementById('st02-second');\n  const scope = document.getElementById('st02-scope');\n  const count = document.getElementById('st02-count');\n  const grid = document.getElementById('st02-grid');\n  const agree = document.getElementById('st02-agree');\n  const showAll = document.getElementById('st02-show-all');\n\n  const quantity = (number, one, many) => `${number} ${number === 1 ? one : many}`;\n  const version = id => versions.find(entry => entry.id === id);\n\n  // The version before the current one stands first: the question is what has changed since.\n  let earlier = versions[1];\n  let later = versions[0];\n\n  // A cell is built once for what it shows, not on every render: the grid calls the formatter each\n  // time it renders, and a node built anew would take away the focus a restore hands to the next row.\n  const differenceCells = new Map();\n  const cellKey = row => [row.key, row.first, row.second, later.current].join('|');\n  let rows = [];\n\n  function differenceCell(value, row) {\n    if (!row.changed) return '';\n    const key = cellKey(row);\n    if (differenceCells.has(key)) return differenceCells.get(key);\n\n    const wrapper = document.createElement('span');\n    const badge = document.createElement('ed-badge');\n    badge.setAttribute('variant', 'warning');\n    badge.textContent = 'Changed';\n    wrapper.append(badge);\n\n    if (later.current) {\n      const button = document.createElement('ed-button');\n      const icon = document.createElement('ed-icon');\n      icon.slot = 'start';\n      icon.name = 'restore';\n      button.append(icon, 'Restore');\n      button.setAttribute('appearance', 'plain');\n      button.setAttribute('density', 'compact');\n      button.setAttribute('aria-label', `Restore ${row.label}`);\n      button.addEventListener('click', () => restore(row.key));\n      wrapper.append(' ', button);\n    }\n\n    differenceCells.set(key, wrapper);\n    return wrapper;\n  }\n\n  function compare() {\n    rows = fields.map(({ key, label }) => ({\n      key,\n      label,\n      first: earlier.values[key],\n      second: later.values[key],\n      changed: earlier.values[key] !== later.values[key]\n    }));\n  }\n\n  function show() {\n    const changed = rows.filter(row => row.changed);\n    const unchanged = rows.length - changed.length;\n    const narrowed = scope.value === 'differences';\n    grid.data = rows;\n    grid.filters = narrowed ? [{ id: 'changed', value: true }] : [];\n\n    const total = document.createElement('strong');\n    total.textContent = changed.length ? quantity(changed.length, 'difference', 'differences') : 'No differences';\n    count.replaceChildren(\n      total,\n      narrowed ? ` · ${quantity(unchanged, 'unchanged field', 'unchanged fields')} hidden` : ` · ${quantity(unchanged, 'field', 'fields')} unchanged`\n    );\n    agree.textContent = `The two versions agree in all ${quantity(rows.length, 'field', 'fields')}.`;\n  }\n\n  function compareVersions() {\n    compare();\n    grid.columns = [\n      { field: 'label', label: 'Field', rowheader: true, width: 220, sortable: false },\n      { field: 'first', label: earlier.label, flex: 1, minWidth: 160, sortable: false },\n      { field: 'second', label: later.label, flex: 1, minWidth: 160, sortable: false },\n      { id: 'difference', label: 'Difference', width: 220, sortable: false, formatter: differenceCell },\n      // Only differences is the grid's own filter, on a column that stays hidden: on a shown column\n      // the header would carry a filter button of its own beside the switch that sets it.\n      {\n        id: 'changed',\n        label: 'Changed',\n        value: row => row.changed,\n        hidden: true,\n        hideable: false,\n        sortable: false,\n        searchable: false,\n        filterable: true,\n        filterFn: (changed, wanted) => changed === wanted\n      }\n    ];\n    show();\n  }\n\n  // The focus goes on to the next changed row, back to the one before when this was the last, and to\n  // the count when none is left. It moves before the rows change: the button pressed goes with them,\n  // and the next row's button stays, as its cell is built once.\n  function restore(key) {\n    const changed = rows.filter(row => row.changed);\n    const index = changed.findIndex(row => row.key === key);\n    const next = changed[index + 1] ?? changed[index - 1];\n    const button = next && differenceCells.get(cellKey(next))?.querySelector('ed-button');\n    (button?.isConnected ? button : count).focus();\n    later.values[key] = earlier.values[key];\n    compare();\n    show();\n  }\n\n  // The empty state leaves with the rows its button brings back; the count takes the focus.\n  showAll.addEventListener('click', () => {\n    count.focus();\n    scope.value = 'all';\n    show();\n  });\n\n  scope.addEventListener('change', () => show());\n  first.addEventListener('change', () => {\n    earlier = version(first.value);\n    compareVersions();\n  });\n  second.addEventListener('change', () => {\n    later = version(second.value);\n    compareVersions();\n  });\n\n  await Promise.all(['ed-data-grid', 'ed-select', 'ed-option', 'ed-radio-group'].map(tag => customElements.whenDefined(tag)));\n  for (const select of [first, second]) {\n    select.append(\n      ...versions.map(({ id, label }) => {\n        const option = document.createElement('ed-option');\n        option.value = id;\n        option.textContent = label;\n        return option;\n      })\n    );\n  }\n  first.value = earlier.id;\n  second.value = later.id;\n  compareVersions();\n</script>",
  "page": "https://test-apps.eurodata.de/a/eurodata-ui/docs/patterns/status/st02-diff-view/",
  "topicPage": "https://test-apps.eurodata.de/a/eurodata-ui/docs/patterns/status/"
}
