@reviewflow/ui 0.0.0-stage → 6.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +95 -2
  3. package/package.json +102 -4
  4. package/src/board/bucket-section.tsx +94 -0
  5. package/src/board/dataSource.ts +44 -0
  6. package/src/board/facts.ts +183 -0
  7. package/src/board/index.ts +12 -0
  8. package/src/board/pull-request-board.tsx +239 -0
  9. package/src/board/pull-request-row.tsx +181 -0
  10. package/src/components/branch-diff/diff-themes.ts +6 -0
  11. package/src/components/branch-diff/diff-worker-pool.tsx +12 -0
  12. package/src/components/branch-diff/diff-worker-pool.web.tsx +56 -0
  13. package/src/components/branch-diff/diff-worker.ts +7 -0
  14. package/src/components/branch-diff/use-collapsed-files.ts +49 -0
  15. package/src/components/branch-diff/use-diff-settings.tsx +97 -0
  16. package/src/components/branch-diff/whitespace-button.tsx +32 -0
  17. package/src/components/copyable-code-block.tsx +101 -0
  18. package/src/components/label-pill.tsx +84 -0
  19. package/src/components/link-in-row.ts +6 -0
  20. package/src/components/markdown-suggestion.tsx +60 -0
  21. package/src/components/markdown-text.tsx +1067 -0
  22. package/src/components/pull-request-actions-merge.tsx +341 -0
  23. package/src/components/pull-request-actions-review.tsx +218 -0
  24. package/src/components/pull-request-checks.tsx +86 -0
  25. package/src/components/pull-request-commit-files.ts +143 -0
  26. package/src/components/pull-request-commits.tsx +221 -0
  27. package/src/components/pull-request-conversation-card.tsx +220 -0
  28. package/src/components/pull-request-conversation-edit.tsx +147 -0
  29. package/src/components/pull-request-conversation-event.tsx +267 -0
  30. package/src/components/pull-request-conversation-person.tsx +28 -0
  31. package/src/components/pull-request-conversation-thread.tsx +158 -0
  32. package/src/components/pull-request-diff-annotations.tsx +443 -0
  33. package/src/components/pull-request-diff-collapse.ts +43 -0
  34. package/src/components/pull-request-diff-data.ts +174 -0
  35. package/src/components/pull-request-diff-impacts-summary.tsx +97 -0
  36. package/src/components/pull-request-diff-impacts.ts +45 -0
  37. package/src/components/pull-request-diff-jump.ts +87 -0
  38. package/src/components/pull-request-diff-lines.ts +103 -0
  39. package/src/components/pull-request-diff-overview.tsx +416 -0
  40. package/src/components/pull-request-diff-view.tsx +438 -0
  41. package/src/components/pull-request-diff-viewer.tsx +1374 -0
  42. package/src/components/pull-request-files.tsx +533 -0
  43. package/src/components/pull-request-markdown-input.tsx +91 -0
  44. package/src/components/pull-request-panel.tsx +1270 -0
  45. package/src/components/pull-request-part-notice.tsx +82 -0
  46. package/src/components/pull-request-reactions.tsx +178 -0
  47. package/src/components/pull-request-review.tsx +164 -0
  48. package/src/components/pull-request-revisions.tsx +273 -0
  49. package/src/components/pull-request-screen.tsx +299 -0
  50. package/src/components/pull-request-suggestion-actions.tsx +201 -0
  51. package/src/components/pull-request-timeline.tsx +270 -0
  52. package/src/components/repository-attachment-banner.tsx +42 -0
  53. package/src/components/scroll-parent.ts +50 -0
  54. package/src/css.d.ts +1 -0
  55. package/src/dataSource.ts +242 -0
  56. package/src/editor/alerts.ts +105 -0
  57. package/src/editor/code-editor.native.tsx +25 -0
  58. package/src/editor/code-editor.tsx +112 -0
  59. package/src/editor/codeEditorExtensions.ts +65 -0
  60. package/src/editor/codeHighlight.ts +71 -0
  61. package/src/editor/commands.ts +175 -0
  62. package/src/editor/decorations.ts +425 -0
  63. package/src/editor/editable-file.tsx +224 -0
  64. package/src/editor/emojiCompletion.ts +110 -0
  65. package/src/editor/extensions.ts +149 -0
  66. package/src/editor/frontmatter.ts +76 -0
  67. package/src/editor/inlineFormat.ts +114 -0
  68. package/src/editor/lineTypes.ts +55 -0
  69. package/src/editor/markdown-editor.css +511 -0
  70. package/src/editor/markdown-editor.native.tsx +29 -0
  71. package/src/editor/markdown-editor.tsx +295 -0
  72. package/src/editor/nodeClasses.ts +69 -0
  73. package/src/index.ts +75 -0
  74. package/src/markdown-github/flatten.ts +49 -0
  75. package/src/markdown-github/html-tokens.ts +93 -0
  76. package/src/markdown-github/pair-html.ts +354 -0
  77. package/src/markdown-github/safe-url.ts +15 -0
  78. package/src/markdown-github/table-columns.ts +23 -0
  79. package/src/markdown-github/toggle-task.ts +164 -0
  80. package/src/nativewind-env.d.ts +1 -0
  81. package/src/prStatus.ts +90 -0
  82. package/src/services/PullRequestDraftsProvider.tsx +179 -0
  83. package/src/services/PullRequestReviewDraftProvider.tsx +178 -0
  84. package/src/services/PullRequestSuggestionBatchProvider.tsx +112 -0
  85. package/src/services/emojify.ts +13 -0
  86. package/src/services/errorToMessage.ts +6 -0
  87. package/src/services/expiringDraftStore.ts +102 -0
  88. package/src/services/formatDataAge.ts +12 -0
  89. package/src/services/formatRelativeDate.ts +56 -0
  90. package/src/services/githubImages.tsx +33 -0
  91. package/src/services/isViewerAuthor.ts +7 -0
  92. package/src/slots.tsx +34 -0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ Copyright (c) 2026 Christophe Hurpeau <302891+christophehurpeau@users.noreply.github.com> (https://christophe.hurpeau.com)
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for
4
+ any purpose with or without fee is hereby granted, provided that the
5
+ above copyright notice and this permission notice appear in all
6
+ copies.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
9
+ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
10
+ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
11
+ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
12
+ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
13
+ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
14
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,96 @@
1
- # Temporary Holding Version
1
+ # @reviewflow/ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React Native pull request screens from the
4
+ [reviewflow](https://github.com/christophehurpeau/reviewflow) local app:
5
+ conversation, checks, commits, files and diff, review and edits, plus the pull
6
+ request board. Built on [alouette](https://github.com/christophehurpeau/alouette)
7
+ (nativewind / tailwind), web first.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install @reviewflow/ui @reviewflow/sdk
13
+ ```
14
+
15
+ Peer dependencies your app provides: `alouette` `^25.0.0` (and what it
16
+ requires: `alouette-icons`, `nativewind`, `tailwindcss`, `react-native-svg`),
17
+ `react` `^19.2.3` and `react-native`. On web, `react-dom` too.
18
+
19
+ ## Shipped as sources
20
+
21
+ There is no build: every export resolves to a `.ts` / `.tsx` file under `src/`,
22
+ transformed by your bundler like your own files.
23
+
24
+ | Import | What |
25
+ | --------------------------------- | -------------------------------------------------------------------------------------------- |
26
+ | `@reviewflow/ui` | `PullRequestScreen`, its panels, the board, editors, data source and slot providers |
27
+ | `@reviewflow/ui/diff-worker-pool` | `DiffWorkerPool`: shiki highlighting in web workers (`browser` condition), a no-op elsewhere |
28
+
29
+ - **Metro** (Expo): nothing to configure beyond alouette's own
30
+ `withAlouetteConfig`; metro transforms TypeScript in `node_modules` and bundles
31
+ the diff worker from its `new Worker(new URL(...))` call.
32
+ - **Tailwind**: classes are only emitted for files tailwind scans. Add this
33
+ package's sources to your css entry, next to alouette's:
34
+
35
+ ```css
36
+ @import "alouette/core.css";
37
+ @source "../node_modules/@reviewflow/ui/src/**/*.{ts,tsx}";
38
+ ```
39
+
40
+ The path is relative to the css file; adjust it to where `node_modules` is.
41
+
42
+ - **TypeScript**: the sources import with a `.ts` extension, so set
43
+ `"allowImportingTsExtensions": true`. Your app's own ambient types must
44
+ cover what the sources rely on: `react-native-css/types` (`className` on
45
+ React Native components, which the nativewind setup already adds) and a
46
+ `declare module "*.css";`.
47
+ - `sideEffects` lists `*.css`: the editors import their stylesheet, and a
48
+ tree-shaking bundler must keep it.
49
+
50
+ ## Wiring
51
+
52
+ The screens read nothing on their own: you provide a data source.
53
+
54
+ ```tsx
55
+ import { AlouetteProvider } from "alouette";
56
+ import type { PullRequestDataSource } from "@reviewflow/ui";
57
+ import {
58
+ PullRequestDataSourceProvider,
59
+ PullRequestScreen,
60
+ } from "@reviewflow/ui";
61
+
62
+ const dataSource: PullRequestDataSource = {
63
+ usePart, // detail, checks, timeline, commits, files: a read state per part
64
+ useCandidates,
65
+ useRevisions,
66
+ useDiff,
67
+ useDiffs,
68
+ readFileAtSha,
69
+ edit,
70
+ reload,
71
+ // optional: useSummary, useRepositoryAttachment, useReviewflowPr, useReadingOrder, useImpacts
72
+ };
73
+
74
+ <AlouetteProvider themeVariables={themeVariables}>
75
+ <PullRequestDataSourceProvider value={dataSource}>
76
+ <PullRequestScreen
77
+ ownerRepo="octocat/hello-world"
78
+ number={1}
79
+ tab={tab}
80
+ onTabChange={setTab}
81
+ />
82
+ </PullRequestDataSourceProvider>
83
+ </AlouetteProvider>;
84
+ ```
85
+
86
+ Every hook answers with a `PullRequestReadState` (`value`, `cached`,
87
+ `reading`, `error`); the payload types come from `@reviewflow/sdk`, so a
88
+ source is usually `fetchPullRequestPart` and `readPullRequestDiff` from the sdk
89
+ behind your own cache and server. The board takes a `BoardDataSource`
90
+ (`useBoard`, `useReviewflowStatus`, `refresh`) through
91
+ `BoardDataSourceProvider`. `PullRequestSlotsProvider` (or the `slots` prop)
92
+ replaces the markdown editor or adds an action to the "no checkout" notice.
93
+
94
+ ## License
95
+
96
+ ISC
package/package.json CHANGED
@@ -1,6 +1,104 @@
1
1
  {
2
2
  "name": "@reviewflow/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "6.21.0",
4
+ "description": "react-native pull request screen components. Consumed as SOURCES (no build): exports resolve `default` to ./src/index.ts, which metro transforms like the app's own files; the consuming app's css must @source this package's src so tailwind keeps its classes.",
5
+ "keywords": [
6
+ "alouette",
7
+ "code-review",
8
+ "expo",
9
+ "github",
10
+ "pull-request",
11
+ "react-native",
12
+ "reviewflow"
13
+ ],
14
+ "homepage": "https://github.com/christophehurpeau/reviewflow",
15
+ "bugs": {
16
+ "url": "https://github.com/christophehurpeau/reviewflow/issues"
17
+ },
18
+ "license": "ISC",
19
+ "author": "Christophe Hurpeau <302891+christophehurpeau@users.noreply.github.com> (https://christophe.hurpeau.com)",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/christophehurpeau/reviewflow.git",
23
+ "directory": "packages/ui"
24
+ },
25
+ "files": [
26
+ "src",
27
+ "!src/**/*.test.ts",
28
+ "!src/**/*.stories.tsx",
29
+ "!src/stories",
30
+ "!src/board/fixtures.ts",
31
+ "!src/board/mockDataSource.tsx"
32
+ ],
33
+ "type": "module",
34
+ "sideEffects": [
35
+ "*.css"
36
+ ],
37
+ "main": "./src/index.ts",
38
+ "types": "./src/index.ts",
39
+ "exports": {
40
+ "./package.json": "./package.json",
41
+ ".": {
42
+ "types": "./src/index.ts",
43
+ "default": "./src/index.ts"
44
+ },
45
+ "./diff-worker-pool": {
46
+ "types": "./src/components/branch-diff/diff-worker-pool.tsx",
47
+ "browser": "./src/components/branch-diff/diff-worker-pool.web.tsx",
48
+ "default": "./src/components/branch-diff/diff-worker-pool.tsx"
49
+ }
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "scripts": {
55
+ "lint": "pnpm run lint:oxlint && pnpm run lint:eslint",
56
+ "lint:eslint": "pnpm --dir '../..' exec eslint --quiet 'packages/ui'",
57
+ "lint:oxlint": "pnpm --dir '../..' exec oxlint --quiet 'packages/ui'",
58
+ "tsc": "tsc"
59
+ },
60
+ "dependencies": {
61
+ "@codemirror/autocomplete": "6.20.3",
62
+ "@codemirror/commands": "6.11.1",
63
+ "@codemirror/lang-css": "6.3.1",
64
+ "@codemirror/lang-html": "6.4.12",
65
+ "@codemirror/lang-javascript": "6.2.5",
66
+ "@codemirror/lang-json": "6.0.2",
67
+ "@codemirror/lang-markdown": "6.5.2",
68
+ "@codemirror/lang-yaml": "6.1.3",
69
+ "@codemirror/language": "6.12.4",
70
+ "@codemirror/search": "6.7.2",
71
+ "@codemirror/state": "6.7.5",
72
+ "@codemirror/view": "6.43.12",
73
+ "@lezer/common": "1.5.2",
74
+ "@lezer/highlight": "1.2.3",
75
+ "@lezer/markdown": "1.7.2",
76
+ "@pierre/diffs": "1.5.2",
77
+ "@reviewflow/sdk": "workspace:*",
78
+ "@tanstack/markdown": "0.0.13",
79
+ "alouette-icons": "12.0.0",
80
+ "gemoji": "8.1.0",
81
+ "htmlparser2": "12.0.0"
82
+ },
83
+ "devDependencies": {
84
+ "@storybook/react-native": "10.6.0",
85
+ "@types/react": "19.2.18",
86
+ "@typescript/native": "npm:typescript@7.0.2",
87
+ "alouette": "25.0.1",
88
+ "react": "19.2.8",
89
+ "react-native": "0.86.2",
90
+ "react-native-safe-area-context": "5.8.0",
91
+ "react-native-svg": "15.15.5",
92
+ "storybook": "10.6.5",
93
+ "typescript": "npm:@typescript/typescript6@6.0.2"
94
+ },
95
+ "peerDependencies": {
96
+ "alouette": "^25.0.0",
97
+ "react": "^19.2.3",
98
+ "react-native": "*",
99
+ "react-native-svg": "*"
100
+ },
101
+ "engines": {
102
+ "node": ">=22.18.0"
103
+ }
104
+ }
@@ -0,0 +1,94 @@
1
+ import type { PullRequestRef, ReviewflowBoardBucket } from "@reviewflow/sdk";
2
+ import type { Accent, SVGIconElement } from "alouette";
3
+ import { SectionTitle, Text, View } from "alouette";
4
+ import { ArrowsClockwiseRegularIcon } from "alouette-icons/phosphor-icons/ArrowsClockwise";
5
+ import { ChatCircleTextRegularIcon } from "alouette-icons/phosphor-icons/ChatCircleText";
6
+ import { EyeRegularIcon } from "alouette-icons/phosphor-icons/Eye";
7
+ import { GitMergeRegularIcon } from "alouette-icons/phosphor-icons/GitMerge";
8
+ import { GitPullRequestRegularIcon } from "alouette-icons/phosphor-icons/GitPullRequest";
9
+ import { PencilSimpleLineRegularIcon } from "alouette-icons/phosphor-icons/PencilSimpleLine";
10
+ import { UserPlusRegularIcon } from "alouette-icons/phosphor-icons/UserPlus";
11
+ import { UsersRegularIcon } from "alouette-icons/phosphor-icons/Users";
12
+ import type { ReactNode } from "react";
13
+ import type { BoardRowInfo } from "./pull-request-row.tsx";
14
+ import { PullRequestRow } from "./pull-request-row.tsx";
15
+
16
+ /**
17
+ * Glyphs and accents for the buckets reviewflow ships today, in the pull
18
+ * request badge's colours where they mean the same thing. A bucket it adds
19
+ * later gets the generic glyph and no accent — its title is still reviewflow's.
20
+ */
21
+ const icons: Partial<Record<string, SVGIconElement>> = {
22
+ "re-requested-reviews": <ArrowsClockwiseRegularIcon />,
23
+ "requested-reviews": <EyeRegularIcon />,
24
+ "ready-to-merge": <GitMergeRegularIcon />,
25
+ "changes-requested": <ChatCircleTextRegularIcon />,
26
+ "opened-missing-review-request": <UserPlusRegularIcon />,
27
+ drafts: <PencilSimpleLineRegularIcon />,
28
+ "waiting-for-review": <UsersRegularIcon />,
29
+ };
30
+
31
+ const accents: Partial<Record<string, Accent>> = {
32
+ "re-requested-reviews": "warning",
33
+ "requested-reviews": "warning",
34
+ "ready-to-merge": "success",
35
+ "changes-requested": "danger",
36
+ "opened-missing-review-request": "warning",
37
+ "waiting-for-review": "info",
38
+ };
39
+
40
+ export interface BoardBucketSectionProps {
41
+ bucket: ReviewflowBoardBucket;
42
+ groupKey: string;
43
+ viewer: string | null;
44
+ onOpenPullRequest: (ref: PullRequestRef) => void;
45
+ renderRowExtra?: (row: BoardRowInfo) => ReactNode;
46
+ /** Replaces the glyph looked up from a reviewflow bucket key: an app's own section has none. */
47
+ icon?: SVGIconElement;
48
+ /** Replaces the accent looked up from a reviewflow bucket key. */
49
+ accent?: Accent;
50
+ }
51
+
52
+ /**
53
+ * One reviewflow bucket on the screen ground: its title, its rows, and its size
54
+ * in reviewflow when the pages read stop short of it.
55
+ */
56
+ export function BoardBucketSection({
57
+ bucket,
58
+ groupKey,
59
+ viewer,
60
+ onOpenPullRequest,
61
+ renderRowExtra,
62
+ icon,
63
+ accent,
64
+ }: BoardBucketSectionProps): ReactNode {
65
+ const shown = bucket.prs.length;
66
+
67
+ return (
68
+ <View className="gap-xs">
69
+ <SectionTitle
70
+ level={3}
71
+ icon={icon ?? icons[bucket.key] ?? <GitPullRequestRegularIcon />}
72
+ accent={accent ?? accents[bucket.key]}
73
+ >
74
+ {bucket.title}
75
+ </SectionTitle>
76
+ <View className="gap-xs">
77
+ {bucket.prs.map((pr) => (
78
+ <PullRequestRow
79
+ key={pr.id}
80
+ row={{ pr, bucketKey: bucket.key, groupKey }}
81
+ viewer={viewer}
82
+ renderRowExtra={renderRowExtra}
83
+ onOpenPullRequest={onOpenPullRequest}
84
+ />
85
+ ))}
86
+ </View>
87
+ {bucket.total > shown ? (
88
+ <Text className="text-sm text-muted">
89
+ {`Showing ${String(shown)} of ${String(bucket.total)}`}
90
+ </Text>
91
+ ) : null}
92
+ </View>
93
+ );
94
+ }
@@ -0,0 +1,44 @@
1
+ import type {
2
+ ReviewflowBoardResponse,
3
+ ReviewflowConnectionStatus,
4
+ } from "@reviewflow/sdk";
5
+ import { createContext, use } from "react";
6
+
7
+ /**
8
+ * Where a read is at: `value` is the newest answer held, `null` until the
9
+ * first; `cached` says it is older than the request being answered; `reading`
10
+ * that the source is reading now; `error` why the last read failed (`value`
11
+ * may still hold an older answer).
12
+ */
13
+ export interface BoardReadState<T> {
14
+ value: T | null;
15
+ reading: boolean;
16
+ error: string | null;
17
+ cached?: boolean;
18
+ }
19
+
20
+ /**
21
+ * What the board reads through. The app provides one with
22
+ * {@link BoardDataSourceProvider}; the components never reach for a network or
23
+ * a cache themselves. Hooks follow the rules of hooks.
24
+ */
25
+ export interface BoardDataSource {
26
+ /** The board; the source keeps it current (live events) and re-renders on change. */
27
+ useBoard: () => BoardReadState<ReviewflowBoardResponse>;
28
+ /** Whether a reviewflow token is configured. `value` is null until known. */
29
+ useReviewflowStatus: () => BoardReadState<ReviewflowConnectionStatus>;
30
+ /** Reads the board from reviewflow again; never rejects, a failure is `useBoard().error`. */
31
+ refresh: () => Promise<void>;
32
+ }
33
+
34
+ const BoardDataSourceContext = createContext<BoardDataSource | null>(null);
35
+
36
+ export const BoardDataSourceProvider = BoardDataSourceContext.Provider;
37
+
38
+ export const useBoardDataSource = (): BoardDataSource => {
39
+ const source = use(BoardDataSourceContext);
40
+ if (source === null) {
41
+ throw new Error("useBoardDataSource needs a BoardDataSourceProvider");
42
+ }
43
+ return source;
44
+ };
@@ -0,0 +1,183 @@
1
+ import type { ReviewflowBoardPr, ReviewflowPrChanges } from "@reviewflow/sdk";
2
+ import type { FormatRelativeDateOptions } from "../services/formatRelativeDate.ts";
3
+ import { formatRelativeDate } from "../services/formatRelativeDate.ts";
4
+
5
+ /**
6
+ * What reviewflow's own board row says about a pull request, in its wording so
7
+ * the two boards read the same. The status line comes in three parts by how
8
+ * loudly each is shown: what blocks the merge, changes requested, the rest.
9
+ */
10
+
11
+ export const maxFailedCheckNames = 9;
12
+
13
+ /** Where a row is shown, which decides what it leaves to its bucket's title. */
14
+ export interface ReviewflowRowContext {
15
+ bucketKey: string;
16
+ groupKey: string;
17
+ /** the token's login, named "you" */
18
+ viewer: string | null;
19
+ }
20
+
21
+ /** the buckets whose title already says whether the checks pass */
22
+ const bucketsSilentOnPassedChecks = new Set(["changes-requested", "drafts"]);
23
+ /** the bucket whose title already says every reviewer was asked again */
24
+ const reRequestedBucket = "re-requested-reviews";
25
+ /** someone else's pull request requests a review, the operator's awaits one */
26
+ const requestingAttentionGroup = "requesting-attention";
27
+
28
+ const counted = (count: number, one: string, many: string): string =>
29
+ `${String(count)} ${count === 1 ? one : many}`;
30
+
31
+ const people = (logins: string[], viewer: string | null): string =>
32
+ logins.map((login) => (login === viewer ? "you" : `@${login}`)).join(", ");
33
+
34
+ const failedChecksText = ({
35
+ checksConclusion,
36
+ failedChecks,
37
+ }: Pick<ReviewflowBoardPr, "checksConclusion" | "failedChecks">):
38
+ | string
39
+ | null => {
40
+ if (failedChecks.length === 0) {
41
+ return checksConclusion === "failed" ? "checks failed" : null;
42
+ }
43
+ const names = failedChecks.slice(0, maxFailedCheckNames).join(", ");
44
+ const remaining = failedChecks.length - maxFailedCheckNames;
45
+ const more = remaining > 0 ? ` +${String(remaining)}` : "";
46
+ return `${failedChecks.length === 1 ? "check" : "checks"} failed: ${names}${more}`;
47
+ };
48
+
49
+ /**
50
+ * Failing checks (after reviewflow's allowed-to-fail rules, by name), a failed
51
+ * pr lint and a merge conflict; null when nothing blocks.
52
+ */
53
+ export const reviewflowPrFailure = (
54
+ pr: Pick<
55
+ ReviewflowBoardPr,
56
+ "checksConclusion" | "failedChecks" | "lintFailed" | "mergeable"
57
+ >,
58
+ ): string | null => {
59
+ const parts = [
60
+ failedChecksText(pr),
61
+ pr.lintFailed ? "pr lint failed" : null,
62
+ pr.mergeable === "conflicting" ? "conflicts" : null,
63
+ ].filter((part) => part !== null);
64
+ return parts.length === 0 ? null : parts.join(" · ");
65
+ };
66
+
67
+ export const reviewflowPrChangesRequested = (
68
+ { changesRequestedBy }: Pick<ReviewflowBoardPr, "changesRequestedBy">,
69
+ viewer: string | null,
70
+ ): string | null =>
71
+ changesRequestedBy.length === 0
72
+ ? null
73
+ : `changes requested by ${people(changesRequestedBy, viewer)}`;
74
+
75
+ const checksProgressText = (
76
+ {
77
+ checksConclusion,
78
+ checksRunning,
79
+ }: Pick<ReviewflowBoardPr, "checksConclusion" | "checksRunning">,
80
+ bucketKey: string,
81
+ ): string | null => {
82
+ if (checksRunning > 0) {
83
+ return `${counted(checksRunning, "check", "checks")} running`;
84
+ }
85
+ return checksConclusion === "passed" &&
86
+ !bucketsSilentOnPassedChecks.has(bucketKey)
87
+ ? "checks passed"
88
+ : null;
89
+ };
90
+
91
+ const awaitedText = (
92
+ {
93
+ requestedReviewers,
94
+ reRequestedReviewers,
95
+ requestedTeams,
96
+ }: Pick<
97
+ ReviewflowBoardPr,
98
+ "requestedReviewers" | "requestedTeams" | "reRequestedReviewers"
99
+ >,
100
+ { bucketKey, groupKey, viewer }: ReviewflowRowContext,
101
+ ): string | null => {
102
+ const reviewers = [
103
+ ...requestedReviewers,
104
+ ...(bucketKey === reRequestedBucket ? [] : reRequestedReviewers),
105
+ ];
106
+ if (reviewers.length === 0 && requestedTeams.length === 0) return null;
107
+ const verb = groupKey === requestingAttentionGroup ? "requested" : "awaiting";
108
+ const awaited = [
109
+ ...(reviewers.length === 0 ? [] : [people(reviewers, viewer)]),
110
+ ...requestedTeams.map((team) => `#${team}`),
111
+ ];
112
+ return `${verb} ${awaited.join(", ")}`;
113
+ };
114
+
115
+ /** running or passed checks, approvals, open threads, awaited reviewers */
116
+ export const reviewflowPrProgress = (
117
+ pr: Pick<
118
+ ReviewflowBoardPr,
119
+ | "approvedBy"
120
+ | "checksConclusion"
121
+ | "checksRunning"
122
+ | "requestedReviewers"
123
+ | "requestedTeams"
124
+ | "reRequestedReviewers"
125
+ | "unresolvedThreads"
126
+ >,
127
+ context: ReviewflowRowContext,
128
+ ): string | null => {
129
+ const parts = [
130
+ checksProgressText(pr, context.bucketKey),
131
+ pr.approvedBy.length === 0
132
+ ? null
133
+ : `approved by ${people(pr.approvedBy, context.viewer)}`,
134
+ pr.unresolvedThreads
135
+ ? counted(pr.unresolvedThreads, "unresolved thread", "unresolved threads")
136
+ : null,
137
+ awaitedText(pr, context),
138
+ ].filter((part) => part !== null);
139
+ return parts.length === 0 ? null : parts.join(" · ");
140
+ };
141
+
142
+ /**
143
+ * "by @author", "assigned to @a", or both. An author who is their own only
144
+ * assignee is a byline, not an assignment; a pull request whose only person is
145
+ * the viewer names nobody.
146
+ */
147
+ export const reviewflowPrOwners = (
148
+ { author, assignees }: Pick<ReviewflowBoardPr, "assignees" | "author">,
149
+ viewer: string | null,
150
+ ): string | null => {
151
+ const named = [...(author === null ? [] : [author]), ...assignees];
152
+ if (named.length === 0) return null;
153
+ if (viewer !== null && named.every((login) => login === viewer)) return null;
154
+
155
+ const byline = author === null ? null : `by ${people([author], viewer)}`;
156
+ if (assignees.length === 0) return byline;
157
+ const assignment = `assigned to ${people(assignees, viewer)}`;
158
+
159
+ if (author === null || !assignees.includes(author)) {
160
+ return byline === null ? assignment : `${byline} · ${assignment}`;
161
+ }
162
+ // naming every assignee already names the author among them
163
+ return assignees.length === 1 ? byline : assignment;
164
+ };
165
+
166
+ export const reviewflowPrFlowDate = (
167
+ { openedAt, approvedAt }: Pick<ReviewflowBoardPr, "approvedAt" | "openedAt">,
168
+ options?: FormatRelativeDateOptions,
169
+ ): string | null => {
170
+ if (approvedAt !== null) {
171
+ return `approved ${formatRelativeDate(approvedAt, options)}`;
172
+ }
173
+ return openedAt === null
174
+ ? null
175
+ : `opened ${formatRelativeDate(openedAt, options)}`;
176
+ };
177
+
178
+ export const formatPrChanges = ({
179
+ changedFiles,
180
+ additions,
181
+ deletions,
182
+ }: ReviewflowPrChanges): string =>
183
+ `${counted(changedFiles, "file", "files")} +${String(additions)} −${String(deletions)}`;
@@ -0,0 +1,12 @@
1
+ export type { BoardDataSource, BoardReadState } from "./dataSource.ts";
2
+ export { BoardDataSourceProvider, useBoardDataSource } from "./dataSource.ts";
3
+ export type {
4
+ PullRequestBoardExtraSection,
5
+ PullRequestBoardProps,
6
+ } from "./pull-request-board.tsx";
7
+ export {
8
+ PullRequestBoard,
9
+ extraSectionGroupKey,
10
+ } from "./pull-request-board.tsx";
11
+ export type { BoardRowInfo, PullRequestRowProps } from "./pull-request-row.tsx";
12
+ export { PullRequestRow } from "./pull-request-row.tsx";