From cdabd7a51f2ad15631176d2c204744837c4e2e57 Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Fri, 24 Dec 2021 17:12:13 +0800 Subject: [PATCH] feat: show PR file diff --- src/components/DiffStatGraph.tsx | 37 +++++++++++ src/components/DiffStatText.tsx | 34 ++++++++++ src/components/FileExplorer.tsx | 15 ++++- src/components/Icon.tsx | 20 +++++- src/components/settings/FileTreeSettings.tsx | 7 ++ src/global.d.ts | 6 ++ src/platforms/GitHub/index.ts | 24 ++++--- src/styles/index.scss | 69 +++++++++++++++++++- src/utils/config/helper.ts | 3 + src/utils/general.test.ts | 29 ++++++++ src/utils/general.ts | 24 +++++-- 11 files changed, 249 insertions(+), 19 deletions(-) create mode 100644 src/components/DiffStatGraph.tsx create mode 100644 src/components/DiffStatText.tsx create mode 100644 src/utils/general.test.ts diff --git a/src/components/DiffStatGraph.tsx b/src/components/DiffStatGraph.tsx new file mode 100644 index 0000000..7aaa2dd --- /dev/null +++ b/src/components/DiffStatGraph.tsx @@ -0,0 +1,37 @@ +import * as React from 'react' +import { resolveDiffGraphMeta } from 'utils/general' +import { Icon } from './Icon' + +export function DiffStatGraph({ + diff: { status, changes, additions, deletions }, +}: { + diff: Required['diff'] +}) { + const { g, r, w } = resolveDiffGraphMeta(additions, deletions, changes) + + const children: React.ReactNode[] = [] + for (let i = 0; i < g; i++) + children.push() + for (let i = 0; i < r; i++) + children.push() + for (let i = 0; i < w; i++) + children.push() + + return ( + + + {children} + + ) +} diff --git a/src/components/DiffStatText.tsx b/src/components/DiffStatText.tsx new file mode 100644 index 0000000..2f8715c --- /dev/null +++ b/src/components/DiffStatText.tsx @@ -0,0 +1,34 @@ +import * as React from 'react' +import { Icon } from './Icon' + +export function DiffStatText({ + diff: { status, changes, additions, deletions }, +}: { + diff: Required['diff'] +}) { + return ( + + {status !== 'modified' && ( + + )} + {additions > 0 && ( + {status === 'modified' ? `+${additions}` : additions} + )} + {additions > 0 && deletions > 0 && '/'} + {deletions > 0 && ( + {status === 'modified' ? `-${deletions}` : deletions} + )} + + ) +} diff --git a/src/components/FileExplorer.tsx b/src/components/FileExplorer.tsx index f1f64c5..55b1034 100644 --- a/src/components/FileExplorer.tsx +++ b/src/components/FileExplorer.tsx @@ -17,6 +17,8 @@ import { useOnLocationChange } from 'utils/hooks/useOnLocationChange' import { useOnPJAXDone } from 'utils/hooks/usePJAX' import { VisibleNodes } from 'utils/VisibleNodesGenerator' import { SideBarStateContext } from '../containers/SideBarState' +import { DiffStatGraph } from './DiffStatGraph' +import { DiffStatText } from './DiffStatText' import { Icon } from './Icon' import { SearchMode, searchModes } from './searchModes' import { SizeObserver } from './SizeObserver' @@ -52,6 +54,7 @@ const RawFileExplorer: React.FC = function RawFileExplor searchMode, commentToggle, restoreExpandedFolders, + showDiffInText, }, } = useConfigs() @@ -121,16 +124,26 @@ const RawFileExplorer: React.FC = function RawFileExplor {node.comments > 9 ? '9+' : node.comments} ) + const renderFileStatus = ({ diff }: TreeNode): React.ReactNode => + diff && ( + + {showDiffInText ? : } + + ) const renders: ((node: TreeNode) => React.ReactNode)[] = [] if (commentToggle) renders.push(renderFileCommentAmounts) + renders.push(renderFileStatus) if (searchMode === 'fuzzy') renders.push(renderFindInFolderButton) if (searched) renders.push(renderGoToButton) return renders.length ? node => renders.map((render, i) => {render(node)}) : undefined - }, [goTo, onSearch, searched, searchMode, commentToggle]) + }, [goTo, onSearch, searched, searchMode, commentToggle, showDiffInText]) const renderLabelText = React.useCallback( (node: TreeNode) => searchModes[searchMode].renderNodeLabelText(node, searchKey), diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index d3f8eb4..1204d18 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -3,6 +3,12 @@ import { ChevronRightIcon as ChevronRight, ClockIcon as Clock, CommentIcon as Comment, + DiffAddedIcon as DiffAdded, + DiffIcon as Diff, + DiffIgnoredIcon as DiffIgnored, + DiffModifiedIcon as DiffModified, + DiffRemovedIcon as DiffRemoved, + DiffRenamedIcon as DiffRenamed, FileCodeIcon as FileCode, FileIcon as File, FileMediaIcon as FileMedia, @@ -18,7 +24,7 @@ import { ReplyIcon as Reply, SearchIcon as Search, TabIcon as Tab, - XIcon as X, + XIcon as X } from '@primer/octicons-react' import * as React from 'react' import { cx } from 'utils/cx' @@ -27,6 +33,12 @@ const iconToComponentMap = { Search, Clock, Comment, + Diff, + DiffAdded, + DiffIgnored, + DiffModified, + DiffRemoved, + DiffRenamed, Hourglass, Submodule, Grabber, @@ -60,6 +72,12 @@ const typeToIconComponentMap: { pin: 'Pin', tab: 'Tab', gear: 'Gear', + diff: 'Diff', + diffAdded: 'DiffAdded', + diffIgnored: 'DiffIgnored', + diffModified: 'DiffModified', + diffRemoved: 'DiffRemoved', + diffRenamed: 'DiffRenamed', folder: 'ChevronRight', 'chevron-down': 'ChevronDown', 'go-to': 'Reply', diff --git a/src/components/settings/FileTreeSettings.tsx b/src/components/settings/FileTreeSettings.tsx index 3b79af5..25c823e 100644 --- a/src/components/settings/FileTreeSettings.tsx +++ b/src/components/settings/FileTreeSettings.tsx @@ -90,6 +90,13 @@ export function FileTreeSettings(props: React.PropsWithChildren) { tooltip: 'Show number of comments next to file names in Pull Requests.', }} /> + = { diff --git a/src/platforms/GitHub/index.ts b/src/platforms/GitHub/index.ts index a163c16..5b6ab31 100644 --- a/src/platforms/GitHub/index.ts +++ b/src/platforms/GitHub/index.ts @@ -323,14 +323,22 @@ async function getPullRequestTreeData( } const urlMainPart = `https://${window.location.host}/${userName}/${repoName}/pull/${pullId}/files${window.location.search}` - const nodes: TreeNode[] = treeData.map(item => ({ - path: item.filename || '', - type: 'blob', - name: item.filename?.replace(/^.*\//, '') || '', - url: `${urlMainPart}${formatHash(getFileElementHash(item.filename))}`, - sha: item.sha, - comments: commentData?.filter(comment => item.filename === comment.path).length, - })) + const nodes: TreeNode[] = treeData.map( + ({ filename, sha, additions, deletions, changes, status }) => ({ + path: filename || '', + type: 'blob', + name: filename?.replace(/^.*\//, '') || '', + url: `${urlMainPart}${formatHash(getFileElementHash(filename))}`, + sha: sha, + comments: commentData?.filter(comment => filename === comment.path).length, + diff: { + status, + additions, + deletions, + changes, + }, + }), + ) const root = processTree(nodes) return { root } diff --git a/src/styles/index.scss b/src/styles/index.scss index 11550f3..dc3bb95 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -81,7 +81,6 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header- } } - .#{$name}-ready { // github // code folding start @@ -732,8 +731,9 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header- padding-right: 8px; .node-item-comment { - display: inline-block; - width: 48px; + display: inline-flex; + align-items: center; + min-width: 36px; padding: 0 4px; color: var(--gitako-fg-muted); @@ -742,6 +742,69 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header- } } + .node-item-diff { + display: inline-flex; + align-items: center; + padding: 0 4px; + color: var(--gitako-fg-muted); + + .octicon-wrapper { + margin: 0; // make it closer to the diff details + } + + .added .octicon { + color: var(--gitako-success-emphasis); + } + .removed .octicon { + color: var(--gitako-danger-emphasis); + } + .renamed .octicon { + color: var(--gitako-done-emphasis); + } + .modified .octicon { + color: var(--gitako-accent-emphasis); + } + .ignored .octicon { + color: var(--gitako-neutral-emphasis); + } + + .diff-stat-graph { + display: inline-block; + white-space: nowrap; + > span { + display: inline-block; + width: 2px; + height: 10px; + margin: 0 1px; + border-radius: 2px; + background: var(--gitako-neutral-emphasis); + } + + .diff-stat-graph-addition { + background: var(--gitako-success-emphasis); + } + + .diff-stat-graph-deletion { + background: var(--gitako-danger-emphasis); + } + } + + .diff-stat-text { + display: inline-block; + white-space: nowrap; + font-family: 'Cascadia Code', 'Courier New', Courier, monospace; // Use some commonly available monospace font + .additions { + color: var(--gitako-success-emphasis); + } + .delimiter { + color: var(--gitako-neutral-emphasis); + } + .deletions { + color: var(--gitako-danger-emphasis); + } + } + } + .go-to-button, .find-in-folder-button { @include icon-button(); diff --git a/src/utils/config/helper.ts b/src/utils/config/helper.ts index 24c67b7..ea3c369 100644 --- a/src/utils/config/helper.ts +++ b/src/utils/config/helper.ts @@ -20,6 +20,7 @@ export type Config = { codeFolding: boolean compactFileTree: boolean restoreExpandedFolders: boolean + showDiffInText: boolean } enum configKeys { @@ -40,6 +41,7 @@ enum configKeys { codeFolding = 'codeFolding', compactFileTree = 'compactFileTree', restoreExpandedFolders = 'restoreExpandedFolders', + showDiffInText = 'showDiffInText', } // do NOT use platform name @@ -63,6 +65,7 @@ export const defaultConfigs: Config = { codeFolding: true, compactFileTree: false, restoreExpandedFolders: true, + showDiffInText: false, } const configKeyArray = Object.values(configKeys) diff --git a/src/utils/general.test.ts b/src/utils/general.test.ts new file mode 100644 index 0000000..205acc8 --- /dev/null +++ b/src/utils/general.test.ts @@ -0,0 +1,29 @@ +import { resolveDiffGraphMeta } from './general' + +it(`should resolve diff stat graph meta properly`, () => { + const example = ` + 2 10 0 4 + 3 10 1 3 + 3 17 0 4 + 4 17 0 4 + 4 26 0 4 + 5 17 1 3 + 6 17 1 3 + 12 0 5 0 + 0 12 0 5 + 17 74 0 4 + 11 23 1 3 + 18 28 1 3 + 34 24 2 2 + ` + + example + .split(/\n/) + .map(line => line.trim()) + .filter(line => line.length) + .map(line => line.split(/\s+/).map(_ => parseInt(_))) + .forEach(([additions, deletions, g, r]) => { + const meta = resolveDiffGraphMeta(additions, deletions, additions + deletions) + expect([meta.g, meta.r]).toEqual([g, r]) + }) +}) diff --git a/src/utils/general.ts b/src/utils/general.ts index 3940b86..af14a56 100644 --- a/src/utils/general.ts +++ b/src/utils/general.ts @@ -21,12 +21,14 @@ export enum OperatingSystems { } function detectOS(): OperatingSystems { - const { - navigator: { userAgent }, - } = window - if (userAgent.indexOf(OperatingSystems.Windows) !== -1) return OperatingSystems.Windows - else if (userAgent.indexOf(OperatingSystems.macOS) !== -1) return OperatingSystems.macOS - else if (userAgent.indexOf(OperatingSystems.Linux) !== -1) return OperatingSystems.Linux + if (typeof window !== 'undefined') { + const { + navigator: { userAgent }, + } = window + if (userAgent.indexOf(OperatingSystems.Windows) !== -1) return OperatingSystems.Windows + else if (userAgent.indexOf(OperatingSystems.macOS) !== -1) return OperatingSystems.macOS + else if (userAgent.indexOf(OperatingSystems.Linux) !== -1) return OperatingSystems.Linux + } return OperatingSystems.others } @@ -200,3 +202,13 @@ export function isOpenInNewWindowClick(event: React.MouseEvent 0 && deletions > 0, + overflow = changes > 5, + preserved = both && overflow ? 1 : 0, + g = overflow ? Math.floor(((5 - preserved) * (additions + 1)) / (changes + 1)) : additions, + r = overflow ? 5 - preserved - g : deletions, + w = 5 - g - r + return { g, r, w } +}