@@ -119,19 +119,33 @@ <h5 class="mt-4 mb-2">
119119 < span class ="badge badge-secondary ml-1 "> {{ type_count }}</ span >
120120 </ h5 >
121121 {% for error_msg, configs in err_groups %}
122- < div class ="card mb-2 ">
122+ < div class ="card mb-2 error-group-card "
123+ data-type-name ="{{ type_name|escapejs }} "
124+ data-error-msg ="{{ error_msg|escapejs }} ">
123125 < div class ="card-header d-flex justify-content-between align-items-center ">
124126 < div class ="d-flex align-items-center ">
125127 < span class ="badge badge-warning mr-2 "> {{ configs|length }}</ span >
126128 < code class ="text-truncate " style ="max-width: 700px; " title ="{{ error_msg }} "> {{ error_msg }}</ code >
127129 </ div >
128- < button class ="btn btn-sm btn-outline-secondary collapse-toggle "
129- type ="button "
130- data-toggle ="collapse "
131- data-target ="#group-{{ forloop.parentloop.counter }}-{{ forloop.counter }} "
132- aria-expanded ="true ">
133- < span class ="caret-icon "> ▼</ span >
134- </ button >
130+ < div class ="d-flex align-items-center " style ="gap:.4rem; ">
131+ < button type ="button "
132+ class ="btn btn-link btn-sm p-0 js-copy-group-link "
133+ title ="Copy link to this group ">
134+ < i class ="fas fa-link fa-xs text-muted "> </ i >
135+ </ button >
136+ < button type ="button "
137+ class ="btn btn-link btn-sm p-0 js-email-group "
138+ title ="Share via email ">
139+ < i class ="fas fa-envelope fa-xs text-muted "> </ i >
140+ </ button >
141+ < button class ="btn btn-sm btn-outline-secondary collapse-toggle "
142+ type ="button "
143+ data-toggle ="collapse "
144+ data-target ="#group-{{ forloop.parentloop.counter }}-{{ forloop.counter }} "
145+ aria-expanded ="true ">
146+ < span class ="caret-icon "> ▼</ span >
147+ </ button >
148+ </ div >
135149 </ div >
136150 < div class ="collapse show " id ="group-{{ forloop.parentloop.counter }}-{{ forloop.counter }} ">
137151 < div class ="card-body p-0 ">
@@ -191,6 +205,15 @@ <h5 class="mt-4 mb-2">
191205
192206< script >
193207 ( function ( ) {
208+ function hashStr ( s ) {
209+ var h = 0 ;
210+ for ( var i = 0 ; i < s . length ; i ++ ) h = ( Math . imul ( 31 , h ) + s . charCodeAt ( i ) ) | 0 ;
211+ return ( h >>> 0 ) . toString ( 36 ) ;
212+ }
213+ function cardHash ( card ) {
214+ return hashStr ( card . getAttribute ( 'data-type-name' ) + '||' + card . getAttribute ( 'data-error-msg' ) ) ;
215+ }
216+
194217 var overlay = document . getElementById ( 'slide-panel-overlay' ) ;
195218 var panel = document . getElementById ( 'slide-panel' ) ;
196219 var panelBody = document . getElementById ( 'slide-panel-body' ) ;
@@ -264,6 +287,21 @@ <h5 class="mt-4 mb-2">
264287 }
265288
266289 document . addEventListener ( 'click' , function ( e ) {
290+ var groupCopyBtn = e . target . closest ( '.js-copy-group-link' ) ;
291+ if ( groupCopyBtn ) {
292+ e . stopPropagation ( ) ;
293+ var card = groupCopyBtn . closest ( '.error-group-card' ) ;
294+ var fragment = 'eg-' + cardHash ( card ) ;
295+ var url = window . location . origin + window . location . pathname
296+ + window . location . search + '#' + fragment ;
297+ navigator . clipboard . writeText ( url ) ;
298+ showCopied ( groupCopyBtn ) ;
299+ var icon = groupCopyBtn . querySelector ( 'i' ) ;
300+ var prev = icon . className ;
301+ icon . className = 'fas fa-check fa-xs text-success' ;
302+ setTimeout ( function ( ) { icon . className = prev ; } , 1500 ) ;
303+ return ;
304+ }
267305 var copyBtn = e . target . closest ( '.js-copy-link' ) ;
268306 if ( copyBtn ) {
269307 e . stopPropagation ( ) ;
@@ -284,6 +322,49 @@ <h5 class="mt-4 mb-2">
284322 htmx . ajax ( 'GET' , '{% url "inbound_integration_configuration_list" %}' + '/' + editId + '/edit' , { target : '#slide-panel-body' , swap : 'innerHTML' } ) ;
285323 } ) ;
286324
325+ document . addEventListener ( 'click' , function ( e ) {
326+ var emailBtn = e . target . closest ( '.js-email-group' ) ;
327+ if ( ! emailBtn ) return ;
328+ e . stopPropagation ( ) ;
329+ var card = emailBtn . closest ( '.error-group-card' ) ;
330+ var errorMsg = card . getAttribute ( 'data-error-msg' ) ;
331+ var typeName = card . getAttribute ( 'data-type-name' ) ;
332+ var rows = card . querySelectorAll ( 'tbody tr' ) ;
333+ var configLines = [ ] ;
334+ rows . forEach ( function ( row ) {
335+ var cells = row . querySelectorAll ( 'td' ) ;
336+ var name = cells [ 2 ] ? cells [ 2 ] . textContent . trim ( ) : '' ;
337+ var org = cells [ 3 ] ? cells [ 3 ] . textContent . trim ( ) : '' ;
338+ if ( name ) configLines . push ( '• ' + name + ' (' + org + ')' ) ;
339+ } ) ;
340+ var url = window . location . origin + window . location . pathname
341+ + window . location . search + '#eg-' + cardHash ( card ) ;
342+ var subject = 'Integration errors: ' + typeName + ' — '
343+ + ( errorMsg . length > 80 ? errorMsg . slice ( 0 , 80 ) + '…' : errorMsg ) ;
344+ var body = 'Error:\n' + errorMsg
345+ + '\n\nAffected configurations (' + configLines . length + '):\n'
346+ + configLines . join ( '\n' )
347+ + '\n\nDirect link:\n' + url ;
348+ window . location . href = 'mailto:?subject=' + encodeURIComponent ( subject )
349+ + '&body=' + encodeURIComponent ( body ) ;
350+ } ) ;
351+
352+ // Deep-link: scroll to and highlight error group card from URL fragment
353+ var fragMatch = location . hash . match ( / ^ # ( e g - [ a - z 0 - 9 ] + ) $ / ) ;
354+ if ( fragMatch ) {
355+ var targetHash = fragMatch [ 1 ] . slice ( 3 ) ; // strip 'eg-'
356+ var target = null ;
357+ document . querySelectorAll ( '.error-group-card' ) . forEach ( function ( c ) {
358+ if ( cardHash ( c ) === targetHash ) target = c ;
359+ } ) ;
360+ if ( target ) {
361+ target . scrollIntoView ( { behavior : 'smooth' , block : 'start' } ) ;
362+ target . style . transition = 'box-shadow .3s' ;
363+ target . style . boxShadow = '0 0 0 3px #ffc107' ;
364+ setTimeout ( function ( ) { target . style . boxShadow = '' ; } , 2000 ) ;
365+ }
366+ }
367+
287368 // Deep-link: open edit panel from URL hash
288369 var hashMatch = location . hash . match ( / ^ # e d i t = ( .+ ) $ / ) ;
289370 if ( hashMatch ) {
0 commit comments