@@ -102,7 +102,6 @@ function CompactIssuePreview({group}: {group: Group}) {
102102
103103function ClusterIssues ( { groupIds} : { groupIds : number [ ] } ) {
104104 const organization = useOrganization ( ) ;
105-
106105 const previewGroupIds = groupIds . slice ( 0 , 3 ) ;
107106
108107 const { data : groups , isPending} = useApiQuery < Group [ ] > (
@@ -120,11 +119,7 @@ function ClusterIssues({groupIds}: {groupIds: number[]}) {
120119 }
121120 ) ;
122121
123- if ( isPending ) {
124- return < LoadingIndicator size = { 24 } /> ;
125- }
126-
127- if ( ! groups || groups . length === 0 ) {
122+ if ( isPending || ! groups || groups . length === 0 ) {
128123 return null ;
129124 }
130125
@@ -151,33 +146,27 @@ function ClusterCard({
151146} ) {
152147 const organization = useOrganization ( ) ;
153148 const issueCount = cluster . group_ids . length ;
149+ const [ showDescription , setShowDescription ] = useState ( false ) ;
154150
155151 return (
156152 < CardContainer >
157- < Flex
158- justify = "between"
159- align = "start"
160- gap = "md"
161- paddingBottom = "md"
162- borderBottom = "primary"
163- >
164- < Disclosure >
165- < Disclosure . Title >
166- < Heading as = "h3" size = "md" style = { { wordBreak : 'break-word' } } >
167- { cluster . title }
168- </ Heading >
169- </ Disclosure . Title >
170- < Disclosure . Content >
171- < Text as = "p" variant = "muted" style = { { marginBottom : space ( 1.5 ) } } >
172- { cluster . description }
173- </ Text >
174- { cluster . fixability_score !== null && (
175- < Text size = "sm" variant = "muted" >
176- { t ( '%s%% confidence' , Math . round ( cluster . fixability_score * 100 ) ) }
177- </ Text >
178- ) }
179- </ Disclosure . Content >
180- </ Disclosure >
153+ < Flex justify = "between" align = "start" gap = "md" paddingBottom = "md" >
154+ < Flex direction = "column" gap = "xs" style = { { flex : 1 , minWidth : 0 } } >
155+ < Heading as = "h3" size = "md" style = { { wordBreak : 'break-word' } } >
156+ { cluster . title }
157+ </ Heading >
158+ { cluster . description && (
159+ < Fragment >
160+ { showDescription ? (
161+ < DescriptionText > { cluster . description } </ DescriptionText >
162+ ) : (
163+ < ReadMoreButton onClick = { ( ) => setShowDescription ( true ) } >
164+ { t ( 'View summary' ) }
165+ </ ReadMoreButton >
166+ ) }
167+ </ Fragment >
168+ ) }
169+ </ Flex >
181170 < IssueCountBadge >
182171 < IssueCountNumber > { issueCount } </ IssueCountNumber >
183172 < Text size = "xs" variant = "muted" uppercase >
@@ -264,119 +253,142 @@ function DynamicGrouping() {
264253 }
265254
266255 return (
267- < Container padding = "lg" maxWidth = "1600px" margin = "0 auto" >
268- < Breadcrumbs
269- crumbs = { [
270- {
271- label : t ( 'Issues' ) ,
272- to : `/organizations/${ organization . slug } /issues/` ,
273- } ,
274- {
275- label : t ( 'Top Issues' ) ,
276- } ,
277- ] }
278- />
279-
280- < Heading as = "h1" style = { { marginBottom : space ( 2 ) } } >
281- { t ( 'Top Issues' ) }
282- </ Heading >
283-
284- { isPending ? (
285- < LoadingIndicator />
286- ) : (
287- < Fragment >
288- < Text size = "sm" variant = "muted" >
289- { tn (
290- 'Viewing %s issue in %s cluster' ,
291- 'Viewing %s issues across %s clusters' ,
292- totalIssues ,
293- filteredAndSortedClusters . length
294- ) }
295- </ Text >
256+ < PageWrapper >
257+ < HeaderSection >
258+ < Breadcrumbs
259+ crumbs = { [
260+ {
261+ label : t ( 'Issues' ) ,
262+ to : `/organizations/${ organization . slug } /issues/` ,
263+ } ,
264+ {
265+ label : t ( 'Top Issues' ) ,
266+ } ,
267+ ] }
268+ />
269+
270+ < Heading as = "h1" style = { { marginBottom : space ( 2 ) } } >
271+ { t ( 'Top Issues' ) }
272+ </ Heading >
273+
274+ { isPending ? null : (
275+ < Fragment >
276+ < Text size = "sm" variant = "muted" >
277+ { tn (
278+ 'Viewing %s issue in %s cluster' ,
279+ 'Viewing %s issues across %s clusters' ,
280+ totalIssues ,
281+ filteredAndSortedClusters . length
282+ ) }
283+ </ Text >
296284
297- < Container
298- padding = "sm"
299- border = "primary"
300- radius = "md"
301- background = "primary"
302- marginTop = "md"
303- marginBottom = "lg"
304- >
305- < Disclosure >
306- < Disclosure . Title >
307- < Text size = "sm" bold >
308- { t ( 'More Filters' ) }
309- </ Text >
310- </ Disclosure . Title >
311- < Disclosure . Content >
312- < Flex direction = "column" gap = "md" paddingTop = "md ">
313- < Flex gap = "sm" align = "center" >
314- < Checkbox
315- checked = { filterByAssignedToMe }
316- onChange = { e => setFilterByAssignedToMe ( e . target . checked ) }
317- aria-label = { t ( 'Show only issues assigned to me' ) }
318- size = "sm"
319- / >
320- < Text size = "sm" variant = "muted" >
321- { t ( 'Only show issues assigned to me' ) }
322- </ Text >
323- </ Flex >
324- < Flex gap = "sm" align = "center ">
325- < Text size = "sm" variant = "muted" >
326- { t ( 'Minimum fixability score (%)' ) }
327- </ Text >
328- < NumberInput
329- min = { 0 }
330- max = { 100 }
331- value = { minFixabilityScore }
332- onChange = { value => setMinFixabilityScore ( value ?? 0 ) }
333- aria-label = { t ( 'Minimum fixability score' ) }
334- size = "sm"
335- / >
285+ < Container
286+ padding = "sm"
287+ border = "primary"
288+ radius = "md"
289+ background = "primary"
290+ marginTop = "md"
291+ >
292+ < Disclosure >
293+ < Disclosure . Title >
294+ < Text size = "sm" bold >
295+ { t ( 'More Filters' ) }
296+ </ Text >
297+ </ Disclosure . Title >
298+ < Disclosure . Content >
299+ < Flex direction = "column" gap = "md" paddingTop = "md" >
300+ < Flex gap = "sm" align = "center ">
301+ < Checkbox
302+ checked = { filterByAssignedToMe }
303+ onChange = { e => setFilterByAssignedToMe ( e . target . checked ) }
304+ aria-label = { t ( 'Show only issues assigned to me' ) }
305+ size = "sm"
306+ />
307+ < Text size = "sm" variant = "muted" >
308+ { t ( 'Only show issues assigned to me' ) }
309+ </ Text >
310+ </ Flex >
311+ < Flex gap = "sm" align = "center" >
312+ < Text size = "sm" variant = "muted ">
313+ { t ( 'Minimum fixability score (%)' ) }
314+ </ Text >
315+ < NumberInput
316+ min = { 0 }
317+ max = { 100 }
318+ value = { minFixabilityScore }
319+ onChange = { value => setMinFixabilityScore ( value ?? 0 ) }
320+ aria-label = { t ( 'Minimum fixability score' ) }
321+ size = "sm"
322+ />
323+ </ Flex >
336324 </ Flex >
337- </ Flex >
338- </ Disclosure . Content >
339- </ Disclosure >
340- </ Container >
341-
342- { filteredAndSortedClusters . length === 0 ? (
343- < Container padding = "lg" border = "primary" radius = "md" background = "primary" >
344- < Text variant = "muted" align = "center" as = "div" >
345- { t ( 'No clusters match the current filters' ) }
346- </ Text >
325+ </ Disclosure . Content >
326+ </ Disclosure >
347327 </ Container >
348- ) : (
349- < CardsGrid >
350- { filteredAndSortedClusters . map ( cluster => (
351- < ClusterCard
352- key = { cluster . cluster_id }
353- cluster = { cluster }
354- onRemove = { handleRemoveCluster }
355- />
356- ) ) }
357- </ CardsGrid >
358- ) }
359- </ Fragment >
360- ) }
361- </ Container >
328+ </ Fragment >
329+ ) }
330+ </ HeaderSection >
331+
332+ < CardsSection >
333+ { isPending ? (
334+ < LoadingIndicator />
335+ ) : filteredAndSortedClusters . length === 0 ? (
336+ < Container padding = "lg" border = "primary" radius = "md" background = "primary" >
337+ < Text variant = "muted" align = "center" as = "div" >
338+ { t ( 'No clusters match the current filters' ) }
339+ </ Text >
340+ </ Container >
341+ ) : (
342+ < CardsGrid >
343+ { filteredAndSortedClusters . map ( cluster => (
344+ < ClusterCard
345+ key = { cluster . cluster_id }
346+ cluster = { cluster }
347+ onRemove = { handleRemoveCluster }
348+ />
349+ ) ) }
350+ </ CardsGrid >
351+ ) }
352+ </ CardsSection >
353+ </ PageWrapper >
362354 ) ;
363355}
364356
365- // Grid layout for cards - needs CSS grid
357+ const PageWrapper = styled ( 'div' ) `
358+ display: flex;
359+ flex-direction: column;
360+ min-height: 100%;
361+ ` ;
362+
363+ const HeaderSection = styled ( 'div' ) `
364+ padding: ${ space ( 4 ) } ${ space ( 4 ) } ${ space ( 3 ) } ;
365+ ` ;
366+
367+ const CardsSection = styled ( 'div' ) `
368+ flex: 1;
369+ padding: ${ space ( 2 ) } ${ space ( 4 ) } ${ space ( 4 ) } ;
370+ ` ;
371+
366372const CardsGrid = styled ( 'div' ) `
367373 display: grid;
368- grid-template-columns: repeat(auto-fill, minmax(500px, 1fr) );
374+ grid-template-columns: repeat(2, 1fr);
369375 gap: ${ space ( 3 ) } ;
376+ align-items: start;
377+
378+ @media (max-width: ${ p => p . theme . breakpoints . lg } ) {
379+ grid-template-columns: 1fr;
380+ }
370381` ;
371382
372- // Card with hover effect - needs custom transition/hover styles
383+ // Card with hover effect
373384const CardContainer = styled ( 'div' ) `
374385 background: ${ p => p . theme . background } ;
375386 border: 1px solid ${ p => p . theme . border } ;
376387 border-radius: ${ p => p . theme . borderRadius } ;
377388 padding: ${ space ( 3 ) } ;
378389 display: flex;
379390 flex-direction: column;
391+ min-width: 0;
380392 transition:
381393 border-color 0.2s ease,
382394 box-shadow 0.2s ease;
@@ -405,7 +417,7 @@ const IssueCountNumber = styled('div')`
405417 line-height: 1;
406418` ;
407419
408- // Issue preview link with hover transform effect
420+ // Issue preview link with hover effect
409421const IssuePreviewLink = styled ( Link ) `
410422 display: block;
411423 padding: ${ space ( 1.5 ) } ${ space ( 2 ) } ;
@@ -414,13 +426,11 @@ const IssuePreviewLink = styled(Link)`
414426 border-radius: ${ p => p . theme . borderRadius } ;
415427 transition:
416428 border-color 0.15s ease,
417- background 0.15s ease,
418- transform 0.15s ease;
429+ background 0.15s ease;
419430
420431 &:hover {
421432 border-color: ${ p => p . theme . purple300 } ;
422433 background: ${ p => p . theme . backgroundElevated } ;
423- transform: translateX(2px);
424434 }
425435` ;
426436
@@ -454,4 +464,26 @@ const MetaSeparator = styled('div')`
454464 background-color: ${ p => p . theme . innerBorder } ;
455465` ;
456466
467+ const ReadMoreButton = styled ( 'button' ) `
468+ background: none;
469+ border: none;
470+ padding: 0;
471+ font-size: ${ p => p . theme . fontSize . sm } ;
472+ color: ${ p => p . theme . subText } ;
473+ cursor: pointer;
474+ text-align: left;
475+
476+ &:hover {
477+ color: ${ p => p . theme . textColor } ;
478+ text-decoration: underline;
479+ }
480+ ` ;
481+
482+ const DescriptionText = styled ( 'p' ) `
483+ margin: 0;
484+ font-size: ${ p => p . theme . fontSize . sm } ;
485+ color: ${ p => p . theme . subText } ;
486+ line-height: 1.5;
487+ ` ;
488+
457489export default DynamicGrouping ;
0 commit comments