refactor(approval-ux): eng-review fixes — extract helpers, add tests, register i18n
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { shallowDiffSummary, isEmptyDiffSummary } from './diff'
|
||||
|
||||
describe('shallowDiffSummary', () => {
|
||||
it('returns null when both inputs are null', () => {
|
||||
expect(shallowDiffSummary(null, null)).toBeNull()
|
||||
expect(shallowDiffSummary(undefined, undefined)).toBeNull()
|
||||
expect(shallowDiffSummary(null, undefined)).toBeNull()
|
||||
})
|
||||
|
||||
it('reports added keys when before is empty/null', () => {
|
||||
const s = shallowDiffSummary(null, { code: 'A', name: 'Alpha' })
|
||||
expect(s).toEqual({ added: ['code', 'name'], removed: [], changed: [] })
|
||||
})
|
||||
|
||||
it('reports removed keys when after is empty/null', () => {
|
||||
const s = shallowDiffSummary({ code: 'A', name: 'Alpha' }, null)
|
||||
expect(s).toEqual({ added: [], removed: ['code', 'name'], changed: [] })
|
||||
})
|
||||
|
||||
it('reports changed keys when values differ', () => {
|
||||
const s = shallowDiffSummary({ code: 'A', count: 1 }, { code: 'A', count: 2 })
|
||||
expect(s).toEqual({ added: [], removed: [], changed: ['count'] })
|
||||
})
|
||||
|
||||
it('handles mixed +/-/~ in one call', () => {
|
||||
const s = shallowDiffSummary(
|
||||
{ code: 'A', count: 1, oldField: 'x' },
|
||||
{ code: 'A', count: 2, newField: 'y' },
|
||||
)
|
||||
expect(s).toEqual({
|
||||
added: ['newField'],
|
||||
removed: ['oldField'],
|
||||
changed: ['count'],
|
||||
})
|
||||
})
|
||||
|
||||
it('treats nested-object change as one changed entry on the outer key', () => {
|
||||
const s = shallowDiffSummary(
|
||||
{ code: 'A', meta: { lat: 55, lon: 37 } },
|
||||
{ code: 'A', meta: { lat: 55, lon: 38 } },
|
||||
)
|
||||
expect(s).toEqual({ added: [], removed: [], changed: ['meta'] })
|
||||
})
|
||||
|
||||
it('reports empty buckets when payloads are deeply equal', () => {
|
||||
const s = shallowDiffSummary({ code: 'A', count: 1 }, { code: 'A', count: 1 })
|
||||
expect(s).toEqual({ added: [], removed: [], changed: [] })
|
||||
expect(s && isEmptyDiffSummary(s)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isEmptyDiffSummary', () => {
|
||||
it('returns true when all three buckets are empty', () => {
|
||||
expect(isEmptyDiffSummary({ added: [], removed: [], changed: [] })).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when any bucket has entries', () => {
|
||||
expect(isEmptyDiffSummary({ added: ['x'], removed: [], changed: [] })).toBe(false)
|
||||
expect(isEmptyDiffSummary({ added: [], removed: ['x'], changed: [] })).toBe(false)
|
||||
expect(isEmptyDiffSummary({ added: [], removed: [], changed: ['x'] })).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Shallow JSON-record diff utilities for UI summary chips.
|
||||
*
|
||||
* <p>Lives in `src/lib` (not in a route file) so that callers beyond the
|
||||
* record-draft ReviewDrawer can reuse it — RecordHistoryDrawer, audit row
|
||||
* summary, future audit-trail tooltips. Pre-emptive DRY: surfaced during
|
||||
* the 2026-05-14 eng review of MR !186.
|
||||
*
|
||||
* <p>Shallow on purpose: ordinis business records are flat-ish at the top
|
||||
* level (business fields, no deep object trees that would require a real
|
||||
* structural diff). Nested-object changes are reported as a single
|
||||
* "changed" entry on the outer key. Good enough for "what's the gist" UI
|
||||
* chips. For full schema-level diff with line-by-line highlighting, see
|
||||
* `ChangelogDiffModal` + its backend `summary.added/removed/changed`
|
||||
* payload.
|
||||
*
|
||||
* <p>Equality is checked via `JSON.stringify`. Order-sensitive — relies on
|
||||
* V8/JSC preserving insertion order for plain objects (true on every
|
||||
* modern runtime ordinis ships against). Sufficient for record data which
|
||||
* is always object-literal serialized; not safe for Map/Set/Date/RegExp,
|
||||
* none of which appear in record payloads.
|
||||
*/
|
||||
|
||||
export type ShallowDiffSummary = {
|
||||
added: string[]
|
||||
removed: string[]
|
||||
changed: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns null when both inputs are null/undefined (e.g. CLOSE op draft
|
||||
* with no live record). Caller can skip rendering entirely.
|
||||
* Otherwise: per-bucket key lists, never null.
|
||||
*/
|
||||
export function shallowDiffSummary(
|
||||
before: Record<string, unknown> | null | undefined,
|
||||
after: Record<string, unknown> | null | undefined,
|
||||
): ShallowDiffSummary | null {
|
||||
if (!before && !after) return null
|
||||
const b = before ?? {}
|
||||
const a = after ?? {}
|
||||
const added: string[] = []
|
||||
const removed: string[] = []
|
||||
const changed: string[] = []
|
||||
for (const k of Object.keys(a)) {
|
||||
if (!(k in b)) {
|
||||
added.push(k)
|
||||
} else if (JSON.stringify(a[k]) !== JSON.stringify(b[k])) {
|
||||
changed.push(k)
|
||||
}
|
||||
}
|
||||
for (const k of Object.keys(b)) {
|
||||
if (!(k in a)) removed.push(k)
|
||||
}
|
||||
return { added, removed, changed }
|
||||
}
|
||||
|
||||
/** Convenience: empty when all three buckets are empty. */
|
||||
export function isEmptyDiffSummary(s: ShallowDiffSummary): boolean {
|
||||
return s.added.length === 0 && s.removed.length === 0 && s.changed.length === 0
|
||||
}
|
||||
Reference in New Issue
Block a user