@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.
- package/LICENSE +15 -0
- package/README.md +95 -2
- package/package.json +102 -4
- package/src/board/bucket-section.tsx +94 -0
- package/src/board/dataSource.ts +44 -0
- package/src/board/facts.ts +183 -0
- package/src/board/index.ts +12 -0
- package/src/board/pull-request-board.tsx +239 -0
- package/src/board/pull-request-row.tsx +181 -0
- package/src/components/branch-diff/diff-themes.ts +6 -0
- package/src/components/branch-diff/diff-worker-pool.tsx +12 -0
- package/src/components/branch-diff/diff-worker-pool.web.tsx +56 -0
- package/src/components/branch-diff/diff-worker.ts +7 -0
- package/src/components/branch-diff/use-collapsed-files.ts +49 -0
- package/src/components/branch-diff/use-diff-settings.tsx +97 -0
- package/src/components/branch-diff/whitespace-button.tsx +32 -0
- package/src/components/copyable-code-block.tsx +101 -0
- package/src/components/label-pill.tsx +84 -0
- package/src/components/link-in-row.ts +6 -0
- package/src/components/markdown-suggestion.tsx +60 -0
- package/src/components/markdown-text.tsx +1067 -0
- package/src/components/pull-request-actions-merge.tsx +341 -0
- package/src/components/pull-request-actions-review.tsx +218 -0
- package/src/components/pull-request-checks.tsx +86 -0
- package/src/components/pull-request-commit-files.ts +143 -0
- package/src/components/pull-request-commits.tsx +221 -0
- package/src/components/pull-request-conversation-card.tsx +220 -0
- package/src/components/pull-request-conversation-edit.tsx +147 -0
- package/src/components/pull-request-conversation-event.tsx +267 -0
- package/src/components/pull-request-conversation-person.tsx +28 -0
- package/src/components/pull-request-conversation-thread.tsx +158 -0
- package/src/components/pull-request-diff-annotations.tsx +443 -0
- package/src/components/pull-request-diff-collapse.ts +43 -0
- package/src/components/pull-request-diff-data.ts +174 -0
- package/src/components/pull-request-diff-impacts-summary.tsx +97 -0
- package/src/components/pull-request-diff-impacts.ts +45 -0
- package/src/components/pull-request-diff-jump.ts +87 -0
- package/src/components/pull-request-diff-lines.ts +103 -0
- package/src/components/pull-request-diff-overview.tsx +416 -0
- package/src/components/pull-request-diff-view.tsx +438 -0
- package/src/components/pull-request-diff-viewer.tsx +1374 -0
- package/src/components/pull-request-files.tsx +533 -0
- package/src/components/pull-request-markdown-input.tsx +91 -0
- package/src/components/pull-request-panel.tsx +1270 -0
- package/src/components/pull-request-part-notice.tsx +82 -0
- package/src/components/pull-request-reactions.tsx +178 -0
- package/src/components/pull-request-review.tsx +164 -0
- package/src/components/pull-request-revisions.tsx +273 -0
- package/src/components/pull-request-screen.tsx +299 -0
- package/src/components/pull-request-suggestion-actions.tsx +201 -0
- package/src/components/pull-request-timeline.tsx +270 -0
- package/src/components/repository-attachment-banner.tsx +42 -0
- package/src/components/scroll-parent.ts +50 -0
- package/src/css.d.ts +1 -0
- package/src/dataSource.ts +242 -0
- package/src/editor/alerts.ts +105 -0
- package/src/editor/code-editor.native.tsx +25 -0
- package/src/editor/code-editor.tsx +112 -0
- package/src/editor/codeEditorExtensions.ts +65 -0
- package/src/editor/codeHighlight.ts +71 -0
- package/src/editor/commands.ts +175 -0
- package/src/editor/decorations.ts +425 -0
- package/src/editor/editable-file.tsx +224 -0
- package/src/editor/emojiCompletion.ts +110 -0
- package/src/editor/extensions.ts +149 -0
- package/src/editor/frontmatter.ts +76 -0
- package/src/editor/inlineFormat.ts +114 -0
- package/src/editor/lineTypes.ts +55 -0
- package/src/editor/markdown-editor.css +511 -0
- package/src/editor/markdown-editor.native.tsx +29 -0
- package/src/editor/markdown-editor.tsx +295 -0
- package/src/editor/nodeClasses.ts +69 -0
- package/src/index.ts +75 -0
- package/src/markdown-github/flatten.ts +49 -0
- package/src/markdown-github/html-tokens.ts +93 -0
- package/src/markdown-github/pair-html.ts +354 -0
- package/src/markdown-github/safe-url.ts +15 -0
- package/src/markdown-github/table-columns.ts +23 -0
- package/src/markdown-github/toggle-task.ts +164 -0
- package/src/nativewind-env.d.ts +1 -0
- package/src/prStatus.ts +90 -0
- package/src/services/PullRequestDraftsProvider.tsx +179 -0
- package/src/services/PullRequestReviewDraftProvider.tsx +178 -0
- package/src/services/PullRequestSuggestionBatchProvider.tsx +112 -0
- package/src/services/emojify.ts +13 -0
- package/src/services/errorToMessage.ts +6 -0
- package/src/services/expiringDraftStore.ts +102 -0
- package/src/services/formatDataAge.ts +12 -0
- package/src/services/formatRelativeDate.ts +56 -0
- package/src/services/githubImages.tsx +33 -0
- package/src/services/isViewerAuthor.ts +7 -0
- 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
|
-
#
|
|
1
|
+
# @reviewflow/ui
|
|
2
2
|
|
|
3
|
-
|
|
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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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";
|