Compare commits

..

2 Commits

Author SHA1 Message Date
hyung-hwan 1e184f075e touching up frontend/src/pages/RepoGitDetailPage.tsx 2026-06-22 01:10:53 +09:00
hyung-hwan 76fe4f6830 some git detail page touch up 2026-06-22 00:32:15 +09:00
3 changed files with 210 additions and 194 deletions
+1
View File
@@ -43,6 +43,7 @@ export default function CodeBlock({ code = '', language = 'diff', showLineNumber
overflow: 'auto', overflow: 'auto',
backgroundColor: 'transparent', backgroundColor: 'transparent',
lineHeight: 1.4, lineHeight: 1.4,
fontSize: '0.85rem',
display: 'grid', display: 'grid',
gridTemplateColumns: showLineNumbers ? 'auto 1fr' : '1fr', gridTemplateColumns: showLineNumbers ? 'auto 1fr' : '1fr',
columnGap: showLineNumbers ? 12 : 0 columnGap: showLineNumbers ? 12 : 0
+1 -1
View File
@@ -22,7 +22,7 @@ export default function LazyCodeBlock(props: LazyCodeBlockProps) {
p: 0, p: 0,
whiteSpace: 'pre', whiteSpace: 'pre',
overflow: 'auto', overflow: 'auto',
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace' //fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace'
}} }}
> >
<code>{code}</code> <code>{code}</code>
+96 -81
View File
@@ -3,12 +3,15 @@ import { lazy, Suspense, useEffect, useRef, useState } from 'react'
import { useParams, useSearchParams } from 'react-router-dom' import { useParams, useSearchParams } from 'react-router-dom'
import { api, Project, Repo, RepoCommit, RepoTreeEntry } from '../api' import { api, Project, Repo, RepoCommit, RepoTreeEntry } from '../api'
import Autocomplete from '../components/Autocomplete' import Autocomplete from '../components/Autocomplete'
import AccountTreeIcon from '@mui/icons-material/AccountTree'
import CodeIcon from '@mui/icons-material/Code'
import ContentCopyIcon from '@mui/icons-material/ContentCopy' import ContentCopyIcon from '@mui/icons-material/ContentCopy'
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined' import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'
import FolderIcon from '@mui/icons-material/Folder' import FolderIcon from '@mui/icons-material/Folder'
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile' import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'
import FileDownloadIcon from '@mui/icons-material/FileDownload' import FileDownloadIcon from '@mui/icons-material/FileDownload'
import OpenInNewIcon from '@mui/icons-material/OpenInNew' import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import TerminalIcon from '@mui/icons-material/Terminal'
import LazyCodeBlock from '../components/LazyCodeBlock' import LazyCodeBlock from '../components/LazyCodeBlock'
import PageAlert from '../components/PageAlert' import PageAlert from '../components/PageAlert'
import ContextToolbar from '../components/ContextToolbar' import ContextToolbar from '../components/ContextToolbar'
@@ -167,37 +170,39 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
}) })
}, [repoId, ref, path, branches, treeReloadTick]) }, [repoId, ref, path, branches, treeReloadTick])
const clearReadme = () => {
setReadmeContent('')
setReadmeKind('')
setReadmePath('')
}
useEffect(() => { useEffect(() => {
if (!repoId || !ref) { if (!repoId || !ref) {
setReadmeContent('') clearReadme()
setReadmeKind('')
setReadmePath('')
return return
} }
if (!repo) return if (!repo) return
if (repo && repo.type && repo.type !== 'git') {
setReadmeContent('') if (repo.type && repo.type !== 'git') {
setReadmeKind('') clearReadme()
setReadmePath('')
return return
} }
if (selectedFile) { if (selectedFile) {
setReadmeContent('') clearReadme()
setReadmeKind('')
setReadmePath('')
return return
} }
const entries = tree || [] const entries = tree || []
let readme = entries.find((entry) => entry.name.toLowerCase() === 'readme.md') let readme = entries.find((entry) => entry.name.toLowerCase() === 'readme.md')
let kind: 'markdown' | 'text' | '' = readme ? 'markdown' : '' let kind: 'markdown' | 'text' | '' = (readme? 'markdown' : '')
if (!readme) { if (!readme) {
readme = entries.find((entry) => entry.name.toLowerCase() === 'readme.txt') readme = entries.find((entry) => entry.name.toLowerCase() === 'readme.txt')
kind = readme ? 'text' : '' kind = readme ? 'text' : ''
} }
if (!readme || readme.type !== 'file') { if (!readme || readme.type !== 'file') {
setReadmeContent('') clearReadme()
setReadmeKind('')
setReadmePath('')
return return
} }
if (readmePath === readme.path && readmeKind === kind) return if (readmePath === readme.path && readmeKind === kind) return
@@ -208,9 +213,7 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
setReadmePath(readme.path) setReadmePath(readme.path)
}) })
.catch(() => { .catch(() => {
setReadmeContent('') clearReadme()
setReadmeKind('')
setReadmePath('')
}) })
}, [repoId, ref, tree, selectedFile, readmePath, readmeKind]) }, [repoId, ref, tree, selectedFile, readmePath, readmeKind])
@@ -236,6 +239,7 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
setImagePreviewRef(ref) setImagePreviewRef(ref)
if (!isImageFile(entry.path)) { if (!isImageFile(entry.path)) {
try { try {
// get the content of the select file and set it to the content view
const blob = await api.getRepoBlob(repoId, ref || undefined, entry.path) const blob = await api.getRepoBlob(repoId, ref || undefined, entry.path)
setContent(blob.content) setContent(blob.content)
} catch { } catch {
@@ -539,59 +543,13 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
return match[1] return match[1]
} }
const renderClonePopover = () => {
if (!repo?.clone_url) return null
return ( return (
<ProjectPageFrame
projectId={projectId ?? ''}
project={project}
currentLabel={repo?.name || 'Repository'}
contextToolbar={
<ContextToolbar
kind="Repository"
label={repo?.name || 'Repository'}
nav={projectId && repoId ? <RepoSubNav projectId={projectId} repoId={repoId} branchRef={ref} repoType={repo?.type} inline /> : null}
/>
}
>
<Divider sx={{ mb: 1 }} />
{loadError ? (
<Typography variant="body2" color="error" sx={{ mb: 1 }}>
{loadError}
</Typography>
) : null}
<Box sx={{ display: 'flex', alignItems: 'stretch', gap: 1, mb: 1 }}>
<Paper sx={{ p: 1, display: 'flex', alignItems: 'center', gap: 1, minWidth: 240, backgroundColor: 'transparent' }}>
<Typography variant="subtitle2" color="text.secondary">
Branch
</Typography>
{branches.length ? (
<Autocomplete
freeSolo
options={branches}
value={ref}
inputValue={ref}
onChange={(_, value) => setRef(String(value || ''))}
onInputChange={(_, value, reason) => {
if (reason === 'input') setRef(value)
if (reason === 'clear') setRef('')
}}
renderInput={(params) => <TextField {...params} size="small" placeholder="branch or ref" />}
slotProps={{
popper: { sx: { zIndex: 1400 } }
}}
sx={{ minWidth: 180 }}
/>
) : null}
{defaultBranch ? (
<Typography variant="caption" color="text.secondary">
HEAD: {defaultBranch}
</Typography>
) : null}
</Paper>
{repo?.clone_url ? (
<> <>
<Button size="small" variant="outlined" onClick={(event) => setCodeAnchor(event.currentTarget)}> <IconButton onClick={(event) => setCodeAnchor(event.currentTarget)}>
Clone <CodeIcon />
</Button> </IconButton>
<Popover <Popover
open={Boolean(codeAnchor)} open={Boolean(codeAnchor)}
anchorEl={codeAnchor} anchorEl={codeAnchor}
@@ -609,9 +567,12 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
}} }}
> >
<Box sx={{ display: 'grid', gap: 1 }}> <Box sx={{ display: 'grid', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<TerminalIcon />
<Typography variant="subtitle2" color="text.secondary"> <Typography variant="subtitle2" color="text.secondary">
Clone Clone
</Typography> </Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ minWidth: 48 }}> <Typography variant="caption" color="text.secondary" sx={{ minWidth: 48 }}>
/git /git
@@ -647,13 +608,11 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
</Box> </Box>
</Popover> </Popover>
</> </>
) : null} )
</Box> }
<Divider sx={{ mb: 1 }} />
<SplitPanel defaultRatio={0.25} left={( const renderPathBreadcrumb = () => {
<TintedPanel> return (<Box sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 0 }}>
<IconButton <IconButton
size="small" size="small"
onClick={() => handleBreadcrumb('')} onClick={() => handleBreadcrumb('')}
@@ -684,8 +643,67 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
</Box> </Box>
) )
})} })}
{selectedFile ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0 }}>
<Typography variant="body2" color="text.secondary" sx={{ mx: 0.25 }}>
/
</Typography>
<Typography variant="body2" sx={{ px: 0.5 }}>
{selectedFile.split('/').pop()}
</Typography>
</Box> </Box>
) : null}
</Box>)
}
return (
<ProjectPageFrame
projectId={projectId ?? ''}
project={project}
currentLabel={repo?.name || 'Repository'}
contextToolbar={
<ContextToolbar
kind="Repository"
label={repo?.name || 'Repository'}
nav={projectId && repoId ? <RepoSubNav projectId={projectId} repoId={repoId} branchRef={ref} repoType={repo?.type} inline /> : null}
/>
}
>
<Divider sx={{ mb: 1 }} />
{loadError ? (
<Typography variant="body2" color="error" sx={{ mb: 1 }}>
{loadError}
</Typography>
) : null}
<SplitPanel defaultRatio={0.25} left={(
<TintedPanel>
<Box sx={{ display: 'flex', alignItems: 'stretch', gap: 1, mb: 1 }}>
<Paper sx={{ p: 1, display: 'flex', alignItems: 'center', gap: 1, minWidth: 240, backgroundColor: 'transparent' }}>
<AccountTreeIcon />
{branches.length ? (
<Autocomplete
freeSolo
options={branches}
value={ref}
inputValue={ref}
onChange={(_, value) => setRef(String(value || ''))}
onInputChange={(_, value, reason) => {
if (reason === 'input') setRef(value)
if (reason === 'clear') setRef('')
}}
renderInput={(params) => <TextField {...params} size="small" placeholder="branch or ref" />}
slotProps={{
popper: { sx: { zIndex: 1400 } }
}}
sx={{ minWidth: 180 }}
/>
) : null}
{renderClonePopover()}
</Paper>
</Box> </Box>
<PageAlert severity="warning" message={treeError} onClose={() => setTreeError(null)} sx={{ mb: 1 }} /> <PageAlert severity="warning" message={treeError} onClose={() => setTreeError(null)} sx={{ mb: 1 }} />
<List dense> <List dense>
<Box sx={{ px: 1, pb: 1 }}> <Box sx={{ px: 1, pb: 1 }}>
@@ -721,7 +739,6 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
<ListItemButton onClick={() => handleOpen(entry)}> <ListItemButton onClick={() => handleOpen(entry)}>
<ListItemText <ListItemText
primary={ primary={
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Tooltip title={entry.type === 'dir' ? 'directory' : 'file'}> <Tooltip title={entry.type === 'dir' ? 'directory' : 'file'}>
<Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ display: 'flex', alignItems: 'center' }}>
@@ -734,8 +751,6 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
</Tooltip> </Tooltip>
<Typography variant="body2">{entry.name}</Typography> <Typography variant="body2">{entry.name}</Typography>
</Box> </Box>
<Box />
</Box>
} }
/> />
</ListItemButton> </ListItemButton>
@@ -745,6 +760,9 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
</TintedPanel> </TintedPanel>
)} right={( )} right={(
<TintedPanel> <TintedPanel>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
{renderPathBreadcrumb()}
</Box>
{selectedFile ? ( {selectedFile ? (
<Box sx={{ mt: 0 }}> <Box sx={{ mt: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
@@ -837,9 +855,6 @@ export default function RepoGitDetailPage(props: RepoGitDetailPageProps) {
</> </>
) )
) : null} ) : null}
<Typography variant="subtitle2" color="text.secondary">
{selectedFile}
</Typography>
</Box> </Box>
</Box> </Box>
{previewTab === 'content' ? ( {previewTab === 'content' ? (