@@ -35,6 +35,12 @@ const selectedTeam = shallowRef('')
3535const permission = shallowRef <' read-only' | ' read-write' >(' read-only' )
3636const isGranting = shallowRef (false )
3737
38+ // Revoke confirmation state
39+ const revokeDialogRef = useTemplateRef (' revokeDialogRef' )
40+ const revokeTarget = shallowRef <{ name: string ; displayName: string } | null >(null )
41+ const isRevoking = shallowRef (false )
42+ const revokeError = shallowRef <string | null >(null )
43+
3844// Computed collaborator list with type detection
3945const collaboratorList = computed (() => {
4046 return Object .entries (collaborators .value )
@@ -117,23 +123,49 @@ async function handleGrantAccess() {
117123 }
118124}
119125
120- // Revoke access
121- async function handleRevokeAccess(collaboratorName : string ) {
122- // For teams, we use the full org:team format
123- // For users... actually npm access revoke only works for teams
124- // Users get access via maintainers/owners which is managed separately
125-
126- const operation: NewOperation = {
127- type: ' access:revoke' ,
128- params: {
129- scopeTeam: collaboratorName ,
130- pkg: props .packageName ,
131- },
132- description: ` Revoke ${collaboratorName } access to ${props .packageName } ` ,
133- command: ` npm access revoke ${collaboratorName } ${props .packageName } ` ,
134- }
126+ // Open revoke confirmation dialog
127+ function openRevokeDialog(collaboratorName : string , displayName : string ) {
128+ revokeTarget .value = { name: collaboratorName , displayName }
129+ revokeError .value = null
130+ revokeDialogRef .value ?.showModal ()
131+ }
132+
133+ // Close revoke confirmation dialog
134+ function closeRevokeDialog() {
135+ revokeDialogRef .value ?.close ()
136+ revokeTarget .value = null
137+ revokeError .value = null
138+ }
139+
140+ // Revoke access (after confirmation)
141+ async function handleRevokeAccess() {
142+ if (! revokeTarget .value ) return
143+
144+ isRevoking .value = true
145+ revokeError .value = null
146+
147+ try {
148+ const operation: NewOperation = {
149+ type: ' access:revoke' ,
150+ params: {
151+ scopeTeam: revokeTarget .value .name ,
152+ pkg: props .packageName ,
153+ },
154+ description: ` Revoke ${revokeTarget .value .name } access to ${props .packageName } ` ,
155+ command: ` npm access revoke ${revokeTarget .value .name } ${props .packageName } ` ,
156+ }
135157
136- await addOperation (operation )
158+ const result = await addOperation (operation )
159+ if (result ) {
160+ closeRevokeDialog ()
161+ } else {
162+ revokeError .value = connectorError .value || ' Failed to queue revoke operation'
163+ }
164+ } catch (err ) {
165+ revokeError .value = err instanceof Error ? err .message : ' Failed to revoke access'
166+ } finally {
167+ isRevoking .value = false
168+ }
137169}
138170
139171// Reload when package changes
@@ -227,7 +259,7 @@ watch(
227259 type =" button"
228260 class =" p-1 text-fg-subtle hover:text-red-400 transition-colors duration-200 shrink-0 rounded focus-visible:outline-accent/70"
229261 :aria-label =" $t('package.access.revoke_access', { name: collab.displayName })"
230- @click =" handleRevokeAccess( collab.name)"
262+ @click =" openRevokeDialog(collab.name, collab.displayName ?? collab.name)"
231263 >
232264 <span class =" i-lucide:x w-3.5 h-3.5" aria-hidden =" true" />
233265 </button >
@@ -303,4 +335,70 @@ watch(
303335 {{ $t('package.access.grant_access') }}
304336 </button >
305337 </section >
338+
339+ <!-- Revoke Confirmation Modal -->
340+ <ClientOnly >
341+ <Modal
342+ ref =" revokeDialogRef"
343+ :modal-title =" $t('package.access.revoke.title')"
344+ id =" revoke-access-modal"
345+ class =" max-w-sm"
346+ >
347+ <div class =" space-y-4" >
348+ <!-- Warning message -->
349+ <div
350+ class =" p-3 text-sm text-yellow-400 bg-yellow-500/10 border border-yellow-500/20 rounded-md"
351+ >
352+ <p class =" font-medium mb-1" >{{ $t('package.access.revoke.warning') }}</p >
353+ <p class =" text-xs text-yellow-400/80" >
354+ {{
355+ $t('package.access.revoke.impact', {
356+ team: revokeTarget?.displayName,
357+ package: packageName,
358+ })
359+ }}
360+ </p >
361+ </div >
362+
363+ <!-- Team being revoked -->
364+ <div class =" flex items-center gap-2 p-3 bg-bg-subtle border border-border rounded-md" >
365+ <span class =" i-lucide:users w-4 h-4 text-fg-subtle shrink-0" aria-hidden =" true" />
366+ <span class =" font-mono text-sm text-fg" >{{ revokeTarget?.name }}</span >
367+ </div >
368+
369+ <!-- Error message -->
370+ <div
371+ v-if =" revokeError"
372+ class =" p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md"
373+ role =" alert"
374+ >
375+ {{ revokeError }}
376+ </div >
377+
378+ <!-- Actions -->
379+ <div class =" flex gap-3" >
380+ <button
381+ type =" button"
382+ class =" flex-1 px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-accent/70"
383+ :disabled =" isRevoking"
384+ @click =" closeRevokeDialog"
385+ >
386+ {{ $t('common.close') }}
387+ </button >
388+ <button
389+ type =" button"
390+ class =" flex-1 px-4 py-2 font-mono text-sm text-white bg-red-600 rounded-md transition-colors duration-200 hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-accent/70"
391+ :disabled =" isRevoking"
392+ @click =" handleRevokeAccess"
393+ >
394+ <span v-if =" isRevoking" class =" flex items-center justify-center gap-2" >
395+ <span class =" i-svg-spinners:ring-resize w-4 h-4 animate-spin" aria-hidden =" true" />
396+ {{ $t('package.access.revoke.revoking') }}
397+ </span >
398+ <span v-else >{{ $t('package.access.revoke.confirm') }}</span >
399+ </button >
400+ </div >
401+ </div >
402+ </Modal >
403+ </ClientOnly >
306404</template >
0 commit comments