Skip to content

Commit 8f03c22

Browse files
committed
Group by errors
1 parent 15b6805 commit 8f03c22

4 files changed

Lines changed: 85 additions & 5 deletions

File tree

code/core/src/common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export * from './js-package-manager';
4545
export * from './utils/scan-and-transform-files';
4646
export * from './utils/transform-imports';
4747
export * from '../shared/utils/module';
48+
export * from './utils/utils';
4849

4950
export { versions };
5051

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Object.groupBy polyfill
2+
export const groupBy = <K extends PropertyKey, T>(
3+
items: T[],
4+
keySelector: (item: T, index: number) => K
5+
) => {
6+
return items.reduce<Record<K, T[]>>(
7+
(acc, item, index) => {
8+
const key = keySelector(item, index);
9+
acc[key] ??= [];
10+
acc[key].push(item);
11+
return acc;
12+
},
13+
{} as Record<K, T[]>
14+
);
15+
};
16+
17+
// This invariant allows for lazy evaluation of the message, which we need to avoid excessive computation.
18+
export function invariant(
19+
condition: unknown,
20+
message?: string | (() => string)
21+
): asserts condition {
22+
if (condition) {
23+
return;
24+
}
25+
throw new Error((typeof message === 'function' ? message() : message) ?? 'Invariant failed');
26+
}

code/core/src/core-server/manifest.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { groupBy } from 'storybook/internal/common';
2+
13
import { dedent } from 'ts-dedent';
24

35
import type { ComponentManifest, ComponentsManifest } from '../types';
@@ -35,6 +37,34 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) {
3537

3638
const grid = entries.map(([key, c], idx) => renderComponentCard(key, c, idx)).join('');
3739

40+
const errorGroups = Object.entries(
41+
groupBy(
42+
entries.map(([, it]) => it).filter((it) => it.error),
43+
(manifest) => manifest.error?.name ?? 'Error'
44+
)
45+
);
46+
47+
const errorGroupsHTML = errorGroups
48+
.map(([error, grouped]) => {
49+
const id = error.toLowerCase().replace(/[^a-z0-9]+/g, '-');
50+
const headerText = `${esc(error)}`;
51+
const cards = grouped
52+
.map((manifest, id) => renderComponentCard(manifest.id, manifest, id))
53+
.join('');
54+
return `
55+
<section class="group">
56+
<input id="${id}-toggle" class="group-tg" type="checkbox" hidden />
57+
<label for="${id}-toggle" class="group-header">
58+
<span class="caret">▸</span>
59+
<span class="group-title">${headerText}</span>
60+
<span class="group-count">${grouped.length}</span>
61+
</label>
62+
<div class="group-cards">${cards}</div>
63+
</section>
64+
`;
65+
})
66+
.join('');
67+
3868
return dedent`<!doctype html>
3969
<html lang="en">
4070
<head>
@@ -127,12 +157,34 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) {
127157
.dot-err{background:var(--err)}
128158
.ex-name{font-weight:600}
129159
160+
/* Error groups (visible in errors filter) */
161+
.error-groups{display:none; margin-bottom:16px;}
162+
.group{border:1px solid var(--border);background:var(--panel);border-radius:14px;overflow:hidden}
163+
.group + .group{margin-top:12px}
164+
.group-header{display:flex;align-items:center;gap:10px;padding:12px 14px;cursor:pointer;border-bottom:1px solid var(--border)}
165+
.group-header:hover{background:#141722}
166+
.group-title{font-weight:600;flex:1}
167+
.group-count{font-size:12px;color:var(--muted);}
168+
.group-cards{display:none;padding:12px}
169+
.group .card{margin:12px 0}
170+
.group .card:first-child{margin-top:0}
171+
.group .card:last-child{margin-bottom:0}
172+
/* caret rotation */
173+
.group-tg:checked + label .caret{transform:rotate(90deg)}
174+
.caret{transition:transform .15s ease}
175+
/* toggle body */
176+
.group-tg:checked ~ .group-cards{display:block}
177+
130178
/* CSS-only filtering of cards via top pills */
131179
#filter-errors:target ~ main .card:not(.has-error):not(.has-example-error){display:none}
132180
#filter-warnings:target ~ main .card:not(.has-warn){display:none}
133181
#filter-example-errors:target ~ main .card:not(.has-example-error){display:none}
134182
#filter-all:target ~ main .card{display:block}
135-
183+
/* In errors view, hide standalone component-error cards in the regular grid (they will appear in groups) */
184+
#filter-errors:target ~ main .grid .card.has-error{display:none}
185+
/* Show grouped section only in errors view */
186+
#filter-errors:target ~ main .error-groups{display:block}
187+
136188
/* When a toggle is checked, show the corresponding panel */
137189
.card > .tg-err:checked ~ .panels .panel-err { display: grid; }
138190
.card > .tg-warn:checked ~ .panels .panel-warn { display: grid; }
@@ -173,6 +225,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) {
173225
<div class="grid" role="list">
174226
${grid || `<div class="card"><div class="head"><div class="hint">No components.</div></div></div>`}
175227
</div>
228+
${errorGroups.length ? `<div class="error-groups" role="region" aria-label="Error groups">${errorGroupsHTML}</div>` : ''}
176229
</div>
177230
</main>
178231
</body>

code/renderers/react/src/componentManifest/generator.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ export const componentManifestGenerator = async () => {
7171

7272
const error = !componentName
7373
? {
74-
name: 'NoMetaComponentError',
74+
name: 'No meta.component specified',
7575
message: 'Specify meta.component for the component to be included in the manifest.',
7676
}
7777
: {
78-
name: 'NoComponentImportError',
78+
name: 'No component import found',
7979
message: `No component file found for the "${componentName}" component.`,
8080
};
8181
return {
@@ -101,7 +101,7 @@ export const componentManifestGenerator = async () => {
101101
name,
102102
examples,
103103
error: {
104-
name: 'ComponentReadError',
104+
name: 'Component file could not be read',
105105
message: `Could not read the component file located at "${entry.componentPath}".\nPrefer relative imports.`,
106106
},
107107
};
@@ -115,7 +115,7 @@ export const componentManifestGenerator = async () => {
115115

116116
const error = !docgen
117117
? {
118-
name: 'DocgenError',
118+
name: 'Docgen evaluation failed',
119119
message:
120120
`Could not parse props information for the component file located at "${entry.componentPath}"\n` +
121121
`Avoid barrel files when importing your component file.\n` +

0 commit comments

Comments
 (0)