diff --git a/eslint.config.mjs b/eslint.config.mjs index 22647924d8e..56666218aa9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -424,7 +424,7 @@ const config = defineConfig([ // Timeline event taxonomy: catalog keys are REST `event.type` wire values // (snake_case), not code identifiers. { - files: ['packages/react/src/Timeline/taxonomy/eventTaxonomy.ts'], + files: ['packages/react/src/Timeline/taxonomy/github/catalogs.ts'], rules: { camelcase: 'off', }, diff --git a/packages/react/src/Timeline/Timeline.code-scanning.features.stories.tsx b/packages/react/src/Timeline/Timeline.code-scanning.features.stories.tsx index 9e03994be10..f1d8832ed37 100644 --- a/packages/react/src/Timeline/Timeline.code-scanning.features.stories.tsx +++ b/packages/react/src/Timeline/Timeline.code-scanning.features.stories.tsx @@ -26,6 +26,7 @@ import { UserActor, VariantSection, } from './internal/timelineStoryHelpers' +import {eventDataAttributesFor} from './taxonomy' import classes from './Timeline.code-scanning.features.stories.module.css' /** @@ -172,6 +173,17 @@ const ConfigPill = ({category}: {category: string}) => ( */ const SubRow = ({children}: {children: React.ReactNode}) =>
{children}
+/** + * Per-row taxonomy `data-*` attributes (Phase 3 tagging, github/primer#6664, + * epic #6654). Each `Timeline.Item` below projects its Code Scanning leaf event + * type through the shared `eventDataAttributesFor('code-scanning', , + * login?)` helper (`./taxonomy`, primer/react#8180), mirroring the License + * Compliance pilot (primer/react#8216). `category` and `visibility` are derived + * FROM the catalog so the story never hand-maintains them. Pass the row's + * rendered `UserActor` login for USER events (resolves `data-actor-type`); omit + * it for SYSTEM rows so `data-actor-type` is left off entirely. + */ + export default { title: 'Components/Timeline/Events/Code Scanning', component: Timeline, @@ -219,7 +231,7 @@ export const EventDetected = () => ( sub-row, and a right-aligned tool-version Label (Timeline.Actions). */} - + @@ -253,7 +265,7 @@ export const EventDetected = () => ( card — `show_timeline_commit?` is false for this event). */} - + @@ -283,7 +295,7 @@ export const EventDetected = () => ( {category}" pill (rendered when the alert has more than one category). */} - + @@ -314,13 +326,20 @@ export const EventDetected = () => ( * Config-deleted renders the analysis category as an inline subtle mono pill * (`MonoPill`); when the category is empty the live ERB substitutes "API * Upload". + * + * TAXONOMY (github/primer#6664): the two "Fixed in branch" rows are the + * ALERT_CLOSED_BECAME_FIXED wire event and carry `data-event-type="fixed"`. The + * two "Configuration deleted" rows are visually grouped under Fixed here, but the + * taxonomy catalog folds their ALERT_CLOSED_BECAME_OUTDATED wire event into + * `closed` (a SYSTEM closure), so those rows carry `data-event-type="closed"`. + * Both are system events with no actor, so neither emits `data-actor-type`. */ export const EventFixed = () => ( {/* Fixed — selected ref → SOLID purple shield-check */} - + @@ -335,7 +354,7 @@ export const EventFixed = () => ( {/* Fixed — non-selected ref → bare default check */} - + @@ -348,9 +367,12 @@ export const EventFixed = () => ( {/* Config deleted — selected ref → SOLID purple shield-check */} + {/* Config-deletion is the ALERT_CLOSED_BECAME_OUTDATED wire event, which the + taxonomy folds into `closed` (system closure), not `fixed` — so this row + carries data-event-type="closed" despite its visual "Fixed" grouping. */} - + @@ -366,7 +388,7 @@ export const EventFixed = () => ( {/* Config deleted — non-selected ref → bare default check */} - + @@ -398,7 +420,7 @@ export const EventClosedByUser = () => ( {/* Closed as false positive — with a resolution-note sub-row */} - + @@ -415,7 +437,7 @@ export const EventClosedByUser = () => ( {/* Closed as used in tests */} - + @@ -431,7 +453,7 @@ export const EventClosedByUser = () => ( {/* Closed as won't fix */} - + @@ -448,7 +470,7 @@ export const EventClosedByUser = () => ( `resolution == :NO_RESOLUTION`. */} - + @@ -475,7 +497,7 @@ export const EventReopened = () => ( - + @@ -510,7 +532,7 @@ export const EventDismissalRequested = () => ( - + @@ -550,7 +572,7 @@ export const EventDismissalReviewed = () => ( {/* Approved — check icon + reviewer-comment sub-row */} - + @@ -567,7 +589,7 @@ export const EventDismissalReviewed = () => ( {/* Denied — x icon */} - + diff --git a/packages/react/src/Timeline/Timeline.dependabot.features.stories.tsx b/packages/react/src/Timeline/Timeline.dependabot.features.stories.tsx index 7768c168dff..791cbde6fa0 100644 --- a/packages/react/src/Timeline/Timeline.dependabot.features.stories.tsx +++ b/packages/react/src/Timeline/Timeline.dependabot.features.stories.tsx @@ -29,6 +29,7 @@ import { UserActor, VariantSection, } from './internal/timelineStoryHelpers' +import {eventDataAttributesFor} from './taxonomy' /** * Dependabot alert Timeline event examples (Phase 2 of github/primer#6663). @@ -56,10 +57,26 @@ import { * base `Timeline` component's own stories, and any docs-site representation is a * Phase 3 consideration via base-component story changes, out of scope here. * - * FUTURE FILTERING (taxonomy still open — github/primer#6663): category - * `data-*` attributes (e.g. `data-event-category="opened"`) will attach to each - * `Timeline.Item` below so stories can be filtered/grouped by event family. We - * intentionally do NOT add them yet to avoid baking in a taxonomy. + * TAXONOMY `data-*` CONTRACT: every cataloged `Timeline.Item` below carries the + * event `data-*` attributes projected from the merged taxonomy module + * (`./taxonomy`, primer/react#8180) — the single source of truth for Timeline + * event categorization (github/primer#6664, docs github/primer#6888). Each row + * spreads the output of the shared `eventDataAttributesFor` helper, which + * derives `category` / `visibility` FROM the catalog entry so the stories stay + * in sync with the catalog, and + * resolves `data-actor-type` at runtime from each row's rendered actor login: + * Dependabot-authored rows resolve to `bot`, user-driven + * rows to `user`. The contract per rendered `
  • `: `data-event-scope`, + * `data-event-type` (the UNSCOPED leaf), `data-event-category`, + * `data-event-visibility` (defaults `primary`), and `data-actor-type`. This + * applies the same contract proven by the License Compliance pilot + * (primer/react#8216). + * + * SHARED / PARKED EVENTS (left untagged): the Assignment and Copilot-work groups + * are cross-surface SHARED events, deliberately kept OUT of the per-surface + * Dependabot catalog (github/primer#6888), so they intentionally carry NO + * `data-*` attributes. See the code comments above `EventAssignment` and + * `EventCopilotWork`. * * SLOT USAGE (Phase 1 slots — same convention as the Issues group): * - `Timeline.Avatar` (gutter slot, #6677): the 40px LEFT-GUTTER avatar. @@ -120,6 +137,15 @@ const PushPill = ({sha}: {sha: string}) => ( ) +/** + * The stories below project each row's `data-*` via the shared + * `eventDataAttributesFor('dependabot', , login?)` helper (`./taxonomy`). + * `category` and `visibility` come FROM the catalog entry; `data-actor-type` + * resolves at runtime from the login (Dependabot-authored rows pass a `…[bot]` + * login so they resolve to `bot`; user-driven rows pass the rendered user + * login). See github/primer#6664 and the taxonomy docs (github/primer#6888). + */ + export default { title: 'Components/Timeline/Events/Dependabot', component: Timeline, @@ -160,7 +186,7 @@ export const EventOpened = () => ( {/* Opened — no source */} - + @@ -176,7 +202,7 @@ export const EventOpened = () => ( {/* OpenedFromPR — bold `#123` pull-request link (scheme: primary, bold) */} - + @@ -192,7 +218,7 @@ export const EventOpened = () => ( {/* OpenedFromPush — blue push-pill with the 7-char `after` SHA */} - + @@ -225,7 +251,7 @@ export const EventFixed = () => ( {/* Fixed — no source */} - + @@ -241,7 +267,7 @@ export const EventFixed = () => ( {/* FixedViaPR — bold `#123` pull-request link */} - + @@ -257,7 +283,7 @@ export const EventFixed = () => ( {/* FixedViaPush — blue push-pill */} - + @@ -290,7 +316,7 @@ export const EventDismissed = () => ( {/* Manual — risk is tolerable (with an optional dismissal note) */} - + @@ -309,7 +335,7 @@ export const EventDismissed = () => ( {/* Manual — fix started */} - + @@ -325,7 +351,7 @@ export const EventDismissed = () => ( {/* Manual — no bandwidth to fix this */} - + @@ -341,7 +367,7 @@ export const EventDismissed = () => ( {/* Manual — vulnerable code is not actually used */} - + @@ -357,7 +383,7 @@ export const EventDismissed = () => ( {/* Manual — inaccurate */} - + @@ -373,7 +399,7 @@ export const EventDismissed = () => ( {/* Auto — rule-based, no source (with optional rule comment) */} - + @@ -396,7 +422,7 @@ export const EventDismissed = () => ( {/* Auto — from a pull request */} - + @@ -412,7 +438,7 @@ export const EventDismissed = () => ( {/* Auto — from a push */} - + @@ -441,7 +467,7 @@ export const EventReopened = () => ( {/* Manual reopen — user actor */} - + @@ -457,7 +483,7 @@ export const EventReopened = () => ( {/* Reintroduced — no source */} - + @@ -473,7 +499,7 @@ export const EventReopened = () => ( {/* Reintroduced — from a pull request */} - + @@ -489,7 +515,7 @@ export const EventReopened = () => ( {/* Reintroduced — from a push */} - + @@ -505,7 +531,7 @@ export const EventReopened = () => ( {/* Auto-reopened — rule change (with optional rule comment) */} - + @@ -560,7 +586,7 @@ export const EventDismissalRequest = () => ( place that right-aligned control in the `Timeline.Actions` slot. */} - + @@ -584,7 +610,7 @@ export const EventDismissalRequest = () => ( {/* Dismissal approved — circle user actor, success/check badge */} - + @@ -601,7 +627,7 @@ export const EventDismissalRequest = () => ( {/* Dismissal denied — circle user actor, danger/x badge */} - + @@ -619,7 +645,7 @@ export const EventDismissalRequest = () => ( "x")` (no bg) → a bare default badge, not a danger/emphasis one. */} - + @@ -651,6 +677,12 @@ export const EventDismissalRequest = () => ( * The five variants mirror the Secret scanning assignment story: self-assign, * assign another, self-unassign, unassign another, and a combined assign + * unassign example. + * + * SHARED / PARKED — NO `data-*` TAXONOMY: assignment is a cross-surface SHARED + * event, deliberately kept OUT of the per-surface Dependabot catalog + * (`DEPENDABOT_TAXONOMY` has no assignment leaf), per the taxonomy docs + * (github/primer#6888). It has no per-surface taxonomy leaf yet, so these rows + * intentionally carry NO `data-*` attributes. */ export const EventAssignment = () => ( @@ -758,6 +790,12 @@ export const EventAssignment = () => ( * button above. * - Work finished (`with_badge(icon: "repo-push")`): default (gray) badge, no * right control. + * + * SHARED / PARKED — NO `data-*` TAXONOMY: Copilot-agent work is a cross-surface + * SHARED event, deliberately kept OUT of the per-surface Dependabot catalog + * (`DEPENDABOT_TAXONOMY` has no copilot leaf), per the taxonomy docs + * (github/primer#6888). It has no per-surface taxonomy leaf yet, so these rows + * intentionally carry NO `data-*` attributes. */ export const EventCopilotWork = () => ( diff --git a/packages/react/src/Timeline/Timeline.issues.features.stories.tsx b/packages/react/src/Timeline/Timeline.issues.features.stories.tsx index 20324b0b6ff..22e14ada799 100644 --- a/packages/react/src/Timeline/Timeline.issues.features.stories.tsx +++ b/packages/react/src/Timeline/Timeline.issues.features.stories.tsx @@ -40,6 +40,7 @@ import Octicon from '../Octicon' import Token from '../Token' import classes from './Timeline.issues.features.stories.module.css' import {BoldLink, Examples, MutedTime, UserActor, VariantSection} from './internal/timelineStoryHelpers' +import {eventDataAttributesFor} from './taxonomy' /** * Issue Timeline event examples (Phase 2 of github/primer#6663). @@ -104,6 +105,16 @@ export default { ], } as Meta> +/** + * The stories below serialize each Issues row's `data-*` contract + * (github/primer#6664) via the shared `eventDataAttributesFor('issue', , + * login?)` helper (`./taxonomy`). `category` + `visibility` are read from the + * catalog, so metadata leaves (labels, assignees, milestones, project fields, + * issue types, rename) pick up `visibility: 'auditOnly'` automatically. Pass the + * row's rendered actor login so `data-actor-type` resolves (every Issues event + * carries an actor). + */ + /** * The Closed event group — `IssueTimeline.eventClosed` (audit § 2). * @@ -118,7 +129,7 @@ export const EventClosed = () => ( {/* Closed as completed */} - + @@ -139,7 +150,7 @@ export const EventClosed = () => ( (portable for docs copy-paste; matches production `TimelineRow`). */} - + ( {/* Closed via pull request */} - + @@ -185,7 +196,7 @@ export const EventClosed = () => ( {/* Closed via commit */} - + @@ -210,7 +221,7 @@ export const EventClosed = () => ( equivalent for github-ui's `ProjectV2` closer link. */} - + @@ -233,7 +244,7 @@ export const EventClosed = () => ( composed here from Primer primitives. */} - + ( {/* Closed with no state reason */} - + @@ -293,7 +304,7 @@ export const EventState = () => ( {/* Reopened — open (green) badge via useIssueState (ReopenedEvent.tsx) */} - + @@ -310,7 +321,7 @@ export const EventState = () => ( plain inline Link (the audit shows it bold; live code is canonical). */} - + @@ -329,7 +340,7 @@ export const EventState = () => ( {/* Pinned */} - + @@ -345,7 +356,7 @@ export const EventState = () => ( {/* Unpinned */} - + @@ -362,7 +373,7 @@ export const EventState = () => ( the resulting discussion by number. */} - + @@ -378,6 +389,11 @@ export const EventState = () => ( {/* Converted from draft */} + {/* Untagged: this IS a real issue-timeline event (github/github + app/platform/unions/issue_timeline_items.rb lists ConvertedFromDraftEvent; + it is the Projects v2 draft-issue -> issue conversion), but the taxonomy + catalog has no 'converted_from_draft' leaf yet, so no leaf -> no data-* tag. + Kept as a coverage example pending a catalog-leaf decision. */} @@ -407,7 +423,7 @@ export const EventReferences = () => ( open PR state icon inline before the PR title). */} - + @@ -431,7 +447,7 @@ export const EventReferences = () => ( icon is behind the live code here. */} - + @@ -451,7 +467,7 @@ export const EventReferences = () => ( inline (showAgoTimestamp={false}) and the commit card is sub-content. */} - + @@ -482,7 +498,7 @@ export const EventReferences = () => ( {/* Multiple commit references — same event, pluralized copy + N cards. */} - + @@ -538,7 +554,7 @@ export const EventDuplicates = () => ( IssueLink uses the open (green) state icon. */} - + @@ -562,6 +578,10 @@ export const EventDuplicates = () => ( {/* Marked as a duplicate of this issue — no right controls. */} + {/* Untagged: verified against the redesign prototype + (github/prototyping janmaarten-a11y/timeline-redesign-v1) as an intentional + exclusion. This variant is not modeled as a distinct catalog leaf (the + catalog has only 'marked_as_duplicate'); no leaf -> no data-* tag. */} @@ -584,6 +604,7 @@ export const EventDuplicates = () => ( {/* Unmarked this as a duplicate of . */} + {/* Untagged: verified against the redesign prototype (github/prototyping janmaarten-a11y/timeline-redesign-v1) as an intentional exclusion, this variant is not modeled as a distinct catalog leaf (the catalog has only 'marked_as_duplicate'); no leaf -> no data-* tag. */} @@ -605,6 +626,7 @@ export const EventDuplicates = () => ( {/* Unmarked as a duplicate of this issue. */} + {/* Untagged: verified against the redesign prototype (github/prototyping janmaarten-a11y/timeline-redesign-v1) as an intentional exclusion, this variant is not modeled as a distinct catalog leaf (the catalog has only 'marked_as_duplicate'); no leaf -> no data-* tag. */} @@ -638,7 +660,7 @@ export const EventModeration = () => ( {/* User blocked (permanent) */} - + @@ -654,7 +676,7 @@ export const EventModeration = () => ( {/* User temporarily blocked */} - + @@ -670,7 +692,7 @@ export const EventModeration = () => ( {/* Comment pinned — IssueCommentPinnedEvent.tsx links the pinned comment. */} - + @@ -689,7 +711,7 @@ export const EventModeration = () => ( {/* Comment unpinned */} - + @@ -720,7 +742,7 @@ export const EventIssueTypes = () => ( {/* Type added */} - + @@ -750,7 +772,7 @@ export const EventIssueTypes = () => ( {/* Type removed */} - + @@ -780,7 +802,7 @@ export const EventIssueTypes = () => ( {/* Type changed */} - + @@ -840,7 +862,7 @@ export const EventIssueHierarchy = () => ( {/* Sub-issue added (single) */} - + @@ -865,7 +887,7 @@ export const EventIssueHierarchy = () => ( {/* Sub-issue added (multiple) — plural copy + N reference rows */} - + @@ -897,7 +919,7 @@ export const EventIssueHierarchy = () => ( {/* Sub-issue removed (single) */} - + @@ -922,7 +944,7 @@ export const EventIssueHierarchy = () => ( {/* Sub-issues removed (multiple) */} - + @@ -954,7 +976,7 @@ export const EventIssueHierarchy = () => ( {/* Parent issue added (single) */} - + @@ -979,7 +1001,7 @@ export const EventIssueHierarchy = () => ( {/* Parent issues added (multiple) */} - + @@ -1011,7 +1033,7 @@ export const EventIssueHierarchy = () => ( {/* Parent issue removed (single) */} - + @@ -1036,7 +1058,7 @@ export const EventIssueHierarchy = () => ( {/* Parent issues removed (multiple) */} - + @@ -1084,7 +1106,7 @@ export const EventDependencies = () => ( {/* Blocked by (single) */} - + @@ -1109,7 +1131,7 @@ export const EventDependencies = () => ( {/* Blocked by (multiple) — count in copy + N rows */} - + @@ -1141,7 +1163,7 @@ export const EventDependencies = () => ( {/* Blocked by removed (single) */} - + @@ -1166,7 +1188,7 @@ export const EventDependencies = () => ( {/* Blocked by removed (multiple) */} - + @@ -1198,7 +1220,7 @@ export const EventDependencies = () => ( {/* Blocking (single) */} - + @@ -1223,7 +1245,7 @@ export const EventDependencies = () => ( {/* Blocking (multiple) */} - + @@ -1255,7 +1277,7 @@ export const EventDependencies = () => ( {/* Blocking removed (single) */} - + @@ -1280,7 +1302,7 @@ export const EventDependencies = () => ( {/* Blocking removed (multiple) */} - + @@ -1332,7 +1354,7 @@ export const EventIssueFields = () => ( {/* Set text field */} - + @@ -1353,7 +1375,7 @@ export const EventIssueFields = () => ( {/* Set number field */} - + @@ -1374,7 +1396,7 @@ export const EventIssueFields = () => ( {/* Set date field */} - + @@ -1395,7 +1417,7 @@ export const EventIssueFields = () => ( {/* Set single-select field — value is a colored token */} - + @@ -1429,7 +1451,7 @@ export const EventIssueFields = () => ( {/* Changed text field */} - + @@ -1450,7 +1472,7 @@ export const EventIssueFields = () => ( {/* Changed number field */} - + @@ -1471,7 +1493,7 @@ export const EventIssueFields = () => ( {/* Changed date field */} - + @@ -1492,7 +1514,7 @@ export const EventIssueFields = () => ( {/* Changed single-select field */} - + @@ -1523,7 +1545,7 @@ export const EventIssueFields = () => ( {/* Cleared text field — no value */} - + @@ -1540,7 +1562,7 @@ export const EventIssueFields = () => ( {/* Cleared number field */} - + @@ -1557,7 +1579,7 @@ export const EventIssueFields = () => ( {/* Cleared date field */} - + @@ -1574,7 +1596,7 @@ export const EventIssueFields = () => ( {/* Cleared single-select field */} - + @@ -1591,7 +1613,7 @@ export const EventIssueFields = () => ( {/* Rollup: updated only — multiple field updates collapsed into one row */} - + @@ -1616,7 +1638,7 @@ export const EventIssueFields = () => ( {/* Rollup: removed only */} - + @@ -1635,7 +1657,7 @@ export const EventIssueFields = () => ( {/* Rollup: updated and also removed — combined row joined by "and also" */} - + @@ -1687,7 +1709,7 @@ export const EventProject = () => ( {/* Added to project */} - + @@ -1708,7 +1730,7 @@ export const EventProject = () => ( {/* Removed from project */} - + @@ -1732,7 +1754,7 @@ export const EventProject = () => ( strings are PLAIN TEXT (not bold). Both forms shown under one caption. */} - + @@ -1746,7 +1768,7 @@ export const EventProject = () => ( - + @@ -1786,7 +1808,7 @@ export const EventLabels = () => ( {/* Label added */} - + @@ -1815,7 +1837,7 @@ export const EventLabels = () => ( {/* Label removed */} - + @@ -1845,6 +1867,7 @@ export const EventLabels = () => ( with "and" between them. */} + {/* Untagged: a rolled-up event (labeled + unlabeled in one row) verified against the redesign prototype (github/prototyping janmaarten-a11y/timeline-redesign-v1) as an intentional exclusion, not modeled as a distinct catalog leaf; no leaf -> no data-* tag. */} @@ -1905,7 +1928,7 @@ export const EventTitle = () => ( {/* Title changed */} - + @@ -1940,7 +1963,7 @@ export const EventMilestones = () => ( {/* Added to milestone */} - + @@ -1960,7 +1983,7 @@ export const EventMilestones = () => ( {/* Removed from milestone */} - + @@ -1999,7 +2022,7 @@ export const EventAssignments = () => ( {/* Self-assigned */} - + @@ -2015,7 +2038,7 @@ export const EventAssignments = () => ( {/* Assigned someone else — assignee is a bold link, no avatar. */} - + @@ -2031,7 +2054,7 @@ export const EventAssignments = () => ( {/* Assigned multiple — joined with "and". */} - + @@ -2049,7 +2072,7 @@ export const EventAssignments = () => ( {/* Self-unassigned */} - + @@ -2065,7 +2088,7 @@ export const EventAssignments = () => ( {/* Unassigned someone else */} - + @@ -2081,7 +2104,7 @@ export const EventAssignments = () => ( {/* Unassigned multiple — joined with "and". */} - + @@ -2118,7 +2141,7 @@ export const EventLockUnlock = () => ( too heated). */} - + @@ -2128,7 +2151,7 @@ export const EventLockUnlock = () => ( - + @@ -2138,7 +2161,7 @@ export const EventLockUnlock = () => ( - + @@ -2148,7 +2171,7 @@ export const EventLockUnlock = () => ( - + @@ -2164,7 +2187,7 @@ export const EventLockUnlock = () => ( {/* Locked (no reason) */} - + @@ -2180,7 +2203,7 @@ export const EventLockUnlock = () => ( {/* Unlocked — UnlockIcon badge (not LockIcon). */} - + @@ -2211,7 +2234,7 @@ export const EventCommentDeleted = () => ( {/* Comment deleted */} - + @@ -2249,7 +2272,7 @@ export const EventCrossReferences = () => ( {/* Mentioned from an issue */} - + @@ -2274,7 +2297,7 @@ export const EventCrossReferences = () => ( {/* Mentioned from a pull request */} - + @@ -2299,7 +2322,7 @@ export const EventCrossReferences = () => ( {/* Linked a closing pull request */} - + diff --git a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx index 36d8dda8015..38dff3326fe 100644 --- a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx +++ b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx @@ -19,6 +19,7 @@ import {Button} from '../Button' import Link from '../Link' import Octicon from '../Octicon' import {EventSubRow, Examples, MutedTime, UserActor, VariantSection} from './internal/timelineStoryHelpers' +import {eventDataAttributesFor} from './taxonomy' import classes from './Timeline.license-compliance.features.stories.module.css' /** @@ -67,10 +68,21 @@ import classes from './Timeline.license-compliance.features.stories.module.css' * base `Timeline` component's own stories, and any docs-site representation is a * Phase 3 consideration via base-component story changes, out of scope here. * - * FUTURE FILTERING (taxonomy still open — github/primer#6663): category - * `data-*` attributes (e.g. `data-event-category="created"`) will attach to each - * `Timeline.Item` below so stories can be filtered/grouped by event family. We - * intentionally do NOT add them yet to avoid baking in a taxonomy. + * TAXONOMY `data-*` CONTRACT (proof-of-pattern pilot): every `Timeline.Item` + * below now carries the event `data-*` attributes projected from the merged + * taxonomy module (`./taxonomy`, primer/react#8180) — the single source of truth + * for Timeline event categorization (github/primer#6664, docs github/primer#6888). + * Each row spreads the output of the shared `eventDataAttributesFor` helper, + * which derives `category` / `visibility` FROM the catalog entry so the stories + * stay in sync with the catalog, and resolves `data-actor-type` at runtime from + * each row's rendered actor login. The contract per rendered `
  • `: `data-event-scope`, + * `data-event-type` (the UNSCOPED leaf), `data-event-category`, + * `data-event-visibility` (defaults `primary`), and `data-actor-type` (OMITTED + * when the row renders no actor, e.g. the synthetic `appeared_in_branch`). + * License Compliance is fully cataloged (all nine leaves), so nothing here is + * left untagged — there are no parked or shared events on this surface. This is + * the proof-of-pattern surface; the same approach fans out to the other four + * timeline surfaces next. * * PROOF-OF-PATTERN HISTORY: this file landed first as an `opened`-only * scaffold to validate the template, then grew to the full nine-group set. All @@ -140,6 +152,14 @@ const PolicyLink = ({href = '../../settings/security_analysis'}: {href?: string} ) +/** + * The stories below project each row's `data-*` via the shared + * `eventDataAttributesFor('license-compliance', , login?)` helper (see + * `./taxonomy`). `category` and `visibility` come from the catalog entry; + * `data-actor-type` resolves from the login and is omitted for actor-less rows. + * See github/primer#6664 and the taxonomy docs (github/primer#6888). + */ + export default { title: 'Components/Timeline/Events/License Compliance', component: Timeline, @@ -186,7 +206,7 @@ export const EventOpened = () => ( {/* Opened — license-compliance system bot, ShieldIcon on success (green) */} - + @@ -222,7 +242,7 @@ export const EventAppearedInBranch = () => ( badge, with a BranchName pill. PR sub-row is dormant (see group doc). */} - + @@ -262,7 +282,7 @@ export const EventReviewRequested = () => ( {/* Requested to close — no reason, no PR, no comment */} - + @@ -278,7 +298,7 @@ export const EventReviewRequested = () => ( {/* Requested to close as {reason}, with a requester comment sub-row */} - + @@ -296,7 +316,7 @@ export const EventReviewRequested = () => ( "Review request" button (latest request only) in Timeline.Actions. */} - + @@ -330,7 +350,7 @@ export const EventReviewApproved = () => ( {/* Approved closure request */} - + @@ -346,7 +366,7 @@ export const EventReviewApproved = () => ( {/* Approved closure request — with reviewer comment */} - + @@ -374,7 +394,7 @@ export const EventReviewDenied = () => ( {/* Denied closure request */} - + @@ -390,7 +410,7 @@ export const EventReviewDenied = () => ( {/* Denied closure request — with reviewer comment */} - + @@ -422,7 +442,9 @@ export const EventReviewExpired = () => ( {/* Request to close expired — license-compliance system bot, automatic expiry */} - + @@ -452,7 +474,7 @@ export const EventExceptionAdded = () => ( {/* Full shape — package + policy link + repo name */} - + @@ -470,7 +492,7 @@ export const EventExceptionAdded = () => ( {/* Fallback shape — body missing package info */} - + @@ -499,7 +521,7 @@ export const EventLicensesAdded = () => ( {/* Full shape — licenses list + policy link + repo name */} - + @@ -517,7 +539,7 @@ export const EventLicensesAdded = () => ( {/* Fallback shape — body missing licenses array */} - + @@ -550,7 +572,7 @@ export const EventClosed = () => ( {/* Closed as amendment — with a closing comment */} - + @@ -567,7 +589,7 @@ export const EventClosed = () => ( {/* Closed as private package */} - + @@ -583,7 +605,7 @@ export const EventClosed = () => ( {/* Closed as inaccurate license */} - + @@ -599,7 +621,7 @@ export const EventClosed = () => ( {/* Closed as policy edited */} - + @@ -615,7 +637,7 @@ export const EventClosed = () => ( {/* Closed as fixed */} - + @@ -631,7 +653,7 @@ export const EventClosed = () => ( {/* Closed as outdated — resolution Outdated */} - + @@ -647,7 +669,7 @@ export const EventClosed = () => ( {/* Closed this alert — default (no reason / resolution) */} - + diff --git a/packages/react/src/Timeline/Timeline.secret-scanning.features.stories.tsx b/packages/react/src/Timeline/Timeline.secret-scanning.features.stories.tsx index d9aa3afd1c8..2013d1a86c9 100644 --- a/packages/react/src/Timeline/Timeline.secret-scanning.features.stories.tsx +++ b/packages/react/src/Timeline/Timeline.secret-scanning.features.stories.tsx @@ -20,6 +20,7 @@ import { import {Button} from '../Button' import Octicon from '../Octicon' import {EventSubRow, MutedTime, RealisticTimeline, UserActor, VariantSection} from './internal/timelineStoryHelpers' +import {eventDataAttributesFor} from './taxonomy' /** * Secret Scanning alert Timeline event examples (Phase 2 of github/primer#6663). @@ -53,10 +54,19 @@ import {EventSubRow, MutedTime, RealisticTimeline, UserActor, VariantSection} fr * base `Timeline` component's own stories, and any docs-site representation is a * Phase 3 consideration via base-component story changes, out of scope here. * - * FUTURE FILTERING (taxonomy still open — github/primer#6663): category - * `data-*` attributes (e.g. `data-event-category="created"`) will attach to each - * `Timeline.Item` below so stories can be filtered/grouped by event family. We - * intentionally do NOT add them yet to avoid baking in a taxonomy. + * EVENT TAXONOMY TAGGING (github/primer#6664, Phase 3): each cataloged + * `Timeline.Item` below carries the `data-*` event contract + * (`data-event-scope` / `data-event-type` / `data-event-category` / + * `data-event-visibility` / `data-actor-type`), projected from the shared + * `SECRET_SCANNING_TAXONOMY` catalog via the `eventDataAttributesFor` helper. + * This mirrors + * the License Compliance pilot (primer/react#8216) and consumes the merged + * taxonomy module (#8180) documented in github/primer#6888. + * + * The catalog was scoped to the five live `switch (event.type)` cases and is + * NARROWER than this story set, so MANY variants have no catalog leaf and are + * left UNTAGGED on purpose (each such item carries a comment explaining why). + * We tag ONLY the seven cataloged leaves and never invent a value. * * SLOT USAGE (Phase 1 slots — same convention as the Issues / Dependabot groups): * - `Timeline.Avatar` (gutter slot, #6677): the 40px LEFT-GUTTER avatar. @@ -97,6 +107,12 @@ import {EventSubRow, MutedTime, RealisticTimeline, UserActor, VariantSection} fr const SIX7_AVATAR = 'https://avatars.githubusercontent.com/u/4548309?v=4' const HUBOT_AVATAR = 'https://avatars.githubusercontent.com/u/480938?v=4' +// The stories below project each row's `data-*` via the shared +// `eventDataAttributesFor('secret-scanning', , login?)` helper: `category` +// and `visibility` are read FROM the catalog so the story never re-declares the +// taxonomy; `login` is the actor rendered in the row, resolved to +// `data-actor-type`. Omit `login` for actor-less rows so none is emitted. + export default { title: 'Components/Timeline/Events/Secret Scanning', component: Timeline, @@ -137,7 +153,10 @@ export const EventCreated = () => ( {/* Created — GitHub system actor, ShieldIcon on success (green) */} - + {/* `detected` leaf. Renders the GitHub SYSTEM-IDENTITY actor + (``); `github` is in the bot set, so + actorTypeForLogin('GitHub') classifies it as 'bot' -> data-actor-type='bot'. */} + @@ -177,7 +196,8 @@ export const EventResolution = () => ( badge. Shown WITH an optional resolution-comment sub-row. */} - + {/* `closed` leaf (Resolution, any non-reopened resolution.type); user actor. */} + @@ -196,7 +216,8 @@ export const EventResolution = () => ( {/* Closed as false positive — gray (default) ShieldSlash badge. */} - + {/* `closed` leaf; user actor. */} + @@ -212,7 +233,8 @@ export const EventResolution = () => ( {/* Closed as won't fix */} - + {/* `closed` leaf; user actor. */} + @@ -228,7 +250,8 @@ export const EventResolution = () => ( {/* Closed as used in tests */} - + {/* `closed` leaf; user actor. */} + @@ -244,7 +267,8 @@ export const EventResolution = () => ( {/* Closed as pattern deleted */} - + {/* `closed` leaf; user actor. */} + @@ -260,7 +284,8 @@ export const EventResolution = () => ( {/* Closed as pattern edited */} - + {/* `closed` leaf; user actor. */} + @@ -276,7 +301,8 @@ export const EventResolution = () => ( {/* Closed as ignored by configuration (resolution type `hidden_by_config`) */} - + {/* `closed` leaf; user actor. */} + @@ -297,7 +323,9 @@ export const EventResolution = () => ( placement rather than leaving the Break as a stray first child. */} - + {/* Context row: a `closed` leaf preceding the reopen, tagged in place + like the seven dedicated "Closed as …" variants; user actor. */} + @@ -308,7 +336,8 @@ export const EventResolution = () => ( - + {/* `reopened` leaf (Resolution, resolution.type === 'reopened'); user actor. */} + @@ -338,7 +367,8 @@ export const EventBypass = () => ( {/* Bypassed — AlertIcon, default (gray) badge */} - + {/* `bypassed` leaf; user actor. */} + @@ -355,6 +385,11 @@ export const EventBypass = () => ( repo has delegated bypass enabled. */} + {/* UNTAGGED: the delegated-BYPASS request/approve flow has no catalog + leaf. The catalog's dismissal_requested / dismissal_reviewed cover the + delegated-CLOSURE flow, a different thing. Verified against the redesign + prototype (github/prototyping janmaarten-a11y/timeline-redesign-v1) as an + intentional exclusion. No leaf -> no data-* tag. */} @@ -371,6 +406,9 @@ export const EventBypass = () => ( {/* Bypass approved — CheckCircleIcon. Delegated bypass: gated as above. */} + {/* UNTAGGED: delegated-BYPASS flow, no catalog leaf (see the request + variant above), verified against the redesign prototype as an + intentional exclusion. No leaf -> no data-* tag. */} @@ -403,7 +441,8 @@ export const EventValidityChange = () => ( {/* Active — automated (GitHub), AlertIcon on danger (red) */} - + {/* `validity_changed` leaf; automated -> GitHub system actor -> 'bot'. */} + @@ -419,7 +458,8 @@ export const EventValidityChange = () => ( {/* Active — manual (user), same danger badge */} - + {/* `validity_changed` leaf; manual -> user actor. */} + @@ -435,7 +475,8 @@ export const EventValidityChange = () => ( {/* Inactive — automated (GitHub), SkipIcon on default (gray) */} - + {/* `validity_changed` leaf; automated -> GitHub system actor -> 'bot'. */} + @@ -451,7 +492,8 @@ export const EventValidityChange = () => ( {/* Inactive — manual (user) */} - + {/* `validity_changed` leaf; manual -> user actor. */} + @@ -467,7 +509,8 @@ export const EventValidityChange = () => ( {/* Unknown — automated (GitHub), AlertIcon on attention (amber) */} - + {/* `validity_changed` leaf; automated -> GitHub system actor -> 'bot'. */} + @@ -483,7 +526,8 @@ export const EventValidityChange = () => ( {/* Unknown — manual (user) */} - + {/* `validity_changed` leaf; manual -> user actor. */} + @@ -511,7 +555,10 @@ export const EventReport = () => ( {/* Reported — ShieldCheckIcon, default (gray) badge, user actor */} - + {/* `reported` leaf (findings). Live AlertTimeline.tsx `TimelineEventType.Report` + does NOT pass `isGitHubActor`, so the actor is the USER — resolves to + data-actor-type='user'. */} + @@ -545,6 +592,8 @@ export const EventMetadata = () => ( {/* Metadata created — GitHub system actor, FileAddedIcon, default (gray) badge */} + {/* UNTAGGED: MetadataCreated is PARKED per github/primer#6888 ('Parked' + section), no catalog leaf. No leaf -> no data-* tag. */} @@ -561,6 +610,8 @@ export const EventMetadata = () => ( {/* Metadata removed — GitHub system actor, FileRemovedIcon, default (gray) badge */} + {/* UNTAGGED: MetadataRemoved is PARKED per github/primer#6888 ('Parked' + section), no catalog leaf. No leaf -> no data-* tag. */} @@ -606,7 +657,8 @@ export const EventClosureRequest = () => ( un-bolded `resolutionText(exemption_request.reason)`. */} - + {/* `dismissal_requested` leaf (DelegatedClosureRequestOpened); user actor. */} + @@ -630,7 +682,8 @@ export const EventClosureRequest = () => ( {/* Requested — requester view: the invisible "Cancel request" button. */} - + {/* `dismissal_requested` leaf; user actor. */} + @@ -651,7 +704,8 @@ export const EventClosureRequest = () => ( {/* Approved — CheckCircleIcon, with a reviewer comment sub-row. */} - + {/* `dismissal_reviewed` leaf; reviewer user actor (six7). */} + @@ -670,7 +724,8 @@ export const EventClosureRequest = () => ( {/* Denied — XIcon */} - + {/* `dismissal_reviewed` leaf; reviewer user actor (six7). */} + @@ -689,6 +744,10 @@ export const EventClosureRequest = () => ( {/* Cancelled — SkipIcon */} + {/* UNTAGGED: Secret Scanning has NO dismissal_cancelled leaf (unlike + Dependabot); verified against the redesign prototype + (github/prototyping janmaarten-a11y/timeline-redesign-v1) as an + intentional exclusion. No leaf -> no data-* tag. */} @@ -717,6 +776,9 @@ export const EventClosureRequest = () => ( */ export const EventAssignment = () => ( + {/* UNTAGGED (whole group): Assignment is a cross-surface SHARED event, + deliberately kept OUT of the per-surface Secret Scanning catalog. All five + variants below carry no data-* event tag. */} {/* Self-assigned — actor === assignee */} diff --git a/packages/react/src/Timeline/taxonomy/core/index.ts b/packages/react/src/Timeline/taxonomy/core/index.ts new file mode 100644 index 00000000000..e763f37ff60 --- /dev/null +++ b/packages/react/src/Timeline/taxonomy/core/index.ts @@ -0,0 +1,10 @@ +/** + * Generic Timeline taxonomy core — product-agnostic entry point. + * + * Re-exports the shared types and the `data-*` serializer + factory. The + * GitHub-specific surfaces, categories, catalogs, and actor rules live in + * `../github` and build on this core. + */ + +export * from './types' +export * from './serializer' diff --git a/packages/react/src/Timeline/taxonomy/core/serializer.test.ts b/packages/react/src/Timeline/taxonomy/core/serializer.test.ts new file mode 100644 index 00000000000..6a545629875 --- /dev/null +++ b/packages/react/src/Timeline/taxonomy/core/serializer.test.ts @@ -0,0 +1,92 @@ +/** + * Tests for the generic `data-*` serializer and the `createEventDataAttributesFor` + * factory. Uses a small product-agnostic fixture catalog so the core stays + * decoupled from the GitHub-specific values. Covers actor resolution (bot / user + * / no-login), the `hasActor: false` suppression, `auditOnly` pass-through, and + * catalog-derived category. + */ + +import {describe, it, expect} from 'vitest' +import {toEventDataAttributes, createEventDataAttributesFor} from './serializer' +import type {ActorType, EventTaxonomyEntry} from './types' + +const FIXTURE_CATALOG = { + alpha: { + created: {category: 'lifecycle', hasActor: true}, + noted: {category: 'lifecycle', hasActor: false}, + archived: {category: 'housekeeping', visibility: 'auditOnly', hasActor: true}, + }, +} satisfies Record> + +const resolveActor = (login: string | undefined): ActorType => + login && login.toLowerCase().endsWith('[bot]') ? 'bot' : 'user' + +describe('toEventDataAttributes', () => { + it('serializes the axes with the unscoped type and defaults visibility to primary', () => { + const attrs = toEventDataAttributes({scope: 'alpha', type: 'created', category: 'lifecycle', actorType: 'user'}) + expect(attrs).toEqual({ + 'data-event-scope': 'alpha', + 'data-event-type': 'created', + 'data-event-category': 'lifecycle', + 'data-event-visibility': 'primary', + 'data-actor-type': 'user', + }) + }) + + it('respects an explicit auditOnly visibility', () => { + const attrs = toEventDataAttributes({ + scope: 'alpha', + type: 'archived', + category: 'housekeeping', + visibility: 'auditOnly', + }) + expect(attrs['data-event-visibility']).toBe('auditOnly') + }) + + it('omits data-actor-type for actor-less events rather than emitting empty', () => { + const attrs = toEventDataAttributes({scope: 'alpha', type: 'noted', category: 'lifecycle'}) + expect('data-actor-type' in attrs).toBe(false) + }) +}) + +describe('createEventDataAttributesFor', () => { + const attributesFor = createEventDataAttributesFor(FIXTURE_CATALOG, resolveActor) + + it('resolves a bot login to data-actor-type "bot"', () => { + const attrs = attributesFor('alpha', 'created', 'some-app[bot]') + expect(attrs['data-actor-type']).toBe('bot') + expect(attrs['data-event-scope']).toBe('alpha') + expect(attrs['data-event-type']).toBe('created') + }) + + it('resolves a plain user login to data-actor-type "user"', () => { + const attrs = attributesFor('alpha', 'created', 'monalisa') + expect(attrs['data-actor-type']).toBe('user') + }) + + it('omits data-actor-type entirely when no login is passed', () => { + const attrs = attributesFor('alpha', 'created') + expect('data-actor-type' in attrs).toBe(false) + }) + + it('never emits data-actor-type for a hasActor:false leaf, even with a login', () => { + const attrs = attributesFor('alpha', 'noted', 'monalisa') + expect('data-actor-type' in attrs).toBe(false) + }) + + it('passes through auditOnly visibility from the catalog entry', () => { + const attrs = attributesFor('alpha', 'archived', 'monalisa') + expect(attrs['data-event-visibility']).toBe('auditOnly') + }) + + it('derives data-event-category from the catalog entry', () => { + const attrs = attributesFor('alpha', 'archived') + expect(attrs['data-event-category']).toBe('housekeeping') + }) + + it('emits no data-actor-type when no resolveActor is provided', () => { + const attributesWithoutResolver = createEventDataAttributesFor(FIXTURE_CATALOG) + const attrs = attributesWithoutResolver('alpha', 'created', 'some-app[bot]') + expect('data-actor-type' in attrs).toBe(false) + }) +}) diff --git a/packages/react/src/Timeline/taxonomy/core/serializer.ts b/packages/react/src/Timeline/taxonomy/core/serializer.ts new file mode 100644 index 00000000000..8032990450f --- /dev/null +++ b/packages/react/src/Timeline/taxonomy/core/serializer.ts @@ -0,0 +1,98 @@ +/** + * Generic `data-*` serializer for the Timeline event contract (github/primer#6664). + * + * This is the product-agnostic core: it serializes an already-projected event + * input into the `data-*` attribute set, and offers a factory that binds a + * concrete catalog (and an optional login → actor resolver) into a per-catalog + * projection. The GitHub-specific values live in `../github`; nothing here knows + * about GitHub surfaces, categories, or bot logins. + * + * Not part of the public `@primer/react` export surface. + */ + +import type {ActorType, EventTaxonomyEntry, EventVisibility} from './types' + +/** Input for the `data-*` projection. Generic: `scope` and `category` are free strings. */ +export interface EventDataAttributeInput { + /** Owning surface — free-form so the core stays product-agnostic. */ + scope: string + /** Unscoped leaf type (e.g. `opened`). */ + type: string + /** Category family — free-form so the core stays product-agnostic. */ + category: string + visibility?: EventVisibility + /** Omit for actor-less events. */ + actorType?: ActorType +} + +/** The `data-*` attribute set emitted on a timeline event row. */ +export interface EventDataAttributes { + 'data-event-scope': string + 'data-event-type': string + 'data-event-category': string + 'data-event-visibility': EventVisibility + 'data-actor-type'?: ActorType +} + +/** + * Canonical serializer for the event `data-*` contract (primer#6664). The single + * place that turns an already-projected input object into the attribute set; a + * row renderer can delegate here so the contract has exactly one implementation. + * `data-event-type` is the **unscoped** leaf; the surface travels in + * `data-event-scope`. `data-actor-type` is omitted entirely for actor-less events + * rather than emitted empty. + */ +export function toEventDataAttributes({ + scope, + type, + category, + visibility, + actorType, +}: EventDataAttributeInput): EventDataAttributes { + const attributes: EventDataAttributes = { + 'data-event-scope': scope, + 'data-event-type': type, + 'data-event-category': category, + 'data-event-visibility': visibility ?? 'primary', + } + if (actorType) { + attributes['data-actor-type'] = actorType + } + return attributes +} + +/** + * Build a per-catalog `data-*` projection. Given a `catalog` of + * `scope → type → {@link EventTaxonomyEntry}` (and an optional `resolveActor` + * that maps a login to an {@link ActorType}), returns a + * `(scope, type, login?) => EventDataAttributes` function that: + * + * - reads `category` and `visibility` from the catalog entry, so a consumer + * never re-declares the taxonomy (`auditOnly` leaves keep their visibility); + * - emits `data-actor-type` only when the leaf is actor-capable + * (`hasActor: true`) AND a `login` is supplied AND a `resolveActor` was + * provided — a structurally actor-less leaf (`hasActor: false`) never emits + * `data-actor-type`, regardless of any login passed. + * + * The returned function keeps per-scope compile-time safety via mapped types: a + * `type` must be a real leaf of the given `scope`'s catalog. + */ +export function createEventDataAttributesFor>>( + catalog: Catalog, + resolveActor?: (login: string | undefined) => ActorType, +) { + return ( + scope: S, + type: keyof Catalog[S] & string, + login?: string, + ): EventDataAttributes => { + const entry = catalog[scope][type] + return toEventDataAttributes({ + scope, + type, + category: entry.category, + visibility: entry.visibility, + actorType: resolveActor && entry.hasActor && login ? resolveActor(login) : undefined, + }) + } +} diff --git a/packages/react/src/Timeline/taxonomy/core/types.ts b/packages/react/src/Timeline/taxonomy/core/types.ts new file mode 100644 index 00000000000..83511940786 --- /dev/null +++ b/packages/react/src/Timeline/taxonomy/core/types.ts @@ -0,0 +1,63 @@ +/** + * Generic Timeline event taxonomy — shared, product-agnostic types. + * + * This is the render-free, GitHub-agnostic core of the taxonomy: the shape of a + * catalog entry, the coarse actor classification, and the visibility facet. A + * product-specific layer (see `../github`) supplies the concrete surfaces, + * categories, and event catalogs; this core only describes the schema those + * values plug into. + * + * Not part of the public `@primer/react` export surface — it stays beside the + * Timeline component for the stories and the planned playground to consume. + */ + +/** + * How prominently an event surfaces by default. + * + * - `primary` — renders in the main timeline when its category is toggled on + * - `auditOnly` — never in the main timeline; only in the audit view + * + * (Conversation items are implicitly "always" — shown regardless of toggles.) + */ +export type EventVisibility = 'primary' | 'auditOnly' + +/** + * Coarse actor classification, surfaced as the `data-actor-type` attribute on + * event rows (mirrors the Primer Timeline `data-*` convention from + * github/primer#6664, alongside `data-event-type` / `data-event-scope`). + * + * "bot" covers automated actors; everything else is a human "user". This lets a + * filtering/grouping pass target automated vs. human activity declaratively. The + * mapping from a login to a value is product-specific (see `../github`). + */ +export type ActorType = 'user' | 'bot' + +/** + * Placement of one event on the non-identity axes (category + facets). Generic + * over the category union so a product layer can pin `category` to its own set + * (e.g. the GitHub `EventCategory`) while the core stays value-agnostic; the + * default `string` keeps the serializer and factory catalog-shape generic. + */ +export interface EventTaxonomyEntry { + /** + * Category family — axis L2. Drives `data-event-category` and any grouping the + * product layer offers. + */ + category: TCategory + /** + * Default density facet — `data-event-visibility`. Omit for `primary` (the + * common case); set `auditOnly` for events that only ever render in the audit + * view. + */ + visibility?: EventVisibility + /** + * Whether the event renders through the **actor-capable** path. `false` only + * for structurally actor-less events — these emit no `data-actor-type`. `true` + * means the row can carry an actor, but PRESENCE is data-driven, so an + * actor-capable event still renders actor-less when its payload has no actor. + * The concrete `user | bot` value is resolved at runtime from the actor login, + * never fixed by event type; `data-actor-type` is omitted whenever no actor is + * present. + */ + hasActor: boolean +} diff --git a/packages/react/src/Timeline/taxonomy/actorType.ts b/packages/react/src/Timeline/taxonomy/github/actorType.ts similarity index 53% rename from packages/react/src/Timeline/taxonomy/actorType.ts rename to packages/react/src/Timeline/taxonomy/github/actorType.ts index 521405f8dea..db633341f6b 100644 --- a/packages/react/src/Timeline/taxonomy/actorType.ts +++ b/packages/react/src/Timeline/taxonomy/github/actorType.ts @@ -6,24 +6,25 @@ * github/primer#6654, primer/react#8075 (License Compliance stories). */ +import type {ActorType} from '../core' + /** - * Coarse actor classification, surfaced as the `data-actor-type` attribute on - * event rows (mirrors the Primer Timeline `data-*` convention from - * github/primer#6664, alongside `data-event-type` / `data-event-scope`). - * - * "bot" covers GitHub apps and first-party automation (Dependabot, Actions, - * Copilot, Hubot) plus any `…[bot]` login; everything else is a human "user". - * This lets a filtering/grouping pass target automated vs. human activity - * declaratively — e.g. collapsing the system lifecycle on a security alert. + * GitHub's login → {@link ActorType} mapping. "bot" covers GitHub apps and + * first-party automation (Dependabot, Actions, Copilot, Hubot) plus any + * `…[bot]` login; everything else is a human "user". This lets a + * filtering/grouping pass target automated vs. human activity declaratively, + * e.g. collapsing the system lifecycle on a security alert. */ - -export type ActorType = 'user' | 'bot' - const BOT_LOGINS: ReadonlySet = new Set([ 'dependabot', 'dependabot-preview', 'github-actions', 'github-license-compliance', + // `github` is the platform system identity (e.g. Secret Scanning's automated + // detection/validity events). Classified as a bot so automated-vs-human + // filtering treats system activity like other automation; a distinct + // 'system' actor type is a possible future refinement. + 'github', 'copilot', 'hubot', ]) diff --git a/packages/react/src/Timeline/taxonomy/eventTaxonomy.test.ts b/packages/react/src/Timeline/taxonomy/github/catalogs.test.ts similarity index 99% rename from packages/react/src/Timeline/taxonomy/eventTaxonomy.test.ts rename to packages/react/src/Timeline/taxonomy/github/catalogs.test.ts index d442a1aa502..2994b916edd 100644 --- a/packages/react/src/Timeline/taxonomy/eventTaxonomy.test.ts +++ b/packages/react/src/Timeline/taxonomy/github/catalogs.test.ts @@ -13,12 +13,12 @@ import { taxonomyCategoriesMatchSurface, qualifyEventType, unqualifyEventType, - toEventDataAttributes, eventTypesByCategory, type LicenseComplianceEventType, type CodeScanningEventType, type CatalogedScope, -} from './eventTaxonomy' +} from './catalogs' +import {toEventDataAttributes} from '../core' import { SURFACE_CATEGORIES, ALL_TOGGLEABLE_CATEGORIES, @@ -270,7 +270,7 @@ describe('actorTypeForLogin', () => { }) it('classifies known first-party automation logins as bots', () => { - for (const login of ['dependabot', 'github-actions', 'github-license-compliance', 'copilot', 'hubot']) { + for (const login of ['dependabot', 'github-actions', 'github-license-compliance', 'github', 'copilot', 'hubot']) { expect(actorTypeForLogin(login)).toBe('bot') expect(actorTypeForLogin(login.toUpperCase())).toBe('bot') } diff --git a/packages/react/src/Timeline/taxonomy/eventTaxonomy.ts b/packages/react/src/Timeline/taxonomy/github/catalogs.ts similarity index 84% rename from packages/react/src/Timeline/taxonomy/eventTaxonomy.ts rename to packages/react/src/Timeline/taxonomy/github/catalogs.ts index 348910b4c6c..a15214fc76d 100644 --- a/packages/react/src/Timeline/taxonomy/eventTaxonomy.ts +++ b/packages/react/src/Timeline/taxonomy/github/catalogs.ts @@ -7,8 +7,13 @@ */ /** - * Timeline event taxonomy — the single source of truth for the redesigned + * GitHub Timeline event catalogs — the single source of truth for the redesigned * Primer Timeline event categorization, keyed by the `(scope, type)` composite. + * This file holds the GitHub per-surface event catalogs (the five `*_TAXONOMY` + * records and their combined {@link SURFACE_TAXONOMIES} view) together with the + * projections derived from them (`qualifyEventType`, `eventTypesByCategory`, + * `taxonomyCategoriesMatchSurface`), so the `catalogs` filename and the contents + * read consistently. * * The taxonomy has **five axes**. Three are hierarchical (the Figma * surface → category → event nesting, and the `data-*` spine): @@ -41,17 +46,10 @@ * the values the surfaces emit today rather than forking casing unilaterally. */ -import type {EventCategory, EventVisibility, ToggleableCategory} from './eventCategories' +import type {EventCategory, ToggleableCategory} from './eventCategories' import {SURFACE_CATEGORIES, isToggleableCategory} from './eventCategories' -import type {ActorType} from './actorType' -import type {TimelineSurface} from './surfaces' - -/** - * Owning surface of an event — axis L1. Identical to {@link TimelineSurface}: - * scope IS the surface. Aliased so taxonomy consumers read intent (`EventScope`) - * without coupling to the rendering-context type name. - */ -export type EventScope = TimelineSurface +import type {EventScope} from './surfaces' +import type {EventTaxonomyEntry} from '../core' /** * License Compliance leaf event types — axis L3, **unscoped** (the real @@ -75,38 +73,6 @@ export type LicenseComplianceEventType = | 'licenses_added' | 'closed' -/** Placement of one event on the non-identity axes (category + facets). */ -export interface EventTaxonomyEntry { - /** - * Category family — axis L2. Drives `data-event-category` and the Viewing-menu - * grouping. A {@link ToggleableCategory} the surface offers (see - * `SURFACE_CATEGORIES`), OR the always-audit `metadata` family (never - * toggleable, always `auditOnly` — labels, assignees, project fields). Use - * {@link taxonomyCategoriesMatchSurface} to assert a catalog only uses - * categories its surface actually offers. - */ - category: EventCategory - /** - * Default density facet — `data-event-visibility`. Omit for `primary` (the - * common case; every License Compliance event is primary because the audit-log - * surface renders one flat list with no curated/audit split). - */ - visibility?: EventVisibility - /** - * Whether the event renders through the **actor-capable** path. `false` only - * for structurally actor-less events (the synthetic `appeared_in_branch`, - * which upstream renders through `TimelineEventWithoutActor`) — these emit no - * `data-actor-type`. `true` means the row can carry an actor, but PRESENCE is - * data-driven: upstream `TimelineEventWithActor` renders the avatar only when - * `event.actor` exists, so a time-triggered event like `review_expired` is - * `true` yet renders actor-less when the payload has no actor. The concrete - * `user | bot` value is resolved at runtime from the actor login (see - * `actorTypeForLogin`), never fixed by event type; `data-actor-type` is omitted - * whenever no actor is present. - */ - hasActor: boolean -} - /** Convenience handle for the pilot surface. */ export const LICENSE_COMPLIANCE_SCOPE: EventScope = 'license-compliance' @@ -119,7 +85,7 @@ export const LICENSE_COMPLIANCE_SCOPE: EventScope = 'license-compliance' * pinned by the renderer as lifecycle bookends so filtering never empties the * record — a guarantee that lives in the renderer, not in this catalog. */ -export const LICENSE_COMPLIANCE_TAXONOMY: Record = { +export const LICENSE_COMPLIANCE_TAXONOMY: Record> = { opened: {category: 'findings', hasActor: true}, // synthetic; system-identity actor, rendered linked (no "bot" Label) appeared_in_branch: {category: 'findings', hasActor: false}, // synthetic; system, no actor review_requested: {category: 'reviews', hasActor: true}, @@ -151,15 +117,17 @@ export const LICENSE_COMPLIANCE_TAXONOMY: Record = { +export const SECRET_SCANNING_TAXONOMY: Record> = { detected: {category: 'findings', hasActor: true}, // `Creation`; system GitHub actor (isGitHubActor), rendered - validity_changed: {category: 'findings', hasActor: true}, // `Report` path; user actor + validity_changed: {category: 'findings', hasActor: true}, // `ValidityChange` (token verification status); system 'GitHub' actor when automated, user when manual + reported: {category: 'findings', hasActor: true}, // live AlertTimeline.tsx `TimelineEventType.Report`: "reported this secret"; user actor bypassed: {category: 'status', hasActor: true}, dismissal_requested: {category: 'reviews', hasActor: true}, // `DelegatedClosureRequestOpened` dismissal_reviewed: {category: 'reviews', hasActor: true}, @@ -198,7 +166,7 @@ export type CodeScanningEventType = | 'dismissal_requested' | 'dismissal_reviewed' -export const CODE_SCANNING_TAXONOMY: Record = { +export const CODE_SCANNING_TAXONOMY: Record> = { detected: {category: 'findings', hasActor: false}, // ALERT_CREATED; system, no actor appeared: {category: 'findings', hasActor: false}, // ALERT_APPEARED_IN_BRANCH; system, no actor reappeared: {category: 'findings', hasActor: false}, // ALERT_REAPPEARED; system, no actor @@ -245,7 +213,7 @@ export type DependabotEventType = | 'dismissal_reviewed' | 'dismissal_cancelled' -export const DEPENDABOT_TAXONOMY: Record = { +export const DEPENDABOT_TAXONOMY: Record> = { opened: {category: 'findings', hasActor: true}, // Dependabot bot actor (source/PR/push variants) fixed: {category: 'findings', hasActor: true}, // Dependabot bot actor dismissed: {category: 'status', hasActor: true}, // folds manual (user) + auto/rule-based (Dependabot bot) @@ -317,7 +285,7 @@ export type IssueEventType = | 'removed_from_project' | 'project_field_changed' -export const ISSUE_TAXONOMY: Record = { +export const ISSUE_TAXONOMY: Record> = { closed: {category: 'status', hasActor: true}, reopened: {category: 'status', hasActor: true}, pinned: {category: 'status', hasActor: true}, @@ -417,52 +385,6 @@ export function unqualifyEventType(scope: EventScope, flattenedType: string): st return remainder } -/** Input for the `data-*` projection. */ -export interface EventDataAttributeInput { - scope: EventScope - /** Unscoped leaf type (e.g. `opened`). */ - type: string - category: EventCategory - visibility?: EventVisibility - /** Omit for actor-less events. */ - actorType?: ActorType -} - -/** The `data-*` attribute set emitted on a timeline event row. */ -export interface EventDataAttributes { - 'data-event-scope': string - 'data-event-type': string - 'data-event-category': string - 'data-event-visibility': EventVisibility - 'data-actor-type'?: ActorType -} - -/** - * Canonical serializer for the event `data-*` contract (primer#6664). The single - * place that turns the five axes into attribute strings; a row renderer can - * delegate here so the contract has exactly one implementation. `data-event-type` - * is the **unscoped** leaf; the surface travels in `data-event-scope`. `data-actor-type` - * is omitted entirely for actor-less events rather than emitted empty. - */ -export function toEventDataAttributes({ - scope, - type, - category, - visibility, - actorType, -}: EventDataAttributeInput): EventDataAttributes { - const attributes: EventDataAttributes = { - 'data-event-scope': scope, - 'data-event-type': type, - 'data-event-category': category, - 'data-event-visibility': visibility ?? 'primary', - } - if (actorType) { - attributes['data-actor-type'] = actorType - } - return attributes -} - /** * Group a catalog's leaf types by category, preserving catalog order. This is * the projection that regroups the surface-level Storybook stories by category @@ -471,7 +393,7 @@ export function toEventDataAttributes({ * the catalog, never hand-maintained. */ export function eventTypesByCategory( - taxonomy: Record, + taxonomy: Record>, ): Partial> { const groups: Partial> = {} for (const type of Object.keys(taxonomy) as T[]) { @@ -492,7 +414,7 @@ export function eventTypesByCategory( */ export function taxonomyCategoriesMatchSurface( scope: CatalogedScope, - taxonomy: Record, + taxonomy: Record>, ): string[] { const offered = new Set(SURFACE_CATEGORIES[scope]) const mismatches: string[] = [] diff --git a/packages/react/src/Timeline/taxonomy/eventCategories.ts b/packages/react/src/Timeline/taxonomy/github/eventCategories.ts similarity index 93% rename from packages/react/src/Timeline/taxonomy/eventCategories.ts rename to packages/react/src/Timeline/taxonomy/github/eventCategories.ts index c3acfdc0900..21e224c1a25 100644 --- a/packages/react/src/Timeline/taxonomy/eventCategories.ts +++ b/packages/react/src/Timeline/taxonomy/github/eventCategories.ts @@ -68,16 +68,6 @@ export type ToggleableCategory = 'reviews' | 'merging' | 'status' | 'findings' | */ export type EventCategory = ToggleableCategory | 'conversation' | 'metadata' -/** - * How prominently an event surfaces by default. - * - * - `primary` — renders in the main timeline when its category is toggled on - * - `auditOnly` — never in the main timeline; only in the audit view - * - * (Conversation items are implicitly "always" — shown regardless of toggles.) - */ -export type EventVisibility = 'primary' | 'auditOnly' - /** * Which toggleable categories apply to each surface, in menu display order. * The Viewing menu only renders categories applicable to the current surface diff --git a/packages/react/src/Timeline/taxonomy/github/eventDataAttributesFor.test.ts b/packages/react/src/Timeline/taxonomy/github/eventDataAttributesFor.test.ts new file mode 100644 index 00000000000..178fe9d95af --- /dev/null +++ b/packages/react/src/Timeline/taxonomy/github/eventDataAttributesFor.test.ts @@ -0,0 +1,49 @@ +/** + * Tests for the consolidated `data-*` projection `eventDataAttributesFor` — the + * shared helper the per-surface Storybook stories consume. Covers actor + * resolution (bot vs user), actor omission, `auditOnly` pass-through, the + * `hasActor: false` actor suppression, and catalog-derived category. + */ + +import {describe, it, expect} from 'vitest' +import {eventDataAttributesFor} from './eventDataAttributesFor' + +describe('eventDataAttributesFor', () => { + it('resolves a bot login to data-actor-type "bot"', () => { + const attrs = eventDataAttributesFor('dependabot', 'opened', 'dependabot[bot]') + expect(attrs['data-actor-type']).toBe('bot') + expect(attrs['data-event-scope']).toBe('dependabot') + expect(attrs['data-event-type']).toBe('opened') + }) + + it('resolves a plain user login to data-actor-type "user"', () => { + const attrs = eventDataAttributesFor('issue', 'closed', 'monalisa') + expect(attrs['data-actor-type']).toBe('user') + }) + + it('omits data-actor-type entirely when no login is passed', () => { + const attrs = eventDataAttributesFor('issue', 'closed') + expect('data-actor-type' in attrs).toBe(false) + }) + + it('preserves auditOnly visibility for a metadata leaf', () => { + const attrs = eventDataAttributesFor('issue', 'labeled', 'monalisa') + expect(attrs['data-event-visibility']).toBe('auditOnly') + expect(attrs['data-event-category']).toBe('metadata') + }) + + it('defaults visibility to primary for a non-metadata leaf', () => { + const attrs = eventDataAttributesFor('license-compliance', 'review_requested', 'monalisa') + expect(attrs['data-event-visibility']).toBe('primary') + }) + + it('never emits data-actor-type for a hasActor:false leaf, even with a login', () => { + const attrs = eventDataAttributesFor('license-compliance', 'appeared_in_branch', 'monalisa') + expect('data-actor-type' in attrs).toBe(false) + }) + + it('derives data-event-category from the catalog entry', () => { + const attrs = eventDataAttributesFor('license-compliance', 'review_requested') + expect(attrs['data-event-category']).toBe('reviews') + }) +}) diff --git a/packages/react/src/Timeline/taxonomy/github/eventDataAttributesFor.ts b/packages/react/src/Timeline/taxonomy/github/eventDataAttributesFor.ts new file mode 100644 index 00000000000..c3456f73714 --- /dev/null +++ b/packages/react/src/Timeline/taxonomy/github/eventDataAttributesFor.ts @@ -0,0 +1,25 @@ +/** + * GitHub `data-*` projection for cataloged Timeline surfaces. + * + * One type-safe helper that replaces the per-surface inline wrappers the + * Storybook stories used to declare (`lcAttrs`, `codeScanningAttrs`, + * `dependabotAttrs`, `secretScanningAttrs`, `issueAttrs`). It is the generic + * {@link createEventDataAttributesFor} factory from the core, bound to the + * GitHub {@link SURFACE_TAXONOMIES} catalog and the GitHub + * {@link actorTypeForLogin} actor rule. Given a cataloged `scope` and one of + * that scope's leaf `type`s, it reads the catalog entry and serializes the event + * `data-*` contract (github/primer#6664); `category` and `visibility` come from + * the catalog, and `data-actor-type` resolves at runtime from the row's actor + * `login`. + * + * Signature: `eventDataAttributesFor(scope, type, login?)`. + * + * Not part of the public `@primer/react` export surface — this stays beside the + * Timeline component for stories and the planned playground to consume. + */ + +import {createEventDataAttributesFor} from '../core' +import {SURFACE_TAXONOMIES} from './catalogs' +import {actorTypeForLogin} from './actorType' + +export const eventDataAttributesFor = createEventDataAttributesFor(SURFACE_TAXONOMIES, actorTypeForLogin) diff --git a/packages/react/src/Timeline/taxonomy/github/index.ts b/packages/react/src/Timeline/taxonomy/github/index.ts new file mode 100644 index 00000000000..66d9da37f40 --- /dev/null +++ b/packages/react/src/Timeline/taxonomy/github/index.ts @@ -0,0 +1,13 @@ +/** + * GitHub-specific Timeline taxonomy — entry point. + * + * The GitHub product knowledge layer: the concrete surfaces, category model, + * per-surface event catalogs, actor-classification rule, and the bound + * `data-*` projection helper. Builds on the generic `../core`. + */ + +export * from './surfaces' +export * from './eventCategories' +export * from './catalogs' +export * from './actorType' +export * from './eventDataAttributesFor' diff --git a/packages/react/src/Timeline/taxonomy/surfaces.ts b/packages/react/src/Timeline/taxonomy/github/surfaces.ts similarity index 87% rename from packages/react/src/Timeline/taxonomy/surfaces.ts rename to packages/react/src/Timeline/taxonomy/github/surfaces.ts index 9634de12341..edd65baa5de 100644 --- a/packages/react/src/Timeline/taxonomy/surfaces.ts +++ b/packages/react/src/Timeline/taxonomy/github/surfaces.ts @@ -26,6 +26,13 @@ export type TimelineSurface = | 'secret-scanning' | 'license-compliance' +/** + * Owning surface of an event — axis L1. Identical to {@link TimelineSurface}: + * scope IS the surface. Aliased so taxonomy consumers read intent (`EventScope`) + * without coupling to the rendering-context type name. + */ +export type EventScope = TimelineSurface + /** * The security-alert surfaces — Dependabot alerts and the three scanning * surfaces (code scanning, secret scanning, license compliance). They share a diff --git a/packages/react/src/Timeline/taxonomy/index.ts b/packages/react/src/Timeline/taxonomy/index.ts index 560bdd7f50c..c8aede4632b 100644 --- a/packages/react/src/Timeline/taxonomy/index.ts +++ b/packages/react/src/Timeline/taxonomy/index.ts @@ -1,5 +1,5 @@ /** - * Timeline event taxonomy — public entry point. + * Timeline event taxonomy — module entry point. * * The single categorization model for the redesigned Primer Timeline, ported * from the Timeline redesign prototype (github/prototyping, @@ -12,9 +12,13 @@ * source beside the Timeline component so stories and the playground can consume * it. Promoting the projections (`toEventDataAttributes`, the catalogs) to the * package's public API is deferred until the model is ratified. + * + * The module is split into a generic, product-agnostic `./core` (the `data-*` + * schema, serializer, and factory) and a GitHub-specific `./github` (surfaces, + * categories, catalogs, actor rules, and the bound `eventDataAttributesFor` + * helper). This barrel re-exports both, so the full symbol set — including + * `eventDataAttributesFor` — stays available from `./taxonomy`. */ -export * from './surfaces' -export * from './actorType' -export * from './eventCategories' -export * from './eventTaxonomy' +export * from './core' +export * from './github'