Skip to content

Commit e90a9a4

Browse files
neutrino2211claude
andcommitted
feat: add confirmation dialog for team access revoke
- Add confirmation modal before revoking team access - Show warning about irreversible action - Display team name and package being affected - Handle loading state and error feedback - Add i18n keys for revoke dialog - Add unit tests for revoke confirmation flow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5ef60f1 commit e90a9a4

2 files changed

Lines changed: 480 additions & 17 deletions

File tree

app/components/Package/AccessControls.vue

Lines changed: 115 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ const selectedTeam = shallowRef('')
3535
const permission = shallowRef<'read-only' | 'read-write'>('read-only')
3636
const 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
3945
const 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

Comments
 (0)