@@ -141,18 +141,33 @@ const NO_VIEWS: TableViewWire[] = []
141141
142142type ViewModalState = { mode : 'create' } | { mode : 'rename' ; viewId : string } | null
143143
144+ /**
145+ * Order-insensitive JSON, used to compare a locally-built config against one that
146+ * has round-tripped through Postgres. `jsonb` does not preserve object key order
147+ * (`{status,plan}` comes back `{plan,status}`), so a plain `JSON.stringify` would
148+ * report any multi-key filter as permanently dirty. Array order is preserved —
149+ * it is meaningful for `columnOrder`.
150+ */
151+ function stableStringify ( value : unknown ) : string {
152+ if ( value === null || typeof value !== 'object' ) return JSON . stringify ( value ) ?? 'null'
153+ if ( Array . isArray ( value ) ) return `[${ value . map ( stableStringify ) . join ( ',' ) } ]`
154+ const entries = Object . entries ( value as Record < string , unknown > )
155+ . filter ( ( [ , entry ] ) => entry !== undefined )
156+ . sort ( ( [ left ] , [ right ] ) => ( left < right ? - 1 : left > right ? 1 : 0 ) )
157+ return `{${ entries . map ( ( [ key , entry ] ) => `${ JSON . stringify ( key ) } :${ stableStringify ( entry ) } ` ) . join ( ',' ) } }`
158+ }
159+
144160/**
145161 * Structural equality for the parts of a view config the user edits directly.
146162 * Column layout (widths/order/pinning) is excluded — it auto-saves into the
147163 * active view as the user drags, so it can never be the thing that is "unsaved".
148164 *
149- * Compares JSON rather than field-by-field because `filter` is an arbitrarily
150- * nested predicate tree; key order is stable since both sides are built by the
151- * same converters.
165+ * Compares serialized form rather than field-by-field because `filter` is an
166+ * arbitrarily nested predicate tree.
152167 */
153168function isSameViewConfig ( a : TableViewConfig , b : TableViewConfig ) : boolean {
154169 const normalize = ( config : TableViewConfig ) =>
155- JSON . stringify ( {
170+ stableStringify ( {
156171 filter : config . filter ?? null ,
157172 sort : config . sort ?? null ,
158173 hiddenColumns : [ ...( config . hiddenColumns ?? [ ] ) ] . sort ( ) ,
0 commit comments