@@ -18,11 +18,31 @@ const MAX_COL_SIZE = 500
1818// resize handle, filter icon). These sit alongside the header text and consume space.
1919const HEADER_CHROME_PX = 75
2020
21+ // Extra pixels per chip for icon + internal padding + margin.
22+ const CHIP_CHROME_PX = 45
23+
24+ // DateTime columns render as relative time (e.g. "about 2 months ago"). Use a fixed
25+ // character length instead of measuring the raw ISO date string.
26+ const RELATIVE_TIME_CHARS = 20
27+
28+ // Known datetime accessor names and pattern — must stay in sync with get-cipp-formatting.js
29+ const TIME_AGO_NAMES = new Set ( [
30+ 'ExecutedTime' , 'ScheduledTime' , 'Timestamp' , 'timestamp' , 'DateTime' , 'LastRun' ,
31+ 'LastRefresh' , 'createdDateTime' , 'activatedDateTime' , 'lastModifiedDateTime' ,
32+ 'endDateTime' , 'ReceivedTime' , 'Expires' , 'updatedAt' , 'createdAt' , 'Received' ,
33+ 'Date' , 'WhenCreated' , 'WhenChanged' , 'CreationTime' , 'renewalDate' ,
34+ 'commitmentTerm.renewalConfiguration.renewalDate' , 'purchaseDate' , 'NextOccurrence' ,
35+ 'LastOccurrence' , 'NotBefore' , 'NotAfter' , 'latestDataCollection' ,
36+ 'requestDate' , 'reviewedDate' , 'GeneratedAt' ,
37+ ] )
38+ const MATCH_DATE_TIME = / ( [ d D ] a t e [ t T ] i m e | [ E e ] x p i r a t i o n | [ T t ] i m e s t a m p | [ s S ] t a r t [ D d ] a t e ) /
39+ const isDateTimeColumn = ( key ) => TIME_AGO_NAMES . has ( key ) || MATCH_DATE_TIME . test ( key )
40+
2141// Measure the pixel width a column needs based on its header and sampled cell values.
2242// rawValues are the original data values (before formatting) — if they contain arrays or
2343// complex objects the column renders as a button/chip list, so we cap to header width.
2444// Returns { size, minSize } where minSize is always header-width + chrome safe space.
25- const measureColumnSize = ( header , valuesForColumn , rawValues ) => {
45+ const measureColumnSize = ( header , valuesForColumn , rawValues , accessorKey ) => {
2646 const headerLen = header ? header . length : 6
2747 const headerPx = Math . round ( headerLen * CHAR_WIDTH + CELL_PADDING + HEADER_CHROME_PX )
2848 const minSize = Math . max ( MIN_COL_SIZE , headerPx )
@@ -40,34 +60,64 @@ const measureColumnSize = (header, valuesForColumn, rawValues) => {
4060 // "X items" button (CippDataTableButton), so size to the button width.
4161 const allObjectLike = rawValues . every ( ( v ) => {
4262 if ( v === null || v === undefined ) return true // nulls are fine, they show "No items"
43- if ( Array . isArray ( v ) ) return v . some ( ( el ) => typeof el === 'object' && el !== null )
63+ if ( Array . isArray ( v ) ) return v . length === 0 || v . some ( ( el ) => typeof el === 'object' && el !== null )
4464 return typeof v === 'object'
4565 } )
4666 if ( allObjectLike ) {
47- // "X items" button is roughly 80-100px wide — just use header width
48- return { size : minSize , minSize }
67+ // The formatted text tells us how this column actually renders:
68+ // - JSON strings (starts with [ or {) → CippDataTableButton ("X items"), compact
69+ // - Comma-separated text → chips/inline content, needs real measurement
70+ const looksLikeButton = valuesForColumn . every ( ( t ) => {
71+ if ( t === null || t === undefined || t === '' || t === 'No data' ) return true
72+ if ( Array . isArray ( t ) ) return true // handler returned a raw array (e.g. [])
73+ const s = typeof t === 'string' ? t . trim ( ) : ''
74+ return s . startsWith ( '[' ) || s . startsWith ( '{' ) || s === 'Password hidden'
75+ } )
76+ if ( looksLikeButton ) {
77+ return { size : minSize , minSize }
78+ }
79+ // Object arrays that render as chips — measure the longest item from the
80+ // comma-separated text representation.
81+ let longestObjItem = headerLen
82+ for ( const t of valuesForColumn ) {
83+ if ( typeof t !== 'string' ) continue
84+ const parts = t . split ( ',' )
85+ for ( const p of parts ) {
86+ const len = p . trim ( ) . length
87+ if ( len > longestObjItem ) longestObjItem = len
88+ }
89+ }
90+ const objChipPx = Math . round ( longestObjItem * CHAR_WIDTH + CELL_PADDING + CHIP_CHROME_PX + HEADER_CHROME_PX )
91+ const objSize = Math . max ( minSize , Math . min ( MAX_COL_SIZE , objChipPx ) )
92+ return { size : objSize , minSize }
4993 }
5094
51- // String/primitive arrays → rendered as chip list. Measure longest chip,
52- // but cap per-chip text since chips truncate long values (e.g. email addresses).
53- const MAX_CHIP_TEXT = 15
95+ // String/primitive arrays → rendered as chip list. Measure the longest
96+ // single item across all rows, then size like a regular text column.
5497 let longestItem = headerLen
5598 for ( let i = 0 ; i < rawValues . length ; i ++ ) {
5699 const v = rawValues [ i ]
57100 if ( Array . isArray ( v ) ) {
58101 for ( const el of v ) {
59102 const s = typeof el === 'string' ? el : el != null ? String ( el ) : ''
60- const len = Math . min ( s . length , MAX_CHIP_TEXT )
61- if ( len > longestItem ) longestItem = len
103+ if ( s . length > longestItem ) longestItem = s . length
62104 }
63105 }
64106 }
65- const chipPx = Math . round ( longestItem * CHAR_WIDTH + CELL_PADDING )
107+ const chipPx = Math . round ( longestItem * CHAR_WIDTH + CELL_PADDING + CHIP_CHROME_PX + HEADER_CHROME_PX )
66108 const size = Math . max ( minSize , Math . min ( MAX_COL_SIZE , chipPx ) )
67109 return { size, minSize }
68110 }
69111 }
70112
113+ // DateTime columns render as relative time — use a fixed width instead of the raw string.
114+ if ( accessorKey && isDateTimeColumn ( accessorKey ) ) {
115+ const dtLen = Math . max ( headerLen , RELATIVE_TIME_CHARS )
116+ const dtPx = Math . round ( dtLen * CHAR_WIDTH + CELL_PADDING )
117+ const size = Math . max ( minSize , Math . min ( MAX_COL_SIZE , dtPx ) )
118+ return { size, minSize }
119+ }
120+
71121 const sample =
72122 valuesForColumn . length > MAX_SIZE_SAMPLE
73123 ? valuesForColumn . slice ( 0 , MAX_SIZE_SAMPLE )
@@ -214,7 +264,7 @@ export const utilColumnsFromAPI = (dataArray) => {
214264 // Measure content width from formatted text values for this column.
215265 const textValues = valuesForColumn . map ( ( v ) => getCippFormatting ( v , accessorKey , 'text' ) )
216266 const header = getCippTranslation ( accessorKey )
217- const measuredSize = measureColumnSize ( header , textValues , valuesForColumn )
267+ const measuredSize = measureColumnSize ( header , textValues , valuesForColumn , accessorKey )
218268
219269 // Allow per-column size overrides for columns whose rendered output
220270 // doesn't match text width (icons, progress bars, etc.).
0 commit comments