@@ -5,41 +5,53 @@ import { action } from '@ember/object';
55import { debug } from '@ember/debug' ;
66import { isArray } from '@ember/array' ;
77import { capitalize } from '@ember/string' ;
8- import { htmlSafe } from '@ember/template' ;
98import { task } from 'ember-concurrency' ;
109import { parseISO , isDate , isValid , format , formatDistanceToNow } from 'date-fns' ;
1110import smartHumanize from '../utils/smart-humanize' ;
1211
1312export default class ActivityLogComponent extends Component {
1413 @service store ;
1514 @tracked activities = [ ] ;
16- @tracked expanded = new Set ( ) ;
1715 @tracked dateFilter = null ;
18- @tracked query = null ;
1916
20- // Optional style “knobs” (you can pass in from the parent)
2117 get density ( ) {
2218 return this . args . density ?? 'compact' ;
23- } // 'cozy'|'compact'
19+ }
20+
2421 get showAvatars ( ) {
2522 return this . args . showAvatars ?? true ;
2623 }
24+
2725 get showBadges ( ) {
28- return this . args . showBadges ?? true ;
26+ return this . args . showBadges ?? false ;
2927 }
3028
31- get groups ( ) {
32- const activities = isArray ( this . activities ) ? this . activities : [ ] ;
29+ get showHeader ( ) {
30+ return this . args . showHeader ?? true ;
31+ }
3332
34- const normalized = activities . map ( ( a , i ) => this . #normalizeActivity( a , i ) ) . sort ( ( a , b ) => ( b . timestamp ?. dateMs ?? 0 ) - ( a . timestamp ?. dateMs ?? 0 ) ) ;
33+ get showControls ( ) {
34+ return this . args . showControls ?? true ;
35+ }
3536
36- const byDay = new Map ( ) ;
37- for ( const item of normalized ) {
38- const key = item . dayKey ?? 'unknown' ;
39- if ( ! byDay . has ( key ) ) byDay . set ( key , { dateLabel : item . dayLabel ?? key , items : [ ] } ) ;
40- byDay . get ( key ) . items . push ( item ) ;
37+ get showAttributePreviousValues ( ) {
38+ return this . args . showAttributePreviousValues ?? true ;
39+ }
40+
41+ get items ( ) {
42+ const activities = isArray ( this . activities ) ? this . activities : [ ] ;
43+ const normalized = activities . map ( ( activity , index ) => this . #normalizeActivity( activity , index ) ) . sort ( ( a , b ) => ( b . timestamp ?. dateMs ?? 0 ) - ( a . timestamp ?. dateMs ?? 0 ) ) ;
44+ const limit = Number ( this . args . maxVisibleActivities ) ;
45+
46+ if ( Number . isFinite ( limit ) && limit > 0 ) {
47+ return normalized . slice ( 0 , limit ) ;
4148 }
42- return [ ...byDay . values ( ) ] ;
49+
50+ return normalized ;
51+ }
52+
53+ get hasItems ( ) {
54+ return this . items . length > 0 ;
4355 }
4456
4557 constructor ( ) {
@@ -51,6 +63,7 @@ export default class ActivityLogComponent extends Component {
5163 @task * loadActivities ( ) {
5264 try {
5365 const params = { } ;
66+ if ( this . args . companyUuid ) params . company_uuid = this . args . companyUuid ;
5467 if ( this . args . subjectId ) params . subject_id = this . args . subjectId ;
5568 if ( this . args . causerId ) params . causer_id = this . args . causerId ;
5669 if ( this . dateFilter ) params . created_at = this . dateFilter ;
@@ -79,12 +92,6 @@ export default class ActivityLogComponent extends Component {
7992 if ( typeof this . args . onSubjectClick === 'function' ) this . args . onSubjectClick ( subject ) ;
8093 }
8194
82- @action toggleAdvanced ( itemKey ) {
83- const next = new Set ( this . expanded ) ;
84- next . has ( itemKey ) ? next . delete ( itemKey ) : next . add ( itemKey ) ;
85- this . expanded = next ;
86- }
87-
8895 // ── Normalize & Phrase ──────────────────────────────────────────────────────
8996 #normalizeActivity( activity , idx = 0 ) {
9097 const createdISO = activity ?. created_at ?? null ;
@@ -94,105 +101,81 @@ export default class ActivityLogComponent extends Component {
94101 const d = this . #parseDate( tsISO ) ;
95102 const dateMs = d ? d . getTime ( ) : 0 ;
96103 const dayKey = d ? format ( d , 'yyyy-MM-dd' ) : 'unknown' ;
97- const dayLabel = d ? format ( d , 'EEE, MMM dd, yyyy' ) : 'Unknown date' ;
98104 const exactLocal = d ? format ( d , 'PP p' ) : '' ;
99105 const relative = d ? formatDistanceToNow ( d , { addSuffix : true } ) : '' ;
100106
101107 const causer = activity ?. causer ?? { } ;
102108 const subject = activity ?. subject ?? { } ;
103109 const subjectTypeLabel = activity ?. humanized_subject_type ?? this . #subjectTypeLabel( activity ?. subject_type ) ;
104- const subjectDisplay = this . #subjectDisplay( subject ) ;
105110 const event = String ( activity ?. event || '' ) . toLowerCase ( ) ;
111+ const changes = this . #computeChanges( activity ?. properties ) ;
112+ const changeCount = changes . length ;
113+ const shouldShowSubjectContext = this . #shouldShowSubjectContext( ) ;
114+ const verb = this . #eventToVerb( event , activity ?. description , changeCount , shouldShowSubjectContext ) ;
115+ const hasMultipleChanges = changeCount > 1 ;
116+ const inlineChange = changeCount === 1 ? this . #inlineChangeSummary( changes [ 0 ] ) : null ;
117+ const objectLabel = this . #objectLabel( activity , subjectTypeLabel ) ;
118+ const targetPhrase = shouldShowSubjectContext ? this . #targetPhrase( activity , subjectTypeLabel , event ) : null ;
119+ const actorName = causer ?. name ?? 'Someone' ;
106120
107- const eventLabel = capitalize ( event || 'updated' ) ;
108- const verb = this . #eventToVerb( event , activity ?. description ) ;
109-
110- // Diffs → split into simple vs advanced, and also build a human inline sentence
111- const allChanges = this . #computeChanges( activity ?. properties ) ;
112- const simpleChanges = [ ] ;
113- const advancedChanges = [ ] ;
114-
115- for ( const c of allChanges ) {
116- if ( this . #isAdvancedValue( c . fromRaw , c . toRaw ) || this . #isLikelyUuidKey( c . key ) ) {
117- advancedChanges . push ( c ) ;
118- } else {
119- simpleChanges . push ( c ) ;
120- }
121- }
122-
123- const inlineSummary = this . #summarizeSimple( simpleChanges ) ;
124-
125- const sentence = `${ causer ?. name ?? 'Someone' } ${ verb } ${ subjectTypeLabel } (${ subjectDisplay } )` ;
126-
127- // Event → badge style (Fleetbase-ish accent mapping)
128121 const badge = this . #eventBadge( event ) ;
129122
130123 return {
131124 key : `${ dayKey } -${ idx } ` ,
132125 actor : {
133126 name : causer ?. name ?? 'Unknown' ,
134127 avatarUrl : causer ?. avatar_url ?? null ,
128+ initial : this . #initial( actorName ) ,
135129 raw : causer ,
136130 } ,
137131 subject,
138132 causer,
139133 verb,
140134 event,
141- eventLabel,
142- badge, // {text, class}
135+ eventLabel : capitalize ( event || 'updated' ) ,
136+ badge,
143137 subjectTypeLabel,
144- subjectDisplay,
145- sentence,
146- inlineSummary, // "set color to red; status to live"
138+ objectLabel,
139+ targetPhrase,
140+ changes,
141+ changeCount,
142+ hasChanges : changeCount > 0 ,
143+ hasMultipleChanges,
144+ inlineChange,
147145 timestamp : {
148146 iso : tsISO ,
149147 exactLocal,
150148 relative,
151149 dateMs,
152150 } ,
153151 dayKey,
154- dayLabel,
155- simpleChanges,
156- advancedChanges,
157152 raw : activity ,
158153 } ;
159154 }
160155
161- #summarizeSimple( simpleChanges ) {
162- if ( ! simpleChanges ?. length ) return '' ;
163-
164- const parts = [ ] ;
165- for ( const c of simpleChanges ) {
166- const k = c . key . replace ( / _ / g, ' ' ) ;
167-
168- if ( c . from !== 'null' && c . from !== undefined && c . from !== '' && c . from !== c . to ) {
169- parts . push (
170- `<span class="activity-change">changed <span class="activity-change-prop highlight-gray ${
171- this . args . activityChangePropClass ?? ''
172- } ">${ k } </span> from <span class="activity-change-prop highlight-gray ${ this . args . activityPreviousValueClass ?? '' } ">${ this . #code(
173- c . from
174- ) } </span> to <span class="activity-change-prop highlight-blue ${ this . args . activityNewValueClass ?? '' } ">${ this . #code( c . to ) } </span></span>`
175- ) ;
176- } else {
177- parts . push (
178- `<span class="activity-change">set <span class="activity-change-prop highlight-gray ${
179- this . args . activityChangePropClass ?? ''
180- } ">${ k } </span> to <span class="activity-change-prop highlight-blue ${ this . args . activityNewValueClass ?? '' } ">${ this . #code( c . to ) } </span></span>`
181- ) ;
182- }
156+ #inlineChangeSummary( change ) {
157+ if ( ! change ) return null ;
158+ if ( this . #isAdvancedValue( change . fromRaw , change . toRaw ) || this . #isLikelyUuidKey( change . key ) ) return null ;
159+ if ( change . from !== 'null' && change . from !== undefined && change . from !== '' && change . from !== change . to ) {
160+ return {
161+ attribute : change . label ,
162+ from : change . from ,
163+ to : change . to ,
164+ hasPreviousValue : true ,
165+ } ;
183166 }
184167
185- // Make the entire thing safe once at the end
186- return htmlSafe ( parts . join ( ', ' ) ) ;
187- }
188-
189- #code( v ) {
190- // lightweight backtick wrapper for inline emphasis
191- return this . args . backtickValues ? `\`${ String ( v ) } \`` : v ;
168+ return {
169+ attribute : change . label ,
170+ to : change . to ,
171+ hasPreviousValue : false ,
172+ } ;
192173 }
193174
194- #eventToVerb( event , description ) {
175+ #eventToVerb( event , description , changeCount = 0 , showSubjectContext = false ) {
195176 if ( description && typeof description === 'string' ) return description ;
177+ if ( showSubjectContext && event === 'updated' ) return 'updated' ;
178+ if ( changeCount > 0 && ( ! event || event === 'updated' ) ) return 'changed' ;
196179 switch ( event ) {
197180 case 'created' :
198181 return 'created' ;
@@ -234,6 +217,7 @@ export default class ActivityLogComponent extends Component {
234217
235218 out . push ( {
236219 key,
220+ label : this . #attributeLabel( key ) ,
237221 from : this . #formatValue( prev ) ,
238222 to : this . #formatValue( next ) ,
239223 fromRaw : prev ,
@@ -261,6 +245,10 @@ export default class ActivityLogComponent extends Component {
261245 return typeof key === 'string' && ( key . endsWith ( '_uuid' ) || key === 'uuid' || key . endsWith ( 'Id' ) || key . endsWith ( '_id' ) ) ;
262246 }
263247
248+ #attributeLabel( key ) {
249+ return smartHumanize ( String ( key ) . replace ( / _ / g, ' ' ) ) ;
250+ }
251+
264252 #looksLikeUuid( v ) {
265253 return / ^ [ 0 - 9 a - f A - F ] { 8 } - [ 0 - 9 a - f A - F ] { 4 } - [ 1 - 5 ] [ 0 - 9 a - f A - F ] { 3 } - [ 8 9 a b A B ] [ 0 - 9 a - f A - F ] { 3 } - [ 0 - 9 a - f A - F ] { 12 } $ / . test ( v ) ;
266254 }
@@ -292,6 +280,55 @@ export default class ActivityLogComponent extends Component {
292280 return subject . display_name || subject . name || subject . title || subject . address || subject . tracking || subject . public_id || subject . uuid || 'Unknown' ;
293281 }
294282
283+ #objectLabel( activity , subjectTypeLabel ) {
284+ const subjectDisplay = this . #subjectDisplay( activity ?. subject ) ;
285+ return subjectDisplay !== 'Unknown' ? subjectDisplay : subjectTypeLabel ;
286+ }
287+
288+ #shouldShowSubjectContext( ) {
289+ if ( typeof this . args . showSubjectContext === 'boolean' ) {
290+ return this . args . showSubjectContext ;
291+ }
292+
293+ if ( this . args . subjectId ) {
294+ return false ;
295+ }
296+
297+ return Boolean ( this . args . companyUuid || this . args . causerId ) ;
298+ }
299+
300+ #targetPhrase( activity , subjectTypeLabel , event ) {
301+ const typeLabel = this . #sentenceCaseLabel( subjectTypeLabel ) ;
302+ if ( ! typeLabel ) return null ;
303+
304+ const subjectDisplay = this . #subjectDisplay( activity ?. subject ) ;
305+ const hasDisplay = subjectDisplay !== 'Unknown' && subjectDisplay . toLowerCase ( ) !== typeLabel ;
306+ const displaySuffix = hasDisplay ? ` (${ subjectDisplay } )` : '' ;
307+ const article = this . #indefiniteArticle( typeLabel ) ;
308+
309+ if ( event === 'created' ) {
310+ return `${ article } new ${ typeLabel } ${ displaySuffix } ` ;
311+ }
312+
313+ return `${ article } ${ typeLabel } ${ displaySuffix } ` ;
314+ }
315+
316+ #sentenceCaseLabel( label ) {
317+ if ( ! label || typeof label !== 'string' ) return '' ;
318+ return label . trim ( ) . toLowerCase ( ) ;
319+ }
320+
321+ #indefiniteArticle( label ) {
322+ return / ^ [ a e i o u ] / i. test ( label ) ? 'an' : 'a' ;
323+ }
324+
325+ #initial( name ) {
326+ return String ( name || 'S' )
327+ . trim ( )
328+ . charAt ( 0 )
329+ . toUpperCase ( ) ;
330+ }
331+
295332 #isPlainObject( v ) {
296333 return v && typeof v === 'object' && ! isArray ( v ) ;
297334 }
0 commit comments