Compare commits

..

2 Commits

Author SHA1 Message Date
hyung-hwan a59eb48b74 renamed the repos table's source column to Access.
enahnced the table's Status column output
2026-06-15 22:32:31 +09:00
hyung-hwan 3c57ccf0fd switch to use icon-only buttons for rows actions in repospage and boardspage 2026-06-15 22:13:44 +09:00
3 changed files with 121 additions and 58 deletions
@@ -1,6 +1,9 @@
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import { ButtonProps } from '@mui/material/Button'
import IconButton from '@mui/material/IconButton'
import { IconButtonProps } from '@mui/material/IconButton'
import Tooltip from '@mui/material/Tooltip'
import { ReactNode } from 'react'
type ListRowActionsProps = {
@@ -8,6 +11,9 @@ type ListRowActionsProps = {
}
type ListRowActionButtonProps = Omit<ButtonProps, 'size' | 'variant'>
type ListRowIconActionProps = Omit<IconButtonProps, 'size'> & {
title: string
}
export function ListRowActions(props: ListRowActionsProps) {
return (
@@ -31,3 +37,15 @@ export function ListRowActions(props: ListRowActionsProps) {
export function ListRowActionButton(props: ListRowActionButtonProps) {
return <Button variant="outlined" size="small" {...props} />
}
export function ListRowIconAction({ title, children, ...buttonProps }: ListRowIconActionProps) {
return (
<Tooltip title={title}>
<span>
<IconButton size="small" aria-label={title} {...buttonProps}>
{children}
</IconButton>
</span>
</Tooltip>
)
}
+12 -24
View File
@@ -1,10 +1,8 @@
import AddIcon from '@mui/icons-material/Add'
import DeleteIcon from '@mui/icons-material/Delete'
import EditIcon from '@mui/icons-material/Edit'
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import {
Box,
Button,
Typography,
} from '@mui/material'
import { useEffect, useState } from 'react'
@@ -13,7 +11,7 @@ import { api, Board } from '../api'
import BoardFormDialog from '../components/BoardFormDialog'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
import HeaderActionButton from '../components/HeaderActionButton'
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
import { ListRowActions, ListRowIconAction } from '../components/ListRowActions'
import PaginationControls from '../components/PaginationControls'
import PageAlert from '../components/PageAlert'
import ProjectPageFrame from '../components/ProjectPageFrame'
@@ -122,7 +120,6 @@ export default function ProjectBoardsPage() {
display: 'block',
textDecoration: 'none',
color: 'primary.main',
fontWeight: 600,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
@@ -147,35 +144,26 @@ export default function ProjectBoardsPage() {
{
key: 'actions',
label: 'Actions',
defaultWidth: 300,
minWidth: 300,
defaultWidth: 96,
minWidth: 96,
sortable: false,
resizable: false,
cellSx: { textAlign: 'right', whiteSpace: 'nowrap' },
renderCell: (board: Board) => (
<ListRowActions>
<Button
component={Link}
to={`/projects/${projectId}/boards/${board.id}`}
variant="outlined"
size="small"
startIcon={<OpenInNewIcon />}
>
Open
</Button>
<ListRowActionButton
startIcon={<EditIcon />}
<ListRowIconAction
title="Edit board"
onClick={() => { setEditBoard(board); setEditError(null) }}
>
Edit
</ListRowActionButton>
<ListRowActionButton
startIcon={<DeleteIcon />}
<EditIcon fontSize="small" />
</ListRowIconAction>
<ListRowIconAction
title="Delete board"
color="error"
onClick={() => { setDeleteBoard(board); setDeleteError(null); setDeleteConfirmValue('') }}
>
Delete
</ListRowActionButton>
<DeleteIcon fontSize="small" />
</ListRowIconAction>
</ListRowActions>
),
},
@@ -188,7 +176,7 @@ export default function ProjectBoardsPage() {
title="Boards"
actions={
<HeaderActionButton variant="outlined" startIcon={<AddIcon />} onClick={() => setCreateOpen(true)}>
New Board
New
</HeaderActionButton>
}
>
+91 -34
View File
@@ -1,4 +1,5 @@
import AddIcon from '@mui/icons-material/Add'
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
import DeleteIcon from '@mui/icons-material/Delete'
import EditIcon from '@mui/icons-material/Edit'
import LinkOffIcon from '@mui/icons-material/LinkOff'
@@ -9,23 +10,31 @@ import {
Box,
Chip,
Checkbox,
IconButton,
MenuItem,
TextField,
Tooltip,
Typography
} from '@mui/material'
import { useEffect, useState } from 'react'
import { MouseEvent, useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import PageAlert from '../components/PageAlert'
import { api, AvailableRepo, Project, Repo, RepoStats } from '../api'
import ProjectPageFrame from '../components/ProjectPageFrame'
import HeaderActionButton from '../components/HeaderActionButton'
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
import { ListRowActions, ListRowIconAction } from '../components/ListRowActions'
import PaginationControls from '../components/PaginationControls'
import ResizableSortableTable, { RSTColumn } from '../components/ResizableSortableTable'
import RepositoryFormDialog from '../components/RepositoryFormDialog'
import SectionCard from '../components/SectionCard'
import SelectField from '../components/SelectField'
type RepoAccessInfo = {
label: string
displayValue: string
copyValue: string
}
export default function ReposPage() {
const { projectId } = useParams()
const [repos, setRepos] = useState<Repo[]>([])
@@ -337,10 +346,32 @@ export default function ReposPage() {
}
}
function repoSource(repo: Repo): string {
if (repo.type === 'rpm') return repo.rpm_url || ''
if (repo.type === 'docker') return repo.docker_url || ''
return repo.clone_url || ''
function repoAccess(repo: Repo): RepoAccessInfo {
let value: string
if (repo.type === 'rpm') {
value = repo.rpm_url || ''
return { label: 'dnf/yum', displayValue: value, copyValue: fullAccessURL(value) }
}
if (repo.type === 'docker') {
value = repo.docker_url || ''
return { label: 'docker pull', displayValue: value, copyValue: fullAccessURL(value) }
}
value = repo.clone_url || ''
return { label: 'git clone', displayValue: value, copyValue: fullAccessURL(value) }
}
function fullAccessURL(value: string): string {
const trimmed: string = value.trim()
if (!trimmed) return ''
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return trimmed
if (trimmed.startsWith('//')) return `${window.location.protocol}${trimmed}`
if (trimmed.startsWith('/')) return `${window.location.origin}${trimmed}`
return `${window.location.origin}/${trimmed}`
}
function copyRepoAccess(value: string) {
if (!value || !navigator.clipboard || !navigator.clipboard.writeText) return
void navigator.clipboard.writeText(value)
}
const columns: RSTColumn<Repo>[] = [
@@ -363,7 +394,7 @@ export default function ReposPage() {
{
key: 'name',
label: 'Name',
defaultWidth: 200,
defaultWidth: 150,
minWidth: 100,
getValue: (repo: Repo) => repo.name,
renderCell: (repo: Repo) => (
@@ -375,7 +406,6 @@ export default function ReposPage() {
display: 'block',
textDecoration: 'none',
color: 'primary.main',
fontWeight: 600,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
@@ -397,7 +427,7 @@ export default function ReposPage() {
{
key: 'description',
label: 'Description',
defaultWidth: 300,
defaultWidth: 250,
minWidth: 160,
getValue: (repo: Repo) => repo.description || '',
renderCell: (repo: Repo) => (
@@ -407,16 +437,40 @@ export default function ReposPage() {
),
},
{
key: 'source',
label: 'Source',
defaultWidth: 200,
minWidth: 150,
getValue: (repo: Repo) => repoSource(repo),
renderCell: (repo: Repo) => (
<Typography variant="body2" color="text.secondary" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{repoSource(repo) || '-'}
</Typography>
),
key: 'access',
label: 'Access',
defaultWidth: 280,
minWidth: 180,
getValue: (repo: Repo) => {
const access: RepoAccessInfo = repoAccess(repo)
return `${access.label} ${access.displayValue}`
},
renderCell: (repo: Repo) => {
const access: RepoAccessInfo = repoAccess(repo)
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, minWidth: 0 }}>
<Chip label={access.label} size="small" variant="outlined" sx={{ flexShrink: 0 }} />
<Typography variant="body2" color="text.secondary" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 }}>
{access.displayValue || '-'}
</Typography>
{access.copyValue ? (
<Tooltip title="Copy access URL">
<IconButton
size="small"
aria-label="Copy access URL"
onClick={(event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation()
copyRepoAccess(access.copyValue)
}}
sx={{ flexShrink: 0 }}
>
<ContentCopyIcon fontSize="small" />
</IconButton>
</Tooltip>
) : null}
</Box>
)
},
},
{
key: 'status',
@@ -425,35 +479,38 @@ export default function ReposPage() {
minWidth: 50,
getValue: (repo: Repo) => `${repo.is_foreign ? 'foreign' : 'local'} ${repo.owner_slug || ''}`,
renderCell: (repo: Repo) => (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}>
{repo.is_foreign ? <Chip label="foreign" size="small" color="warning" variant="outlined" /> : <Chip label="local" size="small" variant="outlined" />}
{repo.is_foreign && repo.owner_slug ? <Chip label={repo.owner_slug} size="small" variant="outlined" /> : null}
</Box>
repo.is_foreign ? (
<Tooltip title={repo.owner_slug ? `Owned by ${repo.owner_slug}` : 'Foreign repository'}>
<Chip label="foreign" size="small" color="warning" variant="outlined" />
</Tooltip>
) : (
<Chip label="local" size="small" variant="outlined" />
)
),
},
{
key: 'actions',
label: 'Actions',
defaultWidth: 210,
minWidth: 210,
defaultWidth: 96,
minWidth: 96,
sortable: false,
resizable: false,
cellSx: { textAlign: 'right', whiteSpace: 'nowrap' },
renderCell: (repo: Repo) => (
!repo.is_foreign ? (
<ListRowActions>
<ListRowActionButton startIcon={<EditIcon />} onClick={() => openEdit(repo)}>
Edit
</ListRowActionButton>
<ListRowActionButton startIcon={<DeleteIcon />} color="error" onClick={() => openDelete(repo)}>
Delete
</ListRowActionButton>
<ListRowIconAction title="Edit repository" onClick={() => openEdit(repo)}>
<EditIcon fontSize="small" />
</ListRowIconAction>
<ListRowIconAction title="Delete repository" color="error" onClick={() => openDelete(repo)}>
<DeleteIcon fontSize="small" />
</ListRowIconAction>
</ListRowActions>
) : (
<ListRowActions>
<ListRowActionButton startIcon={<LinkOffIcon />} onClick={() => openDetachForeign(repo)}>
Detach
</ListRowActionButton>
<ListRowIconAction title="Detach repository" onClick={() => openDetachForeign(repo)}>
<LinkOffIcon fontSize="small" />
</ListRowIconAction>
</ListRowActions>
)
),