Skip to content

Commit 4407f67

Browse files
committed
feat: moved buttons, updated otp+web retry element
1 parent aa9374f commit 4407f67

File tree

2 files changed

+114
-120
lines changed

2 files changed

+114
-120
lines changed

app/components/Header/ConnectorModal.vue

Lines changed: 2 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,15 @@
11
<script setup lang="ts">
2-
const {
3-
isConnected,
4-
isConnecting,
5-
npmUser,
6-
error,
7-
hasOperations,
8-
operations,
9-
connect,
10-
disconnect,
11-
refreshState,
12-
} = useConnector()
2+
const { isConnected, isConnecting, npmUser, error, hasOperations, connect, disconnect } =
3+
useConnector()
134
145
const { settings } = useSettings()
156
16-
const authUrl = computed(() => {
17-
const op = operations.value.find(o => o.status === 'running' && o.authUrl)
18-
return op?.authUrl ?? null
19-
})
20-
21-
const AUTH_POLL_INTERVAL = 20_000
22-
const AUTH_POLL_COUNT = 3
23-
let authPollTimer: ReturnType<typeof setInterval> | null = null
24-
25-
function startAuthPolling() {
26-
stopAuthPolling()
27-
let remaining = AUTH_POLL_COUNT
28-
authPollTimer = setInterval(async () => {
29-
try {
30-
await refreshState()
31-
} catch {
32-
stopAuthPolling()
33-
return
34-
}
35-
remaining--
36-
if (remaining <= 0) {
37-
stopAuthPolling()
38-
}
39-
}, AUTH_POLL_INTERVAL)
40-
}
41-
42-
function stopAuthPolling() {
43-
if (authPollTimer) {
44-
clearInterval(authPollTimer)
45-
authPollTimer = null
46-
}
47-
}
48-
49-
onUnmounted(stopAuthPolling)
50-
51-
function handleOpenAuthUrl() {
52-
if (authUrl.value) {
53-
window.open(authUrl.value, '_blank', 'noopener,noreferrer')
54-
startAuthPolling()
55-
}
56-
}
57-
587
const tokenInput = shallowRef('')
598
const portInput = shallowRef('31415')
609
const { copied, copy } = useClipboard({ copiedDuring: 2000 })
6110
6211
const hasAttemptedConnect = shallowRef(false)
6312
64-
watch(
65-
() => settings.value.connector.webAuth,
66-
webAuth => {
67-
if (!webAuth) {
68-
settings.value.connector.autoOpenURL = false
69-
}
70-
},
71-
)
72-
7313
watch(isConnected, connected => {
7414
if (!connected) {
7515
tokenInput.value = ''
@@ -125,14 +65,9 @@ function handleDisconnect() {
12565

12666
<!-- Connector preferences -->
12767
<div class="flex flex-col gap-2">
128-
<SettingsToggle
129-
:label="$t('connector.modal.web_auth')"
130-
v-model="settings.connector.webAuth"
131-
/>
13268
<SettingsToggle
13369
:label="$t('connector.modal.auto_open_url')"
13470
v-model="settings.connector.autoOpenURL"
135-
:class="!settings.connector.webAuth ? 'opacity-50 pointer-events-none' : ''"
13671
/>
13772
</div>
13873

@@ -145,17 +80,6 @@ function handleDisconnect() {
14580
{{ $t('connector.modal.connected_hint') }}
14681
</div>
14782

148-
<!-- Web auth link -->
149-
<button
150-
v-if="authUrl"
151-
type="button"
152-
class="flex items-center justify-center gap-2 w-full px-4 py-2 font-mono text-sm text-accent bg-accent/10 border border-accent/30 rounded-md transition-colors duration-200 hover:bg-accent/20"
153-
@click="handleOpenAuthUrl"
154-
>
155-
<span class="i-carbon:launch w-4 h-4" aria-hidden="true" />
156-
{{ $t('operations.queue.open_web_auth') }}
157-
</button>
158-
15983
<button
16084
type="button"
16185
class="w-full 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"
@@ -285,14 +209,9 @@ function handleDisconnect() {
285209

286210
<div class="border-t border-border my-3" />
287211
<div class="flex flex-col gap-2">
288-
<SettingsToggle
289-
:label="$t('connector.modal.web_auth')"
290-
v-model="settings.connector.webAuth"
291-
/>
292212
<SettingsToggle
293213
:label="$t('connector.modal.auto_open_url')"
294214
v-model="settings.connector.autoOpenURL"
295-
:class="!settings.connector.webAuth ? 'opacity-50 pointer-events-none' : ''"
296215
/>
297216
</div>
298217
</div>

app/components/Org/OperationsQueue.vue

Lines changed: 112 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const {
77
approvedOperations,
88
completedOperations,
99
activeOperations,
10+
operations,
1011
hasOperations,
1112
hasPendingOperations,
1213
hasApprovedOperations,
@@ -21,15 +22,55 @@ const {
2122
refreshState,
2223
} = useConnector()
2324
24-
const { settings } = useSettings()
25-
2625
const isExecuting = shallowRef(false)
2726
const otpInput = shallowRef('')
27+
const otpError = shallowRef('')
28+
29+
const authUrl = computed(() => {
30+
const op = operations.value.find(o => o.status === 'running' && o.authUrl)
31+
return op?.authUrl ?? null
32+
})
33+
34+
const authPollTimer = shallowRef<ReturnType<typeof setInterval> | null>(null)
35+
36+
function startAuthPolling() {
37+
stopAuthPolling()
38+
let remaining = 3
39+
authPollTimer.value = setInterval(async () => {
40+
try {
41+
await refreshState()
42+
} catch {
43+
stopAuthPolling()
44+
return
45+
}
46+
remaining--
47+
if (remaining <= 0) {
48+
stopAuthPolling()
49+
}
50+
}, 20000)
51+
}
52+
53+
function stopAuthPolling() {
54+
if (authPollTimer.value) {
55+
clearInterval(authPollTimer.value)
56+
authPollTimer.value = null
57+
}
58+
}
59+
60+
onUnmounted(stopAuthPolling)
2861
29-
/** Check if any active operation needs OTP */
62+
function handleOpenAuthUrl() {
63+
if (authUrl.value) {
64+
window.open(authUrl.value, '_blank', 'noopener,noreferrer')
65+
startAuthPolling()
66+
}
67+
}
68+
69+
/** Check if any active operation needs OTP (fallback for web auth failures) */
3070
const hasOtpFailures = computed(() =>
3171
activeOperations.value.some(
32-
(op: PendingOperation) => op.status === 'failed' && op.result?.requiresOtp,
72+
(op: PendingOperation) =>
73+
op.status === 'failed' && (op.result?.requiresOtp || op.result?.authFailure),
3374
),
3475
)
3576
@@ -48,9 +89,19 @@ async function handleExecute(otp?: string) {
4889
4990
/** Retry all OTP-failed operations with the provided OTP */
5091
async function handleRetryWithOtp() {
51-
if (!otpInput.value.trim()) return
52-
5392
const otp = otpInput.value.trim()
93+
94+
if (!otp) {
95+
otpError.value = 'OTP required'
96+
return
97+
}
98+
99+
if (!/^\d{6}$/.test(otp)) {
100+
otpError.value = 'OTP must be a 6-digit code'
101+
return
102+
}
103+
104+
otpError.value = ''
54105
otpInput.value = ''
55106
56107
// First, re-approve all OTP-failed operations
@@ -65,12 +116,15 @@ async function handleRetryWithOtp() {
65116
await handleExecute(otp)
66117
}
67118
68-
/** Retry all OTP-failed operations using web auth (no OTP needed) */
69-
async function handleRetryWithWebAuth() {
70-
const otpFailedOps = activeOperations.value.filter(
71-
(op: PendingOperation) => op.status === 'failed' && op.result?.requiresOtp,
119+
/** Retry failed operations with web auth (no OTP) */
120+
async function handleRetryWebAuth() {
121+
// Find all failed operations that need auth retry
122+
const failedOps = activeOperations.value.filter(
123+
(op: PendingOperation) =>
124+
op.status === 'failed' && (op.result?.requiresOtp || op.result?.authFailure),
72125
)
73-
for (const op of otpFailedOps) {
126+
127+
for (const op of failedOps) {
74128
await retryOperation(op.id)
75129
}
76130
@@ -80,6 +134,7 @@ async function handleRetryWithWebAuth() {
80134
async function handleClearAll() {
81135
await clearOperations()
82136
otpInput.value = ''
137+
otpError.value = ''
83138
}
84139
85140
function getStatusColor(status: string): string {
@@ -242,7 +297,7 @@ watch(isExecuting, executing => {
242297
</li>
243298
</ul>
244299

245-
<!-- Inline OTP prompt (appears when operations need OTP) -->
300+
<!-- Inline OTP prompt (appears when web auth fails and OTP is needed as fallback) -->
246301
<div
247302
v-if="hasOtpFailures"
248303
class="p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg"
@@ -254,35 +309,46 @@ watch(isExecuting, executing => {
254309
{{ $t('operations.queue.otp_prompt') }}
255310
</span>
256311
</div>
257-
<form class="flex items-center gap-2" @submit.prevent="handleRetryWithOtp">
258-
<label for="otp-input" class="sr-only">{{ $t('operations.queue.otp_label') }}</label>
259-
<InputBase
260-
id="otp-input"
261-
v-model="otpInput"
262-
type="text"
263-
name="otp-code"
264-
inputmode="numeric"
265-
pattern="[0-9]*"
266-
:placeholder="$t('operations.queue.otp_placeholder')"
267-
autocomplete="one-time-code"
268-
spellcheck="false"
269-
class="flex-1 min-w-25"
270-
size="small"
271-
/>
272-
<button
273-
type="submit"
274-
:disabled="!otpInput.trim() || isExecuting"
275-
class="px-3 py-2 font-mono text-xs text-bg bg-amber-500 rounded transition-all duration-200 hover:bg-amber-400 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/50"
276-
>
277-
{{ isExecuting ? $t('operations.queue.retrying') : $t('operations.queue.retry_otp') }}
278-
</button>
312+
<form class="flex flex-col gap-1" @submit.prevent="handleRetryWithOtp">
313+
<div class="flex items-center gap-2">
314+
<label for="otp-input" class="sr-only">{{ $t('operations.queue.otp_label') }}</label>
315+
<InputBase
316+
id="otp-input"
317+
v-model="otpInput"
318+
type="text"
319+
name="otp-code"
320+
inputmode="numeric"
321+
pattern="[0-9]*"
322+
maxlength="6"
323+
:placeholder="$t('operations.queue.otp_placeholder')"
324+
autocomplete="one-time-code"
325+
spellcheck="false"
326+
:class="['flex-1 min-w-25', otpError ? 'border-red-500 focus:outline-red-500' : '']"
327+
size="small"
328+
@input="otpError = ''"
329+
/>
330+
<button
331+
type="submit"
332+
:disabled="isExecuting"
333+
class="px-3 py-2 font-mono text-xs text-bg bg-amber-500 rounded transition-all duration-200 hover:bg-amber-400 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/50"
334+
>
335+
{{ isExecuting ? $t('operations.queue.retrying') : $t('operations.queue.retry_otp') }}
336+
</button>
337+
</div>
338+
<p v-if="otpError" class="text-xs text-red-400 font-mono">
339+
{{ otpError }}
340+
</p>
279341
</form>
342+
<div class="flex items-center gap-2 my-3">
343+
<div class="flex-1 h-px bg-amber-500/30" />
344+
<span class="text-xs text-amber-400 font-mono uppercase">{{ $t('common.or') }}</span>
345+
<div class="flex-1 h-px bg-amber-500/30" />
346+
</div>
280347
<button
281-
v-if="settings.connector.webAuth"
282348
type="button"
283349
:disabled="isExecuting"
284-
class="w-full mt-2 px-3 py-2 font-mono text-xs text-fg bg-bg-subtle border border-border rounded transition-all duration-200 hover:text-fg hover:border-border-hover disabled:opacity-50 disabled:cursor-not-allowed"
285-
@click="handleRetryWithWebAuth"
350+
class="w-full px-3 py-2 font-mono text-xs text-fg bg-bg-subtle border border-border rounded transition-all duration-200 hover:text-fg hover:border-border-hover disabled:opacity-50 disabled:cursor-not-allowed"
351+
@click="handleRetryWebAuth"
286352
>
287353
{{ isExecuting ? $t('operations.queue.retrying') : $t('operations.queue.retry_web_auth') }}
288354
</button>
@@ -311,6 +377,15 @@ watch(isExecuting, executing => {
311377
: `${$t('operations.queue.execute')} (${approvedOperations.length})`
312378
}}
313379
</button>
380+
<button
381+
v-if="authUrl"
382+
type="button"
383+
class="flex-1 px-4 py-2 font-mono text-sm text-accent bg-accent/10 border border-accent/30 rounded-md transition-colors duration-200 hover:bg-accent/20"
384+
@click="handleOpenAuthUrl"
385+
>
386+
<span class="i-carbon:launch w-4 h-4 inline-block me-1" aria-hidden="true" />
387+
{{ $t('operations.queue.open_web_auth') }}
388+
</button>
314389
</div>
315390

316391
<!-- Completed operations log (collapsed by default) -->

0 commit comments

Comments
 (0)