Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31b006000e | |||
| 27ce87574b | |||
| 54f848dab8 |
@@ -294,7 +294,7 @@ func (s *Store) ListProjectGroupRoles(projectID string) ([]models.ProjectGroupRo
|
||||
var items []models.ProjectGroupRole
|
||||
var item models.ProjectGroupRole
|
||||
|
||||
rows, err = s.Query(`SELECT p.public_id, g.public_id, b.role, b.created_at
|
||||
rows, err = s.Query(`SELECT p.public_id, g.public_id, g.name, b.role, b.created_at
|
||||
FROM project_role_bindings b
|
||||
JOIN projects p ON p.id = b.project_id
|
||||
JOIN user_groups g ON g.id = b.subject_id
|
||||
@@ -304,7 +304,7 @@ func (s *Store) ListProjectGroupRoles(projectID string) ([]models.ProjectGroupRo
|
||||
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&item.ProjectID, &item.GroupID, &item.Role, &item.CreatedAt)
|
||||
err = rows.Scan(&item.ProjectID, &item.GroupID, &item.GroupName, &item.Role, &item.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -20,34 +20,24 @@ func (s *Store) GetProjectSummary(projectID string) (models.ProjectSummary, erro
|
||||
return summary, err
|
||||
}
|
||||
|
||||
// Repos = local repos plus foreign repos attached to this project (matching
|
||||
// the repo list). Members counts user role bindings (group roles are listed
|
||||
// separately on the page).
|
||||
// Repos count only owned repositories. Foreign attachments are references
|
||||
// shown in the repository list, not project-owned repository resources.
|
||||
err = s.QueryRow(`SELECT
|
||||
(SELECT COUNT(*) FROM repos WHERE project_id = ?)
|
||||
+ (SELECT COUNT(*) FROM project_repos pr JOIN repos r ON r.id = pr.repo_id WHERE pr.project_id = ? AND r.project_id <> ? AND r.attachable = 1),
|
||||
(SELECT COUNT(*) FROM repos WHERE project_id = ?),
|
||||
(SELECT COUNT(*) FROM boards WHERE project_id = ? AND delete_at = 0),
|
||||
(SELECT COUNT(*) FROM issues WHERE project_id = ?),
|
||||
(SELECT COUNT(*) FROM wiki_pages WHERE project_id = ?),
|
||||
(SELECT COUNT(*) FROM project_role_bindings WHERE project_id = ? AND subject_type = 'user')`,
|
||||
pid, pid, pid, pid, pid, pid, pid).Scan(
|
||||
pid, pid, pid, pid, pid).Scan(
|
||||
&summary.Counts.Repos, &summary.Counts.Boards, &summary.Counts.Issues, &summary.Counts.Wiki, &summary.Counts.Members)
|
||||
if err != nil {
|
||||
return summary, err
|
||||
}
|
||||
|
||||
rows, err = s.Query(`SELECT public_id, name, type, created_at
|
||||
FROM (
|
||||
SELECT r.public_id, r.name, r.type, r.created_at
|
||||
FROM repos r
|
||||
WHERE r.project_id = ?
|
||||
UNION ALL
|
||||
SELECT r.public_id, r.name, r.type, r.created_at
|
||||
FROM project_repos pr
|
||||
JOIN repos r ON r.id = pr.repo_id
|
||||
WHERE pr.project_id = ? AND r.project_id <> ? AND r.attachable = 1
|
||||
)
|
||||
ORDER BY created_at DESC, name LIMIT ?`, pid, pid, pid, projectSummaryRecentLimit)
|
||||
FROM repos
|
||||
WHERE project_id = ?
|
||||
ORDER BY created_at DESC, name LIMIT ?`, pid, projectSummaryRecentLimit)
|
||||
if err != nil {
|
||||
return summary, err
|
||||
}
|
||||
|
||||
@@ -536,16 +536,8 @@ func (s *Store) ListProjectRPMMirrorStatus(projectID string) ([]models.RPMMirror
|
||||
JOIN repos r ON r.id = d.repo_id
|
||||
JOIN projects p ON p.id = r.project_id
|
||||
WHERE d.mode = 'mirror'
|
||||
AND (
|
||||
r.project_id = (SELECT id FROM projects WHERE public_id = ?)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM project_repos pr
|
||||
JOIN projects pp ON pp.id = pr.project_id
|
||||
WHERE pr.repo_id = r.id AND pp.public_id = ? AND r.attachable = 1
|
||||
)
|
||||
)
|
||||
ORDER BY d.sync_running DESC, d.updated_at DESC, p.slug, r.name, d.path`, projectID, projectID)
|
||||
AND r.project_id = (SELECT id FROM projects WHERE public_id = ?)
|
||||
ORDER BY d.sync_running DESC, d.updated_at DESC, p.slug, r.name, d.path`, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1220,7 +1220,14 @@ func (s *Store) ListProjectMembers(projectID string) ([]models.ProjectMember, er
|
||||
var members []models.ProjectMember
|
||||
var m models.ProjectMember
|
||||
|
||||
rows, err = s.Query(`SELECT p.public_id, u.public_id, b.role, b.created_at
|
||||
rows, err = s.Query(`SELECT p.public_id,
|
||||
u.public_id,
|
||||
u.username,
|
||||
u.display_name,
|
||||
u.email,
|
||||
CASE WHEN u.avatar_storage_path != '' THEN '/api/users/' || u.public_id || '/avatar?v=' || u.avatar_updated_at ELSE '' END,
|
||||
b.role,
|
||||
b.created_at
|
||||
FROM project_role_bindings b
|
||||
JOIN projects p ON p.id = b.project_id
|
||||
JOIN users u ON u.id = b.subject_id
|
||||
@@ -1232,7 +1239,7 @@ func (s *Store) ListProjectMembers(projectID string) ([]models.ProjectMember, er
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&m.ProjectID, &m.UserID, &m.Role, &m.CreatedAt)
|
||||
err = rows.Scan(&m.ProjectID, &m.UserID, &m.Username, &m.DisplayName, &m.Email, &m.AvatarURL, &m.Role, &m.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1548,56 +1555,6 @@ func (s *Store) DetachRepoFromProject(projectID string, repoID string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) GetRepoProjectIDs(repoID string) ([]string, error) {
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
var ids []string
|
||||
var id string
|
||||
rows, err = s.Query(`
|
||||
SELECT p.public_id FROM repos r JOIN projects p ON p.id = r.project_id WHERE r.public_id = ?
|
||||
UNION
|
||||
SELECT p.public_id
|
||||
FROM project_repos pr
|
||||
JOIN projects p ON p.id = pr.project_id
|
||||
WHERE pr.repo_id = (SELECT id FROM repos WHERE public_id = ?)
|
||||
`, repoID, repoID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) GetRepoAttachedProjectIDs(repoID string) ([]string, error) {
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
var ids []string
|
||||
var id string
|
||||
|
||||
// returns the list of project public IDs that have attached a repository as a foreign repo
|
||||
rows, err = s.Query(`
|
||||
SELECT p.public_id
|
||||
FROM project_repos pr
|
||||
JOIN projects p ON p.id = pr.project_id
|
||||
WHERE pr.repo_id = (SELECT id FROM repos WHERE public_id = ?)
|
||||
`, repoID)
|
||||
if err != nil { return nil, err }
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&id)
|
||||
if err != nil { return nil, err }
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) ListAvailableReposForProject(projectID string, query string, limit int, offset int) ([]models.Repo, error) {
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
|
||||
@@ -6732,13 +6732,8 @@ func (api *API) requireRepoRole(w http.ResponseWriter, r *http.Request, repoID s
|
||||
var principal models.ServicePrincipal
|
||||
var repo models.Repo
|
||||
var ok bool
|
||||
var projectIDs []string
|
||||
var role string
|
||||
var err error
|
||||
var attachedProjectIDs []string
|
||||
var attachedProjectIDsLoaded bool
|
||||
var attachedProjectIDsErr error
|
||||
var loadAttachedProjectIDs func() ([]string, error)
|
||||
|
||||
user, ok = middleware.UserFromContext(r.Context())
|
||||
if ok && user.IsAdmin { return true }
|
||||
@@ -6748,34 +6743,10 @@ func (api *API) requireRepoRole(w http.ResponseWriter, r *http.Request, repoID s
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return false
|
||||
}
|
||||
loadAttachedProjectIDs = func() ([]string, error) {
|
||||
if !attachedProjectIDsLoaded {
|
||||
attachedProjectIDs, attachedProjectIDsErr = api.store(r).GetRepoAttachedProjectIDs(repo.ID)
|
||||
attachedProjectIDsLoaded = true
|
||||
}
|
||||
return attachedProjectIDs, attachedProjectIDsErr
|
||||
}
|
||||
if ok {
|
||||
// If the user has a role in the repo’s owner project, normal repo permissions apply.
|
||||
role, err = api.store(r).GetProjectRoleForUser(repo.ProjectID, user.ID)
|
||||
if err == nil && roleAllows(role, required) { return true }
|
||||
|
||||
if repo.Attachable && roleAllows(models.RoleViewer, required) {
|
||||
// If the user only has a role in one of the attached projects, access is treated as foreign/attached access.
|
||||
// The following part may look surprosing:
|
||||
// - Attached project permissions can grant read-only access to the repo, if the repo is marked attachable.
|
||||
// - So a user with no role in the owner project can still browse the repo through an attaching project.
|
||||
// The attachable flag is a onwer-controlled flag for read-only sharing.
|
||||
projectIDs, err = loadAttachedProjectIDs()
|
||||
if err == nil {
|
||||
var i int
|
||||
for i = 0; i < len(projectIDs); i++ {
|
||||
role, err = api.store(r).GetProjectRoleForUser(projectIDs[i], user.ID)
|
||||
if err != nil { continue }
|
||||
if roleAllows(role, models.RoleViewer) { return true }
|
||||
}
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return false
|
||||
}
|
||||
@@ -6789,17 +6760,6 @@ func (api *API) requireRepoRole(w http.ResponseWriter, r *http.Request, repoID s
|
||||
|
||||
role, err = api.store(r).GetPrincipalProjectRole(principal.ID, repo.ProjectID)
|
||||
if err == nil && roleAllows(role, required) { return true }
|
||||
if repo.Attachable && roleAllows(models.RoleViewer, required) {
|
||||
projectIDs, err = loadAttachedProjectIDs()
|
||||
if err == nil {
|
||||
var i int
|
||||
for i = 0; i < len(projectIDs); i++ {
|
||||
role, err = api.store(r).GetPrincipalProjectRole(principal.ID, projectIDs[i])
|
||||
if err != nil { continue }
|
||||
if roleAllows(role, models.RoleViewer) { return true }
|
||||
}
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -82,15 +82,20 @@ type Project struct {
|
||||
}
|
||||
|
||||
type ProjectMember struct {
|
||||
ProjectID string `json:"project_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
ProjectID string `json:"project_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username,omitempty"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
AvatarURL string `json:"avatar_url,omitempty"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
type ProjectGroupRole struct {
|
||||
ProjectID string `json:"project_id"`
|
||||
GroupID string `json:"group_id"`
|
||||
GroupName string `json:"group_name,omitempty"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
@@ -482,7 +482,7 @@ Stores repositories belonging to projects.
|
||||
| `description` | Optional short repository description shown in repository lists. |
|
||||
| `type` | Repository type: usually `git`, `rpm`, or `docker`. |
|
||||
| `path` | Storage path. |
|
||||
| `attachable` | Boolean flag allowing other projects to attach this repository as read-only foreign content. When off, existing attachments do not grant access. |
|
||||
| `attachable` | Boolean flag allowing other projects to attach this repository as a foreign reference. Attachments do not grant repository access; repository access still uses the origin project permissions. |
|
||||
| `signing_key_id` | Optional internal FK to `gpg_keys`; used for Git/RPM signing. `NULL` means unsigned. |
|
||||
| `created_by` | Internal user FK. |
|
||||
| `created_at` | Creation time. |
|
||||
@@ -517,7 +517,7 @@ Deleting an owning user or project cascades to scoped keys; deleting a key clear
|
||||
|
||||
### `project_repos`
|
||||
|
||||
Join table for project-to-repository visibility/attachment, including foreign repository attachment. Foreign attachments grant viewer-only access and are effective only while the repository's `attachable` flag is enabled.
|
||||
Join table for project-to-repository visibility/attachment, including foreign repository attachment. Foreign attachments are references to origin-project repositories; they do not grant repository read/write permission by themselves.
|
||||
|
||||
| Column | Purpose |
|
||||
| --- | --- |
|
||||
|
||||
+10
-1
@@ -12292,7 +12292,16 @@ components:
|
||||
description: Optional GPG signing key ID for Git/RPM repositories.
|
||||
attachable:
|
||||
type: boolean
|
||||
description: Allows other projects to attach this repository as read-only foreign content.
|
||||
description: Allows other projects to attach this repository as a foreign reference. Access still follows origin project permissions.
|
||||
owner_project_name:
|
||||
type: string
|
||||
description: Display name of the origin project when returned with repository list/detail responses.
|
||||
owner_project:
|
||||
type: string
|
||||
description: Public ID of the origin project when returned with repository list/detail responses.
|
||||
owner_slug:
|
||||
type: string
|
||||
description: Slug of the origin project when returned with repository list/detail responses.
|
||||
additionalProperties: true
|
||||
Issue:
|
||||
type: object
|
||||
|
||||
@@ -338,6 +338,7 @@ export interface AvailableRepo {
|
||||
project_id: string
|
||||
name: string
|
||||
type?: 'git' | 'rpm' | 'docker'
|
||||
owner_project_name: string
|
||||
owner_project: string
|
||||
owner_slug: string
|
||||
}
|
||||
@@ -405,12 +406,17 @@ export interface Upload {
|
||||
export interface ProjectMember {
|
||||
project_id: string
|
||||
user_id: string
|
||||
username?: string
|
||||
display_name?: string
|
||||
email?: string
|
||||
avatar_url?: string
|
||||
role: string
|
||||
}
|
||||
|
||||
export interface ProjectGroupRole {
|
||||
project_id: string
|
||||
group_id: string
|
||||
group_name?: string
|
||||
role: string
|
||||
created_at: number
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Chip, Tooltip } from '@mui/material'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Repo } from '../api'
|
||||
import { repoIsForeignContext } from '../repoAccess'
|
||||
|
||||
type RepoForeignChipProps = {
|
||||
projectId: string | undefined
|
||||
repo: Repo | null | undefined
|
||||
}
|
||||
|
||||
export default function RepoForeignChip(props: RepoForeignChipProps) {
|
||||
const location = useLocation()
|
||||
const { projectId, repo } = props
|
||||
const foreignContext: boolean = repoIsForeignContext(projectId, repo)
|
||||
let originLabel: string
|
||||
let currentRoot: string
|
||||
let originRoot: string
|
||||
let pathname: string
|
||||
let targetPath: string
|
||||
let target: string
|
||||
let title: string
|
||||
|
||||
if (!foreignContext || !repo) return null
|
||||
|
||||
originLabel = repo.owner_project_name || repo.owner_slug || 'origin'
|
||||
currentRoot = projectId ? `/projects/${projectId}/repos/${repo.id}` : ''
|
||||
originRoot = `/projects/${repo.project_id}/repos/${repo.id}`
|
||||
pathname = location.pathname
|
||||
targetPath = currentRoot && pathname.startsWith(currentRoot)
|
||||
? originRoot + pathname.slice(currentRoot.length)
|
||||
: originRoot
|
||||
target = `${targetPath}${location.search}${location.hash}`
|
||||
title = `Origin project: ${originLabel}. Open this repository page in the origin project context.`
|
||||
|
||||
return (
|
||||
<Tooltip title={title}>
|
||||
<Chip
|
||||
component={Link}
|
||||
to={target}
|
||||
clickable
|
||||
label={`foreign: ${originLabel}`}
|
||||
size="small"
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@@ -412,10 +412,15 @@ export default function ProjectReposPage() {
|
||||
void navigator.clipboard.writeText(value)
|
||||
}
|
||||
|
||||
function repoAccessible(repo: Repo): boolean {
|
||||
function repoAttachmentEnabled(repo: Repo): boolean {
|
||||
return !repo.is_foreign || Boolean(repo.attachable)
|
||||
}
|
||||
|
||||
function repoOpenProjectId(repo: Repo): string {
|
||||
if (repo.is_foreign) return repo.owner_project || repo.project_id
|
||||
return projectId || repo.project_id
|
||||
}
|
||||
|
||||
const columns: RSTColumn<Repo>[] = [
|
||||
{
|
||||
key: 'select',
|
||||
@@ -440,13 +445,13 @@ export default function ProjectReposPage() {
|
||||
minWidth: 100,
|
||||
getValue: (repo: Repo) => repo.name,
|
||||
renderCell: (repo: Repo) => {
|
||||
const accessible: boolean = repoAccessible(repo)
|
||||
const attachmentEnabled: boolean = repoAttachmentEnabled(repo)
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, minWidth: 0 }}>
|
||||
{accessible ? (
|
||||
{attachmentEnabled ? (
|
||||
<Box
|
||||
component={Link}
|
||||
to={`/projects/${projectId}/repos/${repo.id}`}
|
||||
to={`/projects/${repoOpenProjectId(repo)}/repos/${repo.id}`}
|
||||
sx={{
|
||||
display: 'block',
|
||||
textDecoration: 'none',
|
||||
@@ -500,14 +505,14 @@ export default function ProjectReposPage() {
|
||||
},
|
||||
renderCell: (repo: Repo) => {
|
||||
const access: RepoAccessInfo = repoAccess(repo)
|
||||
const accessible: boolean = repoAccessible(repo)
|
||||
const showCopy: boolean = !repo.is_foreign && Boolean(access.copyValue)
|
||||
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 && accessible ? (
|
||||
{showCopy ? (
|
||||
<Tooltip title="Copy access URL">
|
||||
<IconButton
|
||||
size="small"
|
||||
@@ -531,11 +536,11 @@ export default function ProjectReposPage() {
|
||||
label: 'Status',
|
||||
defaultWidth: 130,
|
||||
minWidth: 80,
|
||||
getValue: (repo: Repo) => `${repo.is_foreign ? 'foreign' : 'local'} ${repo.owner_slug || ''} ${repoAccessible(repo) ? 'accessible' : 'not accessible'}`,
|
||||
getValue: (repo: Repo) => `${repo.is_foreign ? 'foreign' : 'local'} ${repo.owner_slug || ''} ${repoAttachmentEnabled(repo) ? 'attached' : 'not accessible'}`,
|
||||
renderCell: (repo: Repo) => (
|
||||
repo.is_foreign ? (
|
||||
<Tooltip title={repoAccessible(repo) ? (repo.owner_slug ? `Owned by ${repo.owner_slug}` : 'Foreign repository') : 'The owner turned off attachment for this repository.'}>
|
||||
<Chip label={repoAccessible(repo) ? 'foreign' : 'not accessible'} size="small" color={repoAccessible(repo) ? 'warning' : 'default'} variant="outlined" />
|
||||
<Tooltip title={repoAttachmentEnabled(repo) ? (repo.owner_slug ? `Attached from ${repo.owner_slug}. Opens in the origin project context.` : 'Attached foreign repository') : 'The owner turned off attachment for this repository.'}>
|
||||
<Chip label={repoAttachmentEnabled(repo) ? 'foreign' : 'not accessible'} size="small" color={repoAttachmentEnabled(repo) ? 'warning' : 'default'} variant="outlined" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Chip label="local" size="small" variant="outlined" />
|
||||
|
||||
@@ -283,17 +283,19 @@ export default function ProjectSettingsPage() {
|
||||
|
||||
const memberName = (userID: string) => {
|
||||
const user = memberUsers.find((item) => item.id === userID)
|
||||
if (!user) return userID
|
||||
if (!user) return 'Unknown user'
|
||||
if (user.display_name) return `${user.display_name} (${user.username})`
|
||||
return user.username
|
||||
}
|
||||
|
||||
const groupName = (groupID: string) => {
|
||||
const group = allGroups.find((item) => item.id === groupID)
|
||||
if (!group) return groupID
|
||||
if (!group) return 'Unknown group'
|
||||
return group.name
|
||||
}
|
||||
|
||||
const projectActionTitle: string = canManageMembers ? '' : 'Project admin permission is required'
|
||||
|
||||
return (
|
||||
<ProjectPageFrame projectId={projectId ?? ''} project={project} currentLabel="Settings">
|
||||
<Box sx={{ display: 'grid', gap: 1 }}>
|
||||
@@ -301,12 +303,16 @@ export default function ProjectSettingsPage() {
|
||||
project={project}
|
||||
actions={
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
|
||||
<HeaderActionButton variant="outlined" startIcon={<EditIcon />} onClick={openEdit} disabled={!project}>
|
||||
Edit Project
|
||||
</HeaderActionButton>
|
||||
<HeaderActionButton variant="outlined" color="error" startIcon={<DeleteOutlinedIcon />} onClick={openDelete} disabled={!project}>
|
||||
Delete Project
|
||||
</HeaderActionButton>
|
||||
<span title={projectActionTitle}>
|
||||
<HeaderActionButton variant="outlined" startIcon={<EditIcon />} onClick={openEdit} disabled={!project || !canManageMembers}>
|
||||
Edit Project
|
||||
</HeaderActionButton>
|
||||
</span>
|
||||
<span title={projectActionTitle}>
|
||||
<HeaderActionButton variant="outlined" color="error" startIcon={<DeleteOutlinedIcon />} onClick={openDelete} disabled={!project || !canManageMembers}>
|
||||
Delete Project
|
||||
</HeaderActionButton>
|
||||
</span>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
@@ -321,12 +327,15 @@ export default function ProjectSettingsPage() {
|
||||
updatingId={updatingMemberUserID}
|
||||
removingId={removingMemberUserID}
|
||||
items={members.map((member) => {
|
||||
const user = memberUsers.find((item) => item.id === member.user_id)
|
||||
const user: User | undefined = memberUsers.find((item) => item.id === member.user_id)
|
||||
const displayName: string = member.display_name || user?.display_name || ''
|
||||
const username: string = member.username || user?.username || ''
|
||||
const email: string = member.email || user?.email || ''
|
||||
return {
|
||||
id: member.user_id,
|
||||
primary: user?.display_name || user?.username || memberName(member.user_id),
|
||||
secondary: user ? (user.display_name ? user.username : user.email) : undefined,
|
||||
avatarUrl: user?.avatar_url,
|
||||
primary: displayName || username || memberName(member.user_id),
|
||||
secondary: displayName ? username : email,
|
||||
avatarUrl: member.avatar_url || user?.avatar_url,
|
||||
role: member.role,
|
||||
}
|
||||
})}
|
||||
@@ -349,7 +358,7 @@ export default function ProjectSettingsPage() {
|
||||
removingId={removingGroupRoleID}
|
||||
items={groupRoles.map((groupRole) => ({
|
||||
id: groupRole.group_id,
|
||||
primary: groupName(groupRole.group_id),
|
||||
primary: groupRole.group_name || groupName(groupRole.group_id),
|
||||
role: groupRole.role,
|
||||
isGroup: true,
|
||||
}))}
|
||||
|
||||
@@ -13,19 +13,56 @@ export default function RepoDetailPage() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!repoId) return
|
||||
setLoading(true)
|
||||
setLoadError(null)
|
||||
api
|
||||
.getRepo(repoId)
|
||||
.then((data) => setRepo(data))
|
||||
.catch((err) => {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load repository'
|
||||
let cancelled: boolean
|
||||
const load: () => Promise<void> = async () => {
|
||||
let data: Repo
|
||||
let message: string
|
||||
let items: Repo[]
|
||||
let found: Repo | undefined
|
||||
|
||||
if (!repoId) return
|
||||
setLoading(true)
|
||||
setLoadError(null)
|
||||
try {
|
||||
data = await api.getRepo(repoId)
|
||||
if (cancelled) return
|
||||
setRepo(data)
|
||||
} catch (err) {
|
||||
message = err instanceof Error ? err.message : 'Failed to load repository'
|
||||
if (!projectId) {
|
||||
if (cancelled) return
|
||||
setLoadError(message)
|
||||
setRepo(null)
|
||||
return
|
||||
}
|
||||
try {
|
||||
items = await api.listRepos(projectId)
|
||||
if (cancelled) return
|
||||
found = items.find((item: Repo) => item.id === repoId)
|
||||
if (found) {
|
||||
setRepo(found)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// Keep the original repo-load failure; the fallback only exists
|
||||
// to render attached-path errors with normal repo page chrome.
|
||||
}
|
||||
if (cancelled) return
|
||||
setLoadError(message)
|
||||
setRepo(null)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [repoId])
|
||||
} finally {
|
||||
if (cancelled) return
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!repoId) return
|
||||
cancelled = false
|
||||
void load()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [repoId, projectId])
|
||||
|
||||
if (!repoId || !projectId) {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Repo } from './api'
|
||||
|
||||
export const foreignRepoUnavailableMessage: string = 'This attached repository is no longer accessible because the owner turned off attachment.'
|
||||
export const foreignRepoUnavailableMessage: string = 'Open this attached repository from the origin project context.'
|
||||
|
||||
export function repoIsForeignContext(projectId: string | undefined, repo: Repo | null | undefined): boolean {
|
||||
if (!projectId || !repo) return false
|
||||
@@ -9,5 +9,5 @@ export function repoIsForeignContext(projectId: string | undefined, repo: Repo |
|
||||
|
||||
export function repoContextAccessError(projectId: string | undefined, repo: Repo | null | undefined): string | null {
|
||||
if (!repoIsForeignContext(projectId, repo)) return null
|
||||
return repo?.attachable ? null : foreignRepoUnavailableMessage
|
||||
return foreignRepoUnavailableMessage
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user