Add rich viewer for files (non-diff) (#246)
* Add rich viewer for files (non-diff) Fixes #74 * Fix test typo * WIP toolbar and proper blob element injection * Add working Preview/Code/Blame toggle * Add missing aria-label * Add e2e test and classic/reactUI support * Re-enable headless e2e * Update e2e snap for linux * Lint * Improve style a bit * Reorg and cleanup
This commit is contained in:
@ -3,6 +3,7 @@ import { Octokit } from '@octokit/rest'
|
|||||||
import {
|
import {
|
||||||
KittycadUser,
|
KittycadUser,
|
||||||
Message,
|
Message,
|
||||||
|
MessageGetFileBlob,
|
||||||
MessageGetFileDiff,
|
MessageGetFileDiff,
|
||||||
MessageGetGithubCommitData,
|
MessageGetGithubCommitData,
|
||||||
MessageGetGithubPullFilesData,
|
MessageGetGithubPullFilesData,
|
||||||
@ -16,7 +17,7 @@ import {
|
|||||||
setStorageGithubToken,
|
setStorageGithubToken,
|
||||||
setStorageKittycadToken,
|
setStorageKittycadToken,
|
||||||
} from './storage'
|
} from './storage'
|
||||||
import { getFileDiff } from './diff'
|
import { getFileBlob, getFileDiff } from './diff'
|
||||||
|
|
||||||
let github: Octokit | undefined
|
let github: Octokit | undefined
|
||||||
let kittycad: Client | undefined
|
let kittycad: Client | undefined
|
||||||
@ -168,5 +169,18 @@ chrome.runtime.onMessage.addListener(
|
|||||||
.catch(error => sendResponse({ error }))
|
.catch(error => sendResponse({ error }))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.id === MessageIds.GetFileBlob) {
|
||||||
|
if (!kittycad || !github) {
|
||||||
|
sendResponse({ error: noClientError })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const { owner, repo, sha, filename } =
|
||||||
|
message.data as MessageGetFileBlob
|
||||||
|
getFileBlob(github, kittycad, owner, repo, sha, filename)
|
||||||
|
.then(r => sendResponse(r))
|
||||||
|
.catch(error => sendResponse({ error }))
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { CadDiffPage } from '../components/diff/CadDiffPage'
|
import { CadDiffPage } from '../components/viewer/CadDiffPage'
|
||||||
|
import { CadBlobPage } from '../components/viewer/CadBlobPage'
|
||||||
import { Commit, DiffEntry, MessageIds, Pull } from './types'
|
import { Commit, DiffEntry, MessageIds, Pull } from './types'
|
||||||
import {
|
import {
|
||||||
getGithubPullUrlParams,
|
getGithubPullUrlParams,
|
||||||
mapInjectableDiffElements,
|
mapInjectableDiffElements,
|
||||||
getGithubCommitUrlParams,
|
getGithubCommitUrlParams,
|
||||||
createReactRoot,
|
createReactRoot,
|
||||||
|
getGithubBlobUrlParams,
|
||||||
} from './web'
|
} from './web'
|
||||||
import gitHubInjection from 'github-injection'
|
import gitHubInjection from 'github-injection'
|
||||||
|
|
||||||
@ -30,6 +32,43 @@ async function injectDiff(
|
|||||||
root.render(cadDiffPage)
|
root.render(cadDiffPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function injectBlob(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
sha: string,
|
||||||
|
filename: string,
|
||||||
|
document: Document
|
||||||
|
) {
|
||||||
|
let classicUi = false
|
||||||
|
// React UI (as of 2023-06-23, for signed-in users only)
|
||||||
|
const childWithProperClass = document.querySelector<HTMLElement>(
|
||||||
|
'.react-blob-view-header-sticky'
|
||||||
|
)
|
||||||
|
let element = childWithProperClass?.parentElement
|
||||||
|
if (!element) {
|
||||||
|
// Classic UI
|
||||||
|
const childWithProperClass =
|
||||||
|
document.querySelector<HTMLElement>('.js-blob-header')
|
||||||
|
element = childWithProperClass?.parentElement
|
||||||
|
classicUi = !!element
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
throw Error("Couldn't find blob html element to inject")
|
||||||
|
}
|
||||||
|
|
||||||
|
element.classList.add('kittycad-injected-file')
|
||||||
|
const cadBlobPage = React.createElement(CadBlobPage, {
|
||||||
|
element,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
sha,
|
||||||
|
filename,
|
||||||
|
classicUi,
|
||||||
|
})
|
||||||
|
root.render(cadBlobPage)
|
||||||
|
}
|
||||||
|
|
||||||
async function injectPullDiff(
|
async function injectPullDiff(
|
||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
@ -88,6 +127,14 @@ async function run() {
|
|||||||
await injectCommitDiff(owner, repo, sha, window.document)
|
await injectCommitDiff(owner, repo, sha, window.document)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blobParams = getGithubBlobUrlParams(url)
|
||||||
|
if (blobParams) {
|
||||||
|
const { owner, repo, sha, filename } = blobParams
|
||||||
|
console.log('Found blob diff: ', owner, repo, sha, filename)
|
||||||
|
await injectBlob(owner, repo, sha, filename, window.document)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function waitForLateDiffNodes(callback: () => void) {
|
function waitForLateDiffNodes(callback: () => void) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Octokit } from '@octokit/rest'
|
import { Octokit } from '@octokit/rest'
|
||||||
import { Client, file } from '@kittycad/lib'
|
import { Client, file } from '@kittycad/lib'
|
||||||
import { ContentFile, DiffEntry, FileDiff } from './types'
|
import { ContentFile, DiffEntry, FileBlob, FileDiff } from './types'
|
||||||
import {
|
import {
|
||||||
FileExportFormat_type,
|
FileExportFormat_type,
|
||||||
FileImportFormat_type,
|
FileImportFormat_type,
|
||||||
@ -132,3 +132,25 @@ export async function getFileDiff(
|
|||||||
|
|
||||||
throw Error(`Unsupported status: ${status}`)
|
throw Error(`Unsupported status: ${status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFileBlob(
|
||||||
|
github: Octokit,
|
||||||
|
kittycad: Client,
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
sha: string,
|
||||||
|
filename: string
|
||||||
|
): Promise<FileBlob> {
|
||||||
|
const extension = filename.split('.').pop()
|
||||||
|
if (!extension || !extensionToSrcFormat[extension]) {
|
||||||
|
throw Error(
|
||||||
|
`Unsupported extension. Given ${extension}, was expecting ${Object.keys(
|
||||||
|
extensionToSrcFormat
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawBlob = await downloadFile(github, owner, repo, sha, filename)
|
||||||
|
const blob = await convert(kittycad, rawBlob, extension)
|
||||||
|
return { blob }
|
||||||
|
}
|
||||||
|
@ -20,6 +20,10 @@ export type FileDiff = {
|
|||||||
after?: string
|
after?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FileBlob = {
|
||||||
|
blob?: string
|
||||||
|
}
|
||||||
|
|
||||||
export enum MessageIds {
|
export enum MessageIds {
|
||||||
GetGithubPullFiles = 'GetPullFiles',
|
GetGithubPullFiles = 'GetPullFiles',
|
||||||
GetGithubUser = 'GetGitHubUser',
|
GetGithubUser = 'GetGitHubUser',
|
||||||
@ -27,6 +31,7 @@ export enum MessageIds {
|
|||||||
SaveKittycadToken = 'SaveKittyCadToken',
|
SaveKittycadToken = 'SaveKittyCadToken',
|
||||||
GetKittycadUser = 'GetKittyCadUser',
|
GetKittycadUser = 'GetKittyCadUser',
|
||||||
GetFileDiff = 'GetFileDiff',
|
GetFileDiff = 'GetFileDiff',
|
||||||
|
GetFileBlob = 'GetFileBlob',
|
||||||
GetGithubPull = 'GetGithubPull',
|
GetGithubPull = 'GetGithubPull',
|
||||||
GetGithubCommit = 'GetGithubCommit',
|
GetGithubCommit = 'GetGithubCommit',
|
||||||
}
|
}
|
||||||
@ -51,6 +56,13 @@ export type MessageGetFileDiff = {
|
|||||||
file: DiffEntry
|
file: DiffEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MessageGetFileBlob = {
|
||||||
|
owner: string
|
||||||
|
repo: string
|
||||||
|
sha: string
|
||||||
|
filename: string
|
||||||
|
}
|
||||||
|
|
||||||
export type MessageSaveToken = {
|
export type MessageSaveToken = {
|
||||||
token: string
|
token: string
|
||||||
}
|
}
|
||||||
@ -76,5 +88,6 @@ export type MessageResponse =
|
|||||||
| KittycadUser
|
| KittycadUser
|
||||||
| MessageSaveToken
|
| MessageSaveToken
|
||||||
| FileDiff
|
| FileDiff
|
||||||
|
| FileBlob
|
||||||
| MessageError
|
| MessageError
|
||||||
| void
|
| void
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
mapInjectableDiffElements,
|
mapInjectableDiffElements,
|
||||||
getSupportedWebDiffElements,
|
getSupportedWebDiffElements,
|
||||||
createReactRoot,
|
createReactRoot,
|
||||||
|
getGithubBlobUrlParams,
|
||||||
} from './web'
|
} from './web'
|
||||||
|
|
||||||
const githubPullHtmlSnippet = `
|
const githubPullHtmlSnippet = `
|
||||||
@ -146,6 +147,27 @@ describe('Function getGithubCommitUrlParams', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Function getGithubBlobUrlParams', () => {
|
||||||
|
it('gets params out of a valid github blob link', () => {
|
||||||
|
const url =
|
||||||
|
'https://github.com/KittyCAD/diff-samples/blob/fd9eec79f0464833686ea6b5b34ea07145e32734/models/box.obj'
|
||||||
|
const params = getGithubBlobUrlParams(url)
|
||||||
|
expect(params).toBeDefined()
|
||||||
|
const { owner, repo, sha, filename } = params!
|
||||||
|
expect(owner).toEqual('KittyCAD')
|
||||||
|
expect(repo).toEqual('diff-samples')
|
||||||
|
expect(sha).toEqual('fd9eec79f0464833686ea6b5b34ea07145e32734')
|
||||||
|
expect(filename).toEqual('models/box.obj')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("doesn't match other URLs", () => {
|
||||||
|
expect(getGithubPullUrlParams('http://google.com')).toBeUndefined()
|
||||||
|
expect(
|
||||||
|
getGithubPullUrlParams('https://github.com/KittyCAD/litterbox')
|
||||||
|
).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('finds web elements for supported files', () => {
|
it('finds web elements for supported files', () => {
|
||||||
const elements = getSupportedWebDiffElements(githubPullHtmlDocument)
|
const elements = getSupportedWebDiffElements(githubPullHtmlDocument)
|
||||||
expect(elements).toHaveLength(2)
|
expect(elements).toHaveLength(2)
|
||||||
|
@ -44,6 +44,34 @@ export function getGithubCommitUrlParams(
|
|||||||
return { owner, repo, sha }
|
return { owner, repo, sha }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GithubBlobUrlParams = {
|
||||||
|
owner: string
|
||||||
|
repo: string
|
||||||
|
sha: string
|
||||||
|
filename: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGithubBlobUrlParams(
|
||||||
|
url: string
|
||||||
|
): GithubBlobUrlParams | undefined {
|
||||||
|
const blobRe =
|
||||||
|
/https:\/\/github\.com\/([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)\/blob\/(\w+)\/([^\0]+)/
|
||||||
|
const result = blobRe.exec(url)
|
||||||
|
if (!result) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, owner, repo, sha, filename] = result
|
||||||
|
console.log(
|
||||||
|
'Found a supported Github Blob URL:',
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
sha,
|
||||||
|
filename
|
||||||
|
)
|
||||||
|
return { owner, repo, sha, filename }
|
||||||
|
}
|
||||||
|
|
||||||
export function getSupportedWebDiffElements(document: Document): HTMLElement[] {
|
export function getSupportedWebDiffElements(document: Document): HTMLElement[] {
|
||||||
const fileTypeSelectors = Object.keys(extensionToSrcFormat).map(
|
const fileTypeSelectors = Object.keys(extensionToSrcFormat).map(
|
||||||
t => `.file[data-file-type=".${t}"]`
|
t => `.file[data-file-type=".${t}"]`
|
||||||
|
68
src/components/viewer/CadBlob.tsx
Normal file
68
src/components/viewer/CadBlob.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import '@react-three/fiber'
|
||||||
|
import { Box, Text, useTheme } from '@primer/react'
|
||||||
|
import { FileBlob } from '../../chrome/types'
|
||||||
|
import { Viewer3D } from './Viewer3D'
|
||||||
|
import { BufferGeometry, Sphere } from 'three'
|
||||||
|
import { WireframeColors, WireframeModel } from './WireframeModel'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import { loadGeometry } from '../../utils/three'
|
||||||
|
import { OrbitControls } from 'three-stdlib'
|
||||||
|
import { RecenterButton } from './RecenterButton'
|
||||||
|
import { ErrorMessage } from './ErrorMessage'
|
||||||
|
|
||||||
|
export function CadBlob({ blob }: FileBlob): React.ReactElement {
|
||||||
|
const [geometry, setGeometry] = useState<BufferGeometry>()
|
||||||
|
const [boundingSphere, setBoundingSphere] = useState<Sphere>()
|
||||||
|
const controlsRef = useRef<OrbitControls | null>(null)
|
||||||
|
const [controlsAltered, setControlsAltered] = useState(false)
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const colors: WireframeColors = {
|
||||||
|
face: theme?.colors.fg.default,
|
||||||
|
edge: theme?.colors.fg.muted,
|
||||||
|
dashEdge: theme?.colors.fg.subtle,
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
let geometry: BufferGeometry | undefined = undefined
|
||||||
|
if (blob) {
|
||||||
|
geometry = loadGeometry(blob)
|
||||||
|
setGeometry(geometry)
|
||||||
|
if (geometry && geometry.boundingSphere) {
|
||||||
|
setBoundingSphere(geometry.boundingSphere)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [blob])
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{geometry && (
|
||||||
|
<Box position="relative">
|
||||||
|
<Box height={300} backgroundColor="canvas.subtle">
|
||||||
|
<Viewer3D
|
||||||
|
geometry={geometry}
|
||||||
|
boundingSphere={boundingSphere}
|
||||||
|
controlsRef={controlsRef}
|
||||||
|
onControlsAltered={() =>
|
||||||
|
!controlsAltered && setControlsAltered(true)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<WireframeModel
|
||||||
|
geometry={geometry}
|
||||||
|
boundingSphere={boundingSphere}
|
||||||
|
colors={colors}
|
||||||
|
/>
|
||||||
|
</Viewer3D>
|
||||||
|
</Box>
|
||||||
|
{controlsAltered && (
|
||||||
|
<RecenterButton
|
||||||
|
onClick={() => {
|
||||||
|
controlsRef.current?.reset()
|
||||||
|
setControlsAltered(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{!geometry && <ErrorMessage />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
155
src/components/viewer/CadBlobPage.tsx
Normal file
155
src/components/viewer/CadBlobPage.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import '@react-three/fiber'
|
||||||
|
import { Box, SegmentedControl, ThemeProvider } from '@primer/react'
|
||||||
|
import { FileBlob, MessageIds } from '../../chrome/types'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { Loading } from '../Loading'
|
||||||
|
import { CadBlob } from './CadBlob'
|
||||||
|
|
||||||
|
function CadBlobPortal({
|
||||||
|
element,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
sha,
|
||||||
|
filename,
|
||||||
|
classicUi,
|
||||||
|
}: {
|
||||||
|
element: HTMLElement
|
||||||
|
owner: string
|
||||||
|
repo: string
|
||||||
|
sha: string
|
||||||
|
filename: string
|
||||||
|
classicUi: boolean
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [richBlob, setRichBlob] = useState<FileBlob>()
|
||||||
|
const [richSelected, setRichSelected] = useState(true)
|
||||||
|
const [toolbarContainer, setToolbarContainer] = useState<HTMLElement>()
|
||||||
|
const [blobContainer, setBlobContainer] = useState<HTMLElement>()
|
||||||
|
const [sourceElements, setSourceElements] = useState<HTMLElement[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let existingToggle: HTMLElement | undefined | null
|
||||||
|
let toolbar: HTMLElement | undefined | null
|
||||||
|
let blob: HTMLElement | undefined | null
|
||||||
|
if (classicUi) {
|
||||||
|
// no existing toggle
|
||||||
|
toolbar = element.querySelector<HTMLElement>('.js-blob-header')
|
||||||
|
blob = element.querySelector<HTMLElement>('.blob-wrapper')
|
||||||
|
} else {
|
||||||
|
existingToggle = element.querySelector<HTMLElement>(
|
||||||
|
'ul[class*=SegmentedControl]'
|
||||||
|
)
|
||||||
|
toolbar = existingToggle?.parentElement
|
||||||
|
blob = element.querySelector<HTMLElement>(
|
||||||
|
'section[aria-labelledby="file-name-id"]'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolbar != null) {
|
||||||
|
setToolbarContainer(toolbar)
|
||||||
|
if (existingToggle) {
|
||||||
|
existingToggle.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blob != null) {
|
||||||
|
setBlobContainer(blob)
|
||||||
|
const sourceElements = Array.from(blob.children) as HTMLElement[]
|
||||||
|
sourceElements.map(n => (n.style.display = 'none'))
|
||||||
|
setSourceElements(sourceElements)
|
||||||
|
}
|
||||||
|
}, [element])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
;(async () => {
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
id: MessageIds.GetFileBlob,
|
||||||
|
data: { owner, repo, sha, filename },
|
||||||
|
})
|
||||||
|
if ('error' in response) {
|
||||||
|
console.log(response.error)
|
||||||
|
} else {
|
||||||
|
setRichBlob(response as FileBlob)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}, [owner, repo, sha, filename])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{toolbarContainer &&
|
||||||
|
createPortal(
|
||||||
|
<SegmentedControl
|
||||||
|
sx={{ mr: classicUi ? 2 : 0, order: -1 }} // prepend in flex
|
||||||
|
aria-label="File view"
|
||||||
|
onChange={(index: number) => {
|
||||||
|
if (index < 2) {
|
||||||
|
setRichSelected(index === 0)
|
||||||
|
sourceElements.map(
|
||||||
|
n =>
|
||||||
|
(n.style.display =
|
||||||
|
index === 0 ? 'none' : 'block')
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.location.href = `https://github.com/${owner}/${repo}/blame/${sha}/${filename}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SegmentedControl.Button selected={richSelected}>
|
||||||
|
Preview
|
||||||
|
</SegmentedControl.Button>
|
||||||
|
<SegmentedControl.Button selected={!richSelected}>
|
||||||
|
Code
|
||||||
|
</SegmentedControl.Button>
|
||||||
|
{!classicUi && (
|
||||||
|
<SegmentedControl.Button>
|
||||||
|
Blame
|
||||||
|
</SegmentedControl.Button>
|
||||||
|
)}
|
||||||
|
</SegmentedControl>,
|
||||||
|
toolbarContainer
|
||||||
|
)}
|
||||||
|
{blobContainer &&
|
||||||
|
createPortal(
|
||||||
|
<Box sx={{ display: richSelected ? 'block' : 'none' }}>
|
||||||
|
{richBlob ? (
|
||||||
|
<CadBlob blob={richBlob.blob} />
|
||||||
|
) : (
|
||||||
|
<Loading />
|
||||||
|
)}
|
||||||
|
</Box>,
|
||||||
|
blobContainer
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CadBlobPageProps = {
|
||||||
|
element: HTMLElement
|
||||||
|
owner: string
|
||||||
|
repo: string
|
||||||
|
sha: string
|
||||||
|
filename: string
|
||||||
|
classicUi: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CadBlobPage({
|
||||||
|
element,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
sha,
|
||||||
|
filename,
|
||||||
|
classicUi,
|
||||||
|
}: CadBlobPageProps): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<ThemeProvider colorMode="auto">
|
||||||
|
<CadBlobPortal
|
||||||
|
element={element}
|
||||||
|
owner={owner}
|
||||||
|
repo={repo}
|
||||||
|
sha={sha}
|
||||||
|
filename={filename}
|
||||||
|
classicUi={classicUi}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
@ -18,6 +18,8 @@ import { BeakerIcon } from '@primer/octicons-react'
|
|||||||
import { LegendBox, LegendLabel } from './Legend'
|
import { LegendBox, LegendLabel } from './Legend'
|
||||||
import { getCommonSphere, loadGeometry } from '../../utils/three'
|
import { getCommonSphere, loadGeometry } from '../../utils/three'
|
||||||
import { OrbitControls } from 'three-stdlib'
|
import { OrbitControls } from 'three-stdlib'
|
||||||
|
import { RecenterButton } from './RecenterButton'
|
||||||
|
import { ErrorMessage } from './ErrorMessage'
|
||||||
|
|
||||||
function Viewer3D2Up({
|
function Viewer3D2Up({
|
||||||
beforeGeometry,
|
beforeGeometry,
|
||||||
@ -88,17 +90,13 @@ function Viewer3D2Up({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{controlsAltered && (
|
{controlsAltered && (
|
||||||
<Box top={2} right={2} position="absolute">
|
<RecenterButton
|
||||||
<Button
|
onClick={() => {
|
||||||
onClick={() => {
|
afterControlsRef.current?.reset()
|
||||||
afterControlsRef.current?.reset()
|
beforeControlsRef.current?.reset()
|
||||||
beforeControlsRef.current?.reset()
|
setControlsAltered(false)
|
||||||
setControlsAltered(false)
|
}}
|
||||||
}}
|
/>
|
||||||
>
|
|
||||||
Recenter
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@ -158,16 +156,12 @@ function Viewer3DCombined({
|
|||||||
/>
|
/>
|
||||||
</LegendBox>
|
</LegendBox>
|
||||||
{controlsAltered && (
|
{controlsAltered && (
|
||||||
<Box top={2} right={2} position="absolute">
|
<RecenterButton
|
||||||
<Button
|
onClick={() => {
|
||||||
onClick={() => {
|
controlsRef.current?.reset()
|
||||||
controlsRef.current?.reset()
|
setControlsAltered(false)
|
||||||
setControlsAltered(false)
|
}}
|
||||||
}}
|
/>
|
||||||
>
|
|
||||||
Recenter
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@ -273,13 +267,7 @@ export function CadDiff({ before, after }: FileDiff): React.ReactElement {
|
|||||||
</TabNav>
|
</TabNav>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{!beforeGeometry && !afterGeometry && (
|
{!beforeGeometry && !afterGeometry && <ErrorMessage />}
|
||||||
<Box p={3}>
|
|
||||||
<Text>
|
|
||||||
Sorry, the rich diff can't be displayed for this file.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
14
src/components/viewer/ErrorMessage.test.tsx
Normal file
14
src/components/viewer/ErrorMessage.test.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { ErrorMessage } from './ErrorMessage'
|
||||||
|
|
||||||
|
it('renders the error message', async () => {
|
||||||
|
render(<ErrorMessage />)
|
||||||
|
const text = await screen.findByText(/preview/)
|
||||||
|
expect(text).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the error message with a custom message', async () => {
|
||||||
|
render(<ErrorMessage message="custom" />)
|
||||||
|
const text = await screen.findByText(/custom/)
|
||||||
|
expect(text).toBeDefined()
|
||||||
|
})
|
12
src/components/viewer/ErrorMessage.tsx
Normal file
12
src/components/viewer/ErrorMessage.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Box, Text } from '@primer/react'
|
||||||
|
|
||||||
|
export function ErrorMessage({ message }: { message?: string }) {
|
||||||
|
return (
|
||||||
|
<Box p={3}>
|
||||||
|
<Text>
|
||||||
|
{message ||
|
||||||
|
"Sorry, the rich preview can't be displayed for this file."}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
12
src/components/viewer/RecenterButton.test.tsx
Normal file
12
src/components/viewer/RecenterButton.test.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import { RecenterButton } from './RecenterButton'
|
||||||
|
|
||||||
|
it('renders the recenter button', async () => {
|
||||||
|
const callback = vi.fn()
|
||||||
|
render(<RecenterButton onClick={callback} />)
|
||||||
|
const button = await screen.findByRole('button')
|
||||||
|
expect(callback.mock.calls).toHaveLength(0)
|
||||||
|
fireEvent.click(button)
|
||||||
|
expect(callback.mock.calls).toHaveLength(1)
|
||||||
|
})
|
9
src/components/viewer/RecenterButton.tsx
Normal file
9
src/components/viewer/RecenterButton.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Box, Button } from '@primer/react'
|
||||||
|
|
||||||
|
export function RecenterButton({ onClick }: { onClick: () => void }) {
|
||||||
|
return (
|
||||||
|
<Box top={2} right={2} position="absolute">
|
||||||
|
<Button onClick={onClick}>Recenter</Button>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
@ -39,6 +39,19 @@ async function getFirstDiffScreenshot(
|
|||||||
return await element.screenshot()
|
return await element.screenshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getBlobPreviewScreenshot(page: Page, url: string) {
|
||||||
|
page.on('console', msg => console.log(msg.text()))
|
||||||
|
await page.goto(url)
|
||||||
|
|
||||||
|
// waiting for the canvas (that holds the diff) to show up
|
||||||
|
await page.waitForSelector('#repo-content-pjax-container canvas')
|
||||||
|
|
||||||
|
// screenshot the file diff with its toolbar
|
||||||
|
const element = await page.waitForSelector('.kittycad-injected-file')
|
||||||
|
await page.waitForTimeout(1000) // making sure the element fully settled in
|
||||||
|
return await element.screenshot()
|
||||||
|
}
|
||||||
|
|
||||||
test('pull request diff with an .obj file', async ({
|
test('pull request diff with an .obj file', async ({
|
||||||
page,
|
page,
|
||||||
authorizedBackground,
|
authorizedBackground,
|
||||||
@ -76,3 +89,13 @@ test('commit diff with a .dae file as LFS', async ({
|
|||||||
const screenshot = await getFirstDiffScreenshot(page, url, 'dae')
|
const screenshot = await getFirstDiffScreenshot(page, url, 'dae')
|
||||||
expect(screenshot).toMatchSnapshot()
|
expect(screenshot).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('blob preview with an .obj file', async ({
|
||||||
|
page,
|
||||||
|
authorizedBackground,
|
||||||
|
}) => {
|
||||||
|
const url =
|
||||||
|
'https://github.com/KittyCAD/diff-samples/blob/fd9eec79f0464833686ea6b5b34ea07145e32734/models/box.obj'
|
||||||
|
const screenshot = await getBlobPreviewScreenshot(page, url)
|
||||||
|
expect(screenshot).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
Reference in New Issue
Block a user