Skip to content

Commit 8557c7c

Browse files
committed
feat: add package access/collaborators page
Gives /package/<name>/access its own dedicated page instead of just being buried in the sidebar. Shows the existing AccessControls component when the npm connector is running, or a connect prompt otherwise. - New page at /package/[[org]]/[name]/access.vue - Access tab added to the package navigation header - i18n keys for the page title, subtitle, and connect prompt - README updated, removed /access from "Not yet supported" URL list
1 parent 7f2fc1a commit 8557c7c

File tree

5 files changed

+137
-5
lines changed

5 files changed

+137
-5
lines changed

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ What npmx offers:
5757
- **Version range resolution** &ndash; dependency ranges (e.g., `^1.0.0`) resolve to actual installed versions
5858
- **Claim new packages** &ndash; register new package names directly from search results (via local connector)
5959
- **Clickable version tags** &ndash; navigate directly to any version from the versions list
60+
- **Package access management** &ndash; view and manage team/collaborator access for scoped packages (via local connector)
6061

6162
### User & org pages
6263

@@ -91,7 +92,7 @@ What npmx offers:
9192
| Multi-provider repo support |||
9293
| Version range resolution |||
9394
| Dependents list || 🚧 |
94-
| Package admin (access/owners) || 🚧 |
95+
| Package admin (access/owners) || |
9596
| Org/team management || 🚧 |
9697
| 2FA/account settings |||
9798
| Claim new package names |||
@@ -118,8 +119,6 @@ npmx.dev supports npm permalinks &ndash; just replace `npmjs.com` with `npmx.dev
118119
119120
#### Not yet supported
120121

121-
- `/package/<name>/access` &ndash; package access settings
122-
- `/package/<name>/dependents` &ndash; dependent packages list
123122
- `/settings/*` &ndash; account settings pages
124123

125124
### Simpler URLs

app/components/Package/Header.vue

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const props = defineProps<{
1010
latestVersion?: SlimVersion | null
1111
provenanceData?: ProvenanceDetails | null
1212
provenanceStatus?: string | null
13-
page: 'main' | 'docs' | 'code' | 'diff'
13+
page: 'main' | 'docs' | 'code' | 'diff' | 'access'
1414
versionUrlPattern: string
1515
}>()
1616
@@ -108,6 +108,18 @@ const mainLink = computed((): RouteLocationRaw | null => {
108108
return packageRoute(props.pkg.name, props.resolvedVersion)
109109
})
110110
111+
const accessLink = computed((): RouteLocationRaw | null => {
112+
if (props.pkg == null) return null
113+
const split = props.pkg.name.split('/')
114+
return {
115+
name: 'package-access',
116+
params: {
117+
org: split.length === 2 ? split[0] : undefined,
118+
name: split.length === 2 ? split[1]! : split[0]!,
119+
},
120+
}
121+
})
122+
111123
const diffLink = computed((): RouteLocationRaw | null => {
112124
if (
113125
props.pkg == null ||
@@ -343,6 +355,14 @@ const fundingUrl = computed(() => {
343355
>
344356
{{ $t('compare.compare_versions') }}
345357
</LinkBase>
358+
<LinkBase
359+
v-if="accessLink"
360+
:to="accessLink"
361+
class="decoration-none border-b-2 p-1 hover:border-accent/50 focus-visible:[outline-offset:-2px]!"
362+
:class="page === 'access' ? 'border-accent text-accent!' : 'border-transparent'"
363+
>
364+
{{ $t('package.links.access') }}
365+
</LinkBase>
346366
</nav>
347367
</div>
348368
</div>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<script setup lang="ts">
2+
definePageMeta({
3+
name: 'package-access',
4+
scrollMargin: 200,
5+
})
6+
7+
const route = useRoute('package-access')
8+
9+
const packageName = computed(() => {
10+
const { org, name } = route.params
11+
return org ? `${org}/${name}` : name
12+
})
13+
14+
const { data: pkg } = usePackage(packageName)
15+
16+
const resolvedVersion = computed(() => {
17+
const latest = pkg.value?.['dist-tags']?.latest
18+
if (!latest) return null
19+
return latest
20+
})
21+
22+
const displayVersion = computed(() => pkg.value?.requestedVersion ?? null)
23+
24+
const latestVersion = computed(() => {
25+
if (!pkg.value) return null
26+
const latestTag = pkg.value['dist-tags']?.latest
27+
if (!latestTag) return null
28+
return pkg.value.versions[latestTag] ?? null
29+
})
30+
31+
const versionUrlPattern = computed(() => {
32+
const split = packageName.value.split('/')
33+
if (split.length === 2) {
34+
return `/package/${split[0]}/${split[1]}/v/{version}`
35+
}
36+
return `/package/${packageName.value}/v/{version}`
37+
})
38+
39+
const { isConnected } = useConnector()
40+
const connectorModal = import.meta.client ? useModal('connector-modal') : null
41+
42+
useSeoMeta({
43+
title: () => `Access - ${packageName.value} - npmx`,
44+
description: () => `Manage access and collaborators for ${packageName.value}`,
45+
})
46+
</script>
47+
48+
<template>
49+
<main class="flex-1 pb-8">
50+
<PackageHeader
51+
:pkg="pkg ?? null"
52+
:resolved-version="resolvedVersion"
53+
:display-version="displayVersion"
54+
:latest-version="latestVersion"
55+
:version-url-pattern="versionUrlPattern"
56+
page="access"
57+
/>
58+
59+
<div class="container py-6">
60+
<h1 class="font-mono text-xl font-semibold mb-1">
61+
{{ $t('package.access.page_title') }}
62+
</h1>
63+
<p class="text-sm text-fg-muted mb-6">
64+
{{ $t('package.access.page_subtitle', { name: packageName }) }}
65+
</p>
66+
67+
<ClientOnly>
68+
<template v-if="isConnected">
69+
<PackageAccessControls :package-name="packageName" />
70+
</template>
71+
<template v-else>
72+
<div class="py-12 text-center">
73+
<span class="i-lucide:lock w-12 h-12 mx-auto mb-4 text-fg-subtle block" />
74+
<p class="text-fg-muted mb-2 font-medium">
75+
{{ $t('package.access.connect_required') }}
76+
</p>
77+
<p class="text-sm text-fg-subtle mb-6">
78+
{{ $t('package.access.connect_hint') }}
79+
</p>
80+
<ButtonBase variant="primary" @click="connectorModal?.open()">
81+
{{ $t('connector.modal.connect') }}
82+
</ButtonBase>
83+
</div>
84+
</template>
85+
<template #fallback>
86+
<div class="space-y-4">
87+
<SkeletonInline v-for="i in 4" :key="i" class="h-12 w-full rounded-md" />
88+
</div>
89+
</template>
90+
</ClientOnly>
91+
</div>
92+
</main>
93+
</template>

i18n/locales/en.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,8 @@
318318
"docs": "docs",
319319
"fund": "fund",
320320
"compare": "compare",
321-
"compare_this_package": "compare this package"
321+
"compare_this_package": "compare this package",
322+
"access": "access"
322323
},
323324
"likes": {
324325
"like": "Like this package",
@@ -587,6 +588,10 @@
587588
"show_all": "show {count} deprecated package | show all {count} deprecated packages"
588589
},
589590
"access": {
591+
"page_title": "Access & Collaborators",
592+
"page_subtitle": "Manage team and collaborator access for {name}",
593+
"connect_required": "Connect your npm CLI to manage access",
594+
"connect_hint": "Use the local connector to view and manage package collaborators and team access.",
590595
"title": "Team Access",
591596
"refresh": "Refresh team access",
592597
"list_label": "Team access list",

i18n/schema.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,9 @@
960960
},
961961
"compare_this_package": {
962962
"type": "string"
963+
},
964+
"access": {
965+
"type": "string"
963966
}
964967
},
965968
"additionalProperties": false
@@ -1765,6 +1768,18 @@
17651768
"access": {
17661769
"type": "object",
17671770
"properties": {
1771+
"page_title": {
1772+
"type": "string"
1773+
},
1774+
"page_subtitle": {
1775+
"type": "string"
1776+
},
1777+
"connect_required": {
1778+
"type": "string"
1779+
},
1780+
"connect_hint": {
1781+
"type": "string"
1782+
},
17681783
"title": {
17691784
"type": "string"
17701785
},

0 commit comments

Comments
 (0)