@cat-factory/app 0.129.0 → 0.130.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/board/AgentFailureCard.vue +3 -1
- package/app/components/board/AgentStopButton.vue +3 -0
- package/app/components/board/BoardCanvas.vue +16 -3
- package/app/components/board/RecurringPipelineModal.vue +7 -1
- package/app/components/board/nodes/BlockNode.vue +47 -41
- package/app/components/brainstorm/BrainstormWindow.vue +25 -5
- package/app/components/clarity/ClarityReviewWindow.vue +17 -4
- package/app/components/docs/DocInterviewWindow.vue +5 -1
- package/app/components/followUp/FollowUpWindow.vue +15 -1
- package/app/components/forkDecision/ForkDecisionWindow.vue +7 -2
- package/app/components/gates/GateResultView.vue +7 -1
- package/app/components/humanTest/HumanTestWindow.vue +11 -5
- package/app/components/layout/BoardSwitcher.vue +21 -12
- package/app/components/layout/BoardToolbar.vue +8 -4
- package/app/components/layout/CommandBar.vue +137 -123
- package/app/components/layout/NotificationsInbox.vue +5 -1
- package/app/components/layout/SideBar.vue +157 -115
- package/app/components/layout/SpendWarningBanner.vue +3 -0
- package/app/components/panels/InspectorPanel.vue +23 -10
- package/app/components/panels/inspector/TaskExecution.vue +13 -4
- package/app/components/prReview/PrReviewWindow.vue +7 -3
- package/app/components/requirements/RequirementsReviewWindow.vue +33 -5
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +7 -3
- package/app/composables/useBlockDeletion.ts +7 -0
- package/app/composables/useBlockDrag.ts +5 -0
- package/app/composables/useFrameResize.ts +4 -0
- package/app/composables/useWorkspaceAccess.spec.ts +102 -0
- package/app/composables/useWorkspaceAccess.ts +84 -0
- package/app/stores/workspace.spec.ts +19 -0
- package/app/stores/workspace.ts +27 -7
- package/app/types/domain.ts +6 -0
- package/i18n/locales/de.json +5 -0
- package/i18n/locales/en.json +8 -0
- package/i18n/locales/es.json +5 -0
- package/i18n/locales/fr.json +5 -0
- package/i18n/locales/he.json +5 -0
- package/i18n/locales/it.json +5 -0
- package/i18n/locales/ja.json +5 -0
- package/i18n/locales/pl.json +5 -0
- package/i18n/locales/tr.json +5 -0
- package/i18n/locales/uk.json +5 -0
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ const props = withDefaults(
|
|
|
15
15
|
|
|
16
16
|
const { t } = useI18n()
|
|
17
17
|
const agentRuns = useAgentRunsStore()
|
|
18
|
+
const access = useWorkspaceAccess()
|
|
18
19
|
const ui = useUiStore()
|
|
19
20
|
const auth = useAuthStore()
|
|
20
21
|
const board = useBoardStore()
|
|
@@ -171,7 +172,8 @@ async function retry() {
|
|
|
171
172
|
type="button"
|
|
172
173
|
class="nodrag flex items-center gap-1 rounded-md bg-rose-900/40 text-rose-200 hover:bg-rose-900/70 disabled:opacity-60"
|
|
173
174
|
:class="compact ? 'px-2 py-0.5 text-[10px]' : 'px-2 py-1 text-[11px]'"
|
|
174
|
-
:disabled="retrying"
|
|
175
|
+
:disabled="retrying || !access.canExecuteRuns.value"
|
|
176
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
175
177
|
data-testid="agent-failure-retry"
|
|
176
178
|
@click.stop="retry"
|
|
177
179
|
>
|
|
@@ -21,6 +21,7 @@ const props = withDefaults(
|
|
|
21
21
|
|
|
22
22
|
const { t } = useI18n()
|
|
23
23
|
const agentRuns = useAgentRunsStore()
|
|
24
|
+
const access = useWorkspaceAccess()
|
|
24
25
|
const toast = useToast()
|
|
25
26
|
const { confirm } = useConfirm()
|
|
26
27
|
const stopping = ref(false)
|
|
@@ -68,6 +69,8 @@ async function stop() {
|
|
|
68
69
|
:size="size"
|
|
69
70
|
icon="i-lucide-circle-stop"
|
|
70
71
|
:loading="stopping"
|
|
72
|
+
:disabled="!access.canExecuteRuns.value"
|
|
73
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
71
74
|
@click.stop="stop"
|
|
72
75
|
>
|
|
73
76
|
{{ displayLabel }}
|
|
@@ -20,6 +20,7 @@ const execution = useExecutionStore()
|
|
|
20
20
|
const ui = useUiStore()
|
|
21
21
|
const github = useGitHubStore()
|
|
22
22
|
const toast = useToast()
|
|
23
|
+
const access = useWorkspaceAccess()
|
|
23
24
|
const { t } = useI18n()
|
|
24
25
|
|
|
25
26
|
const { onNodeDragStop, onViewportChange, screenToFlowCoordinate } = useVueFlow(BOARD_FLOW_ID)
|
|
@@ -240,12 +241,22 @@ async function onDrop(event: DragEvent) {
|
|
|
240
241
|
{{ t('board.canvas.emptyBody') }}
|
|
241
242
|
</p>
|
|
242
243
|
</div>
|
|
243
|
-
<div
|
|
244
|
-
|
|
244
|
+
<div
|
|
245
|
+
v-if="
|
|
246
|
+
access.canManageIntegrations.value || (github.available && access.canWriteBoard.value)
|
|
247
|
+
"
|
|
248
|
+
class="pointer-events-auto flex flex-wrap items-center justify-center gap-2"
|
|
249
|
+
>
|
|
250
|
+
<UButton
|
|
251
|
+
v-if="access.canManageIntegrations.value"
|
|
252
|
+
color="primary"
|
|
253
|
+
icon="i-lucide-git-branch-plus"
|
|
254
|
+
@click="ui.openBootstrap()"
|
|
255
|
+
>
|
|
245
256
|
{{ t('nav.bootstrapRepo') }}
|
|
246
257
|
</UButton>
|
|
247
258
|
<UButton
|
|
248
|
-
v-if="github.available"
|
|
259
|
+
v-if="github.available && access.canWriteBoard.value"
|
|
249
260
|
color="primary"
|
|
250
261
|
variant="soft"
|
|
251
262
|
icon="i-lucide-folder-git-2"
|
|
@@ -254,6 +265,8 @@ async function onDrop(event: DragEvent) {
|
|
|
254
265
|
{{ t('nav.addFromRepo') }}
|
|
255
266
|
</UButton>
|
|
256
267
|
</div>
|
|
268
|
+
<!-- A read-only viewer sees the empty state but no create affordances. -->
|
|
269
|
+
<p v-else class="max-w-sm text-xs text-slate-600">{{ t('access.noBoardWrite') }}</p>
|
|
257
270
|
</div>
|
|
258
271
|
|
|
259
272
|
<!-- task dependency arrows, overlaid in screen space -->
|
|
@@ -16,6 +16,7 @@ const recurring = useRecurringPipelinesStore()
|
|
|
16
16
|
const tracker = useTrackerStore()
|
|
17
17
|
const tasks = useTasksStore()
|
|
18
18
|
const toast = useToast()
|
|
19
|
+
const access = useWorkspaceAccess()
|
|
19
20
|
const { t } = useI18n()
|
|
20
21
|
|
|
21
22
|
const open = computed({
|
|
@@ -200,7 +201,12 @@ function buildIssueIntake(): IssueIntakeConfig {
|
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
const canAdd = computed(
|
|
203
|
-
() =>
|
|
204
|
+
() =>
|
|
205
|
+
name.value.trim().length > 0 &&
|
|
206
|
+
pipelineId.value.length > 0 &&
|
|
207
|
+
intakeReady.value &&
|
|
208
|
+
// Scheduling a recurring pipeline is a `runs.execute` action.
|
|
209
|
+
access.canExecuteRuns.value,
|
|
204
210
|
)
|
|
205
211
|
|
|
206
212
|
async function add() {
|
|
@@ -23,6 +23,7 @@ const tasks = useTasksStore()
|
|
|
23
23
|
const agentRuns = useAgentRunsStore()
|
|
24
24
|
const services = useServicesStore()
|
|
25
25
|
const reviews = useReviewStage()
|
|
26
|
+
const access = useWorkspaceAccess()
|
|
26
27
|
const { t } = useI18n()
|
|
27
28
|
const { lod } = useSemanticZoom()
|
|
28
29
|
// Coarse-pointer (touch) bumps the frame-header actions from `xs` to `sm` so
|
|
@@ -466,45 +467,50 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
466
467
|
<UBadge :color="statusMeta.chip as any" variant="subtle" size="sm">{{
|
|
467
468
|
statusLabel
|
|
468
469
|
}}</UBadge>
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
470
|
+
<!-- Board-authoring buttons (create task / from issue / recurring / initiative)
|
|
471
|
+
are `board.write` — hidden for a read-only viewer, who keeps the badge +
|
|
472
|
+
collapse toggle (view-only affordances). -->
|
|
473
|
+
<template v-if="access.canWriteBoard.value">
|
|
474
|
+
<UButton
|
|
475
|
+
class="nodrag"
|
|
476
|
+
data-testid="frame-add-task"
|
|
477
|
+
:size="isTouch ? 'sm' : 'xs'"
|
|
478
|
+
variant="ghost"
|
|
479
|
+
color="neutral"
|
|
480
|
+
icon="i-lucide-plus"
|
|
481
|
+
:title="t('board.frame.addTaskTitle')"
|
|
482
|
+
@click.stop="addTask"
|
|
483
|
+
/>
|
|
484
|
+
<UButton
|
|
485
|
+
v-if="tasks.anyOffered"
|
|
486
|
+
class="nodrag"
|
|
487
|
+
:size="isTouch ? 'sm' : 'xs'"
|
|
488
|
+
variant="ghost"
|
|
489
|
+
color="neutral"
|
|
490
|
+
icon="i-lucide-ticket"
|
|
491
|
+
:title="t('board.frame.createTaskFromIssueTitle')"
|
|
492
|
+
@click.stop="createTaskFromIssue"
|
|
493
|
+
/>
|
|
494
|
+
<UButton
|
|
495
|
+
class="nodrag"
|
|
496
|
+
:size="isTouch ? 'sm' : 'xs'"
|
|
497
|
+
variant="ghost"
|
|
498
|
+
color="neutral"
|
|
499
|
+
icon="i-lucide-repeat"
|
|
500
|
+
:title="t('board.frame.addRecurringTitle')"
|
|
501
|
+
@click.stop="addRecurring"
|
|
502
|
+
/>
|
|
503
|
+
<UButton
|
|
504
|
+
class="nodrag"
|
|
505
|
+
data-testid="frame-add-initiative"
|
|
506
|
+
:size="isTouch ? 'sm' : 'xs'"
|
|
507
|
+
variant="ghost"
|
|
508
|
+
color="neutral"
|
|
509
|
+
icon="i-lucide-milestone"
|
|
510
|
+
:title="t('board.frame.createInitiativeTitle')"
|
|
511
|
+
@click.stop="createInitiative"
|
|
512
|
+
/>
|
|
513
|
+
</template>
|
|
508
514
|
<UButton
|
|
509
515
|
class="nodrag"
|
|
510
516
|
:size="isTouch ? 'sm' : 'xs'"
|
|
@@ -540,9 +546,9 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
540
546
|
<InitiativeCard v-for="i in initiativeBlocks" :key="i.id" :block-id="i.id" />
|
|
541
547
|
<DraggableTask v-for="t in directTasks" :key="t.id" :task-id="t.id" />
|
|
542
548
|
<button
|
|
543
|
-
v-if="!hasTasks"
|
|
549
|
+
v-if="!hasTasks && access.canWriteBoard.value"
|
|
544
550
|
type="button"
|
|
545
|
-
data-testid="frame-add-task"
|
|
551
|
+
data-testid="frame-add-task-empty"
|
|
546
552
|
class="absolute inset-4 flex items-center justify-center gap-1 rounded-lg border border-dashed border-slate-700 text-[11px] text-slate-500 hover:border-slate-500 hover:text-slate-300"
|
|
547
553
|
@click.stop="addTask"
|
|
548
554
|
>
|
|
@@ -25,6 +25,7 @@ const brainstorm = useBrainstormStore()
|
|
|
25
25
|
const ui = useUiStore()
|
|
26
26
|
const toast = useToast()
|
|
27
27
|
const { t } = useI18n()
|
|
28
|
+
const access = useWorkspaceAccess()
|
|
28
29
|
|
|
29
30
|
const drafts = ref<Record<string, string>>({})
|
|
30
31
|
const redoComment = ref('')
|
|
@@ -427,7 +428,14 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
427
428
|
variant="soft"
|
|
428
429
|
size="xs"
|
|
429
430
|
icon="i-lucide-corner-down-left"
|
|
430
|
-
:disabled="
|
|
431
|
+
:disabled="
|
|
432
|
+
!(drafts[item.id] ?? '').trim() ||
|
|
433
|
+
frozen ||
|
|
434
|
+
!access.canExecuteRuns.value
|
|
435
|
+
"
|
|
436
|
+
:title="
|
|
437
|
+
access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
|
|
438
|
+
"
|
|
431
439
|
@click="submitReply(item)"
|
|
432
440
|
>
|
|
433
441
|
{{ t('brainstorm.saveChoice') }}
|
|
@@ -437,7 +445,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
437
445
|
variant="ghost"
|
|
438
446
|
size="xs"
|
|
439
447
|
icon="i-lucide-x"
|
|
440
|
-
:disabled="frozen"
|
|
448
|
+
:disabled="frozen || !access.canExecuteRuns.value"
|
|
449
|
+
:title="
|
|
450
|
+
access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
|
|
451
|
+
"
|
|
441
452
|
@click="setStatus(item, 'dismissed')"
|
|
442
453
|
>
|
|
443
454
|
{{ t('brainstorm.dismiss') }}
|
|
@@ -452,7 +463,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
452
463
|
variant="ghost"
|
|
453
464
|
size="xs"
|
|
454
465
|
icon="i-lucide-rotate-ccw"
|
|
455
|
-
:disabled="frozen"
|
|
466
|
+
:disabled="frozen || !access.canExecuteRuns.value"
|
|
467
|
+
:title="
|
|
468
|
+
access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
|
|
469
|
+
"
|
|
456
470
|
@click="setStatus(item, 'open')"
|
|
457
471
|
>
|
|
458
472
|
{{ t('brainstorm.reopen') }}
|
|
@@ -533,6 +547,8 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
533
547
|
icon="i-lucide-arrow-right"
|
|
534
548
|
:ui="{ leadingIcon: 'rtl:-scale-x-100', trailingIcon: 'rtl:-scale-x-100' }"
|
|
535
549
|
:loading="acting"
|
|
550
|
+
:disabled="!access.canExecuteRuns.value"
|
|
551
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
536
552
|
@click="proceed"
|
|
537
553
|
>
|
|
538
554
|
{{ t('brainstorm.proceedNothing') }}
|
|
@@ -544,7 +560,8 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
544
560
|
block
|
|
545
561
|
icon="i-lucide-wand-sparkles"
|
|
546
562
|
:loading="reworking"
|
|
547
|
-
:disabled="!canIncorporate"
|
|
563
|
+
:disabled="!canIncorporate || !access.canExecuteRuns.value"
|
|
564
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
548
565
|
@click="incorporate()"
|
|
549
566
|
>
|
|
550
567
|
{{ t('brainstorm.incorporateChoices') }}
|
|
@@ -568,6 +585,8 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
568
585
|
block
|
|
569
586
|
icon="i-lucide-sparkles"
|
|
570
587
|
:loading="busy"
|
|
588
|
+
:disabled="!access.canExecuteRuns.value"
|
|
589
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
571
590
|
@click="reReview"
|
|
572
591
|
>
|
|
573
592
|
{{ busy ? t('brainstorm.reRunning') : t('brainstorm.reRun') }}
|
|
@@ -602,7 +621,8 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
602
621
|
block
|
|
603
622
|
icon="i-lucide-wand-sparkles"
|
|
604
623
|
:loading="reworking"
|
|
605
|
-
:disabled="!redoComment.trim()"
|
|
624
|
+
:disabled="!redoComment.trim() || !access.canExecuteRuns.value"
|
|
625
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
606
626
|
@click="incorporate(redoComment.trim())"
|
|
607
627
|
>
|
|
608
628
|
{{ t('brainstorm.redoWithDirection') }}
|
|
@@ -25,6 +25,7 @@ const clarity = useClarityStore()
|
|
|
25
25
|
const models = useModelsStore()
|
|
26
26
|
const toast = useToast()
|
|
27
27
|
const { t } = useI18n()
|
|
28
|
+
const access = useWorkspaceAccess()
|
|
28
29
|
|
|
29
30
|
// Draft replies, keyed by item id, so editing one item doesn't disturb others.
|
|
30
31
|
const drafts = ref<Record<string, string>>({})
|
|
@@ -474,7 +475,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
474
475
|
variant="ghost"
|
|
475
476
|
size="xs"
|
|
476
477
|
icon="i-lucide-x"
|
|
477
|
-
:disabled="frozen"
|
|
478
|
+
:disabled="frozen || !access.canExecuteRuns.value"
|
|
479
|
+
:title="
|
|
480
|
+
access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
|
|
481
|
+
"
|
|
478
482
|
@click="setStatus(item, 'dismissed')"
|
|
479
483
|
>
|
|
480
484
|
{{ t('clarity.dismissIrrelevant') }}
|
|
@@ -489,7 +493,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
489
493
|
variant="ghost"
|
|
490
494
|
size="xs"
|
|
491
495
|
icon="i-lucide-rotate-ccw"
|
|
492
|
-
:disabled="frozen"
|
|
496
|
+
:disabled="frozen || !access.canExecuteRuns.value"
|
|
497
|
+
:title="
|
|
498
|
+
access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
|
|
499
|
+
"
|
|
493
500
|
@click="setStatus(item, 'open')"
|
|
494
501
|
>
|
|
495
502
|
{{ t('clarity.reopen') }}
|
|
@@ -577,6 +584,8 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
577
584
|
icon="i-lucide-arrow-right"
|
|
578
585
|
:ui="{ leadingIcon: 'rtl:-scale-x-100', trailingIcon: 'rtl:-scale-x-100' }"
|
|
579
586
|
:loading="acting"
|
|
587
|
+
:disabled="!access.canExecuteRuns.value"
|
|
588
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
580
589
|
@click="proceed"
|
|
581
590
|
>
|
|
582
591
|
{{ t('clarity.proceedNothing') }}
|
|
@@ -588,7 +597,8 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
588
597
|
block
|
|
589
598
|
icon="i-lucide-wand-sparkles"
|
|
590
599
|
:loading="reworking"
|
|
591
|
-
:disabled="!canIncorporate"
|
|
600
|
+
:disabled="!canIncorporate || !access.canExecuteRuns.value"
|
|
601
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
592
602
|
@click="incorporate()"
|
|
593
603
|
>
|
|
594
604
|
{{ t('clarity.incorporateAnswers') }}
|
|
@@ -612,6 +622,8 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
612
622
|
block
|
|
613
623
|
icon="i-lucide-sparkles"
|
|
614
624
|
:loading="busy"
|
|
625
|
+
:disabled="!access.canExecuteRuns.value"
|
|
626
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
615
627
|
@click="reReview"
|
|
616
628
|
>
|
|
617
629
|
{{ busy ? t('clarity.reReviewing') : t('clarity.looksGoodReReview') }}
|
|
@@ -646,7 +658,8 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
646
658
|
block
|
|
647
659
|
icon="i-lucide-wand-sparkles"
|
|
648
660
|
:loading="reworking"
|
|
649
|
-
:disabled="!redoComment.trim()"
|
|
661
|
+
:disabled="!redoComment.trim() || !access.canExecuteRuns.value"
|
|
662
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
650
663
|
@click="incorporate(redoComment.trim())"
|
|
651
664
|
>
|
|
652
665
|
{{ t('clarity.redoWithDirection') }}
|
|
@@ -12,6 +12,7 @@ import { computed, reactive, watch } from 'vue'
|
|
|
12
12
|
const board = useBoardStore()
|
|
13
13
|
const docInterview = useDocInterviewStore()
|
|
14
14
|
const { t } = useI18n()
|
|
15
|
+
const access = useWorkspaceAccess()
|
|
15
16
|
|
|
16
17
|
const { open, blockId, close } = useResultView('doc-interview', {
|
|
17
18
|
onOpen: (id) => void docInterview.load(id),
|
|
@@ -186,6 +187,8 @@ const onProceed = () => flushThen((id) => docInterview.proceedInterview(id))
|
|
|
186
187
|
variant="ghost"
|
|
187
188
|
size="sm"
|
|
188
189
|
:loading="resuming"
|
|
190
|
+
:disabled="!access.canExecuteRuns.value"
|
|
191
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
189
192
|
data-testid="doc-interview-proceed"
|
|
190
193
|
@click="onProceed"
|
|
191
194
|
>
|
|
@@ -195,7 +198,8 @@ const onProceed = () => flushThen((id) => docInterview.proceedInterview(id))
|
|
|
195
198
|
color="primary"
|
|
196
199
|
size="sm"
|
|
197
200
|
:loading="resuming"
|
|
198
|
-
:disabled="!allAnswered"
|
|
201
|
+
:disabled="!allAnswered || !access.canExecuteRuns.value"
|
|
202
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
199
203
|
data-testid="doc-interview-continue"
|
|
200
204
|
@click="onContinue"
|
|
201
205
|
>
|
|
@@ -16,6 +16,7 @@ import { FOLLOW_UP_COMPANION_META } from '~/utils/catalog'
|
|
|
16
16
|
const execution = useExecutionStore()
|
|
17
17
|
const board = useBoardStore()
|
|
18
18
|
const followUps = useFollowUpsStore()
|
|
19
|
+
const access = useWorkspaceAccess()
|
|
19
20
|
|
|
20
21
|
const { t } = useI18n()
|
|
21
22
|
|
|
@@ -207,7 +208,12 @@ const STATUS_META: Record<
|
|
|
207
208
|
size="xs"
|
|
208
209
|
color="primary"
|
|
209
210
|
:loading="followUps.isActing(item.id)"
|
|
210
|
-
:disabled="
|
|
211
|
+
:disabled="
|
|
212
|
+
!(drafts[item.id] ?? '').trim() || !access.canExecuteRuns.value
|
|
213
|
+
"
|
|
214
|
+
:title="
|
|
215
|
+
access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
|
|
216
|
+
"
|
|
211
217
|
@click="onAnswer(item)"
|
|
212
218
|
>
|
|
213
219
|
{{ t('followUp.actions.answerAndSend') }}
|
|
@@ -217,6 +223,10 @@ const STATUS_META: Record<
|
|
|
217
223
|
color="neutral"
|
|
218
224
|
variant="ghost"
|
|
219
225
|
:loading="followUps.isActing(item.id)"
|
|
226
|
+
:disabled="!access.canExecuteRuns.value"
|
|
227
|
+
:title="
|
|
228
|
+
access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
|
|
229
|
+
"
|
|
220
230
|
@click="onDismiss(item)"
|
|
221
231
|
>
|
|
222
232
|
{{ t('followUp.actions.dismiss') }}
|
|
@@ -231,6 +241,8 @@ const STATUS_META: Record<
|
|
|
231
241
|
color="primary"
|
|
232
242
|
icon="i-lucide-ticket"
|
|
233
243
|
:loading="followUps.isActing(item.id)"
|
|
244
|
+
:disabled="!access.canExecuteRuns.value"
|
|
245
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
234
246
|
@click="onFile(item)"
|
|
235
247
|
>
|
|
236
248
|
{{ t('followUp.actions.fileAsIssue') }}
|
|
@@ -241,6 +253,8 @@ const STATUS_META: Record<
|
|
|
241
253
|
variant="soft"
|
|
242
254
|
icon="i-lucide-corner-up-left"
|
|
243
255
|
:loading="followUps.isActing(item.id)"
|
|
256
|
+
:disabled="!access.canExecuteRuns.value"
|
|
257
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
244
258
|
@click="onQueue(item)"
|
|
245
259
|
>
|
|
246
260
|
{{ t('followUp.actions.sendToCoder') }}
|
|
@@ -18,6 +18,7 @@ import { FORK_DECISION_META } from '~/utils/catalog'
|
|
|
18
18
|
const execution = useExecutionStore()
|
|
19
19
|
const board = useBoardStore()
|
|
20
20
|
const forkDecision = useForkDecisionStore()
|
|
21
|
+
const access = useWorkspaceAccess()
|
|
21
22
|
|
|
22
23
|
const { t } = useI18n()
|
|
23
24
|
|
|
@@ -348,7 +349,10 @@ async function onSend() {
|
|
|
348
349
|
size="sm"
|
|
349
350
|
icon="i-lucide-send"
|
|
350
351
|
:loading="forkDecision.chatting"
|
|
351
|
-
:disabled="
|
|
352
|
+
:disabled="
|
|
353
|
+
!canChat || chatInput.trim().length === 0 || !access.canExecuteRuns.value
|
|
354
|
+
"
|
|
355
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
352
356
|
@click="onSend"
|
|
353
357
|
>
|
|
354
358
|
{{ t('forkDecision.chat.send') }}
|
|
@@ -380,7 +384,8 @@ async function onSend() {
|
|
|
380
384
|
size="sm"
|
|
381
385
|
icon="i-lucide-check"
|
|
382
386
|
:loading="forkDecision.choosing"
|
|
383
|
-
:disabled="!canChoose"
|
|
387
|
+
:disabled="!canChoose || !access.canExecuteRuns.value"
|
|
388
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
384
389
|
@click="onChoose"
|
|
385
390
|
>
|
|
386
391
|
{{ t('forkDecision.choose') }}
|
|
@@ -17,6 +17,7 @@ import CopyButton from '~/components/common/CopyButton.vue'
|
|
|
17
17
|
const board = useBoardStore()
|
|
18
18
|
const execution = useExecutionStore()
|
|
19
19
|
const { t } = useI18n()
|
|
20
|
+
const access = useWorkspaceAccess()
|
|
20
21
|
|
|
21
22
|
// Synchronous window: it reads its state straight off the execution step, so there's
|
|
22
23
|
// nothing to fetch on open (no `onOpen` loader).
|
|
@@ -305,7 +306,12 @@ const conflictVerdict = computed(() => {
|
|
|
305
306
|
color="primary"
|
|
306
307
|
icon="i-lucide-wrench"
|
|
307
308
|
:loading="fixBusy"
|
|
308
|
-
:disabled="
|
|
309
|
+
:disabled="
|
|
310
|
+
fixBusy ||
|
|
311
|
+
fixInstructions.trim().length === 0 ||
|
|
312
|
+
!access.canExecuteRuns.value
|
|
313
|
+
"
|
|
314
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
309
315
|
@click="submitFix"
|
|
310
316
|
>
|
|
311
317
|
{{ t('gates.humanReview.requestFix') }}
|
|
@@ -20,6 +20,7 @@ const execution = useExecutionStore()
|
|
|
20
20
|
const humanTest = useHumanTestStore()
|
|
21
21
|
const { t, d } = useI18n()
|
|
22
22
|
const toast = useToast()
|
|
23
|
+
const access = useWorkspaceAccess()
|
|
23
24
|
const { confirmAction, toastDone } = useConfirmAction()
|
|
24
25
|
|
|
25
26
|
// Shared seam contract (open/blockId/close + Escape). No `onOpen` loader: the gate state
|
|
@@ -230,7 +231,8 @@ const canDestroy = computed(
|
|
|
230
231
|
color="neutral"
|
|
231
232
|
icon="i-lucide-refresh-cw"
|
|
232
233
|
:loading="busy"
|
|
233
|
-
:disabled="busy || !canManageEnv"
|
|
234
|
+
:disabled="busy || !canManageEnv || !access.canExecuteRuns.value"
|
|
235
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
234
236
|
@click="recreate"
|
|
235
237
|
>
|
|
236
238
|
{{ t('humanTest.actions.recreate') }}
|
|
@@ -240,7 +242,8 @@ const canDestroy = computed(
|
|
|
240
242
|
variant="soft"
|
|
241
243
|
color="neutral"
|
|
242
244
|
icon="i-lucide-trash-2"
|
|
243
|
-
:disabled="busy || !canDestroy"
|
|
245
|
+
:disabled="busy || !canDestroy || !access.canExecuteRuns.value"
|
|
246
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
244
247
|
@click="destroy"
|
|
245
248
|
>
|
|
246
249
|
{{ t('humanTest.actions.destroy') }}
|
|
@@ -251,7 +254,8 @@ const canDestroy = computed(
|
|
|
251
254
|
color="neutral"
|
|
252
255
|
icon="i-lucide-git-merge"
|
|
253
256
|
:loading="busy"
|
|
254
|
-
:disabled="busy || !canManageEnv"
|
|
257
|
+
:disabled="busy || !canManageEnv || !access.canExecuteRuns.value"
|
|
258
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
255
259
|
@click="pullMain"
|
|
256
260
|
>
|
|
257
261
|
{{ t('humanTest.actions.pullMain') }}
|
|
@@ -296,7 +300,8 @@ const canDestroy = computed(
|
|
|
296
300
|
color="warning"
|
|
297
301
|
icon="i-lucide-wrench"
|
|
298
302
|
:loading="busy"
|
|
299
|
-
:disabled="busy || !findings.trim()"
|
|
303
|
+
:disabled="busy || !findings.trim() || !access.canExecuteRuns.value"
|
|
304
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
300
305
|
@click="submitFix"
|
|
301
306
|
>
|
|
302
307
|
{{ t('humanTest.fix.send') }}
|
|
@@ -366,7 +371,8 @@ const canDestroy = computed(
|
|
|
366
371
|
color="primary"
|
|
367
372
|
icon="i-lucide-circle-check"
|
|
368
373
|
:loading="busy"
|
|
369
|
-
:disabled="busy || !awaitingHuman"
|
|
374
|
+
:disabled="busy || !awaitingHuman || !access.canExecuteRuns.value"
|
|
375
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
370
376
|
@click="confirm"
|
|
371
377
|
>
|
|
372
378
|
{{ t('humanTest.confirm') }}
|
|
@@ -12,6 +12,7 @@ const accounts = useAccountsStore()
|
|
|
12
12
|
const workspace = useWorkspaceStore()
|
|
13
13
|
const ui = useUiStore()
|
|
14
14
|
const toast = useToast()
|
|
15
|
+
const access = useWorkspaceAccess()
|
|
15
16
|
const { confirm } = useConfirm()
|
|
16
17
|
|
|
17
18
|
const busy = ref(false)
|
|
@@ -92,7 +93,9 @@ const accountItems = computed<DropdownMenuItem[][]>(() => [
|
|
|
92
93
|
|
|
93
94
|
const boardItems = computed<DropdownMenuItem[][]>(() => [
|
|
94
95
|
workspace.accountWorkspaces.map((w) => ({
|
|
95
|
-
|
|
96
|
+
// Badge a board the caller only reaches as a read-only viewer (a restricted board
|
|
97
|
+
// they're not a member/admin of), so the switcher shows why it's read-only.
|
|
98
|
+
label: w.viewerRole === 'viewer' ? `${w.name} · ${t('access.viewerBadge')}` : w.name,
|
|
96
99
|
icon: 'i-lucide-layout-dashboard',
|
|
97
100
|
trailingIcon: w.id === workspace.workspaceId ? 'i-lucide-check' : undefined,
|
|
98
101
|
onSelect: () => void switchBoard(w.id),
|
|
@@ -103,17 +106,23 @@ const boardItems = computed<DropdownMenuItem[][]>(() => [
|
|
|
103
106
|
icon: 'i-lucide-plus',
|
|
104
107
|
onSelect: () => openPrompt('board'),
|
|
105
108
|
},
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
// Rename/delete of the active board is `settings.manage` — omit for a member/viewer,
|
|
110
|
+
// who would only get a 403 (the backend gates WorkspaceController update/delete).
|
|
111
|
+
...(access.canManageSettings.value
|
|
112
|
+
? [
|
|
113
|
+
{
|
|
114
|
+
label: t('layout.boardSwitcher.board.rename'),
|
|
115
|
+
icon: 'i-lucide-pencil',
|
|
116
|
+
onSelect: () => openPrompt('rename'),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
label: t('layout.boardSwitcher.board.delete'),
|
|
120
|
+
icon: 'i-lucide-trash-2',
|
|
121
|
+
color: 'error' as const,
|
|
122
|
+
onSelect: () => void removeBoard(),
|
|
123
|
+
},
|
|
124
|
+
]
|
|
125
|
+
: []),
|
|
117
126
|
],
|
|
118
127
|
])
|
|
119
128
|
|
|
@@ -10,6 +10,7 @@ const workspace = useWorkspaceStore()
|
|
|
10
10
|
const workspaceSettings = useWorkspaceSettingsStore()
|
|
11
11
|
const services = useServicesStore()
|
|
12
12
|
const toast = useToast()
|
|
13
|
+
const access = useWorkspaceAccess()
|
|
13
14
|
const { t, n } = useI18n()
|
|
14
15
|
const { fitView, zoomIn, zoomOut, resetZoom } = useBoardFlow()
|
|
15
16
|
|
|
@@ -207,15 +208,18 @@ const decisionItems = computed(() =>
|
|
|
207
208
|
</UButton>
|
|
208
209
|
</UDropdownMenu>
|
|
209
210
|
|
|
210
|
-
<!-- in-org sharing: add an existing org service to this board -->
|
|
211
|
-
<UDropdownMenu
|
|
211
|
+
<!-- in-org sharing: add an existing org service to this board (mount = board.write) -->
|
|
212
|
+
<UDropdownMenu
|
|
213
|
+
v-if="mountableItems.length && access.canWriteBoard.value"
|
|
214
|
+
:items="mountableItems"
|
|
215
|
+
>
|
|
212
216
|
<UButton color="neutral" variant="ghost" size="sm" icon="i-lucide-plus-circle">
|
|
213
217
|
<span class="hidden sm:inline">{{ t('board.toolbar.addService') }}</span>
|
|
214
218
|
</UButton>
|
|
215
219
|
</UDropdownMenu>
|
|
216
220
|
|
|
217
|
-
<!-- archived services: hidden from the board, restorable with no expiry -->
|
|
218
|
-
<UDropdownMenu v-if="archivedItems.length" :items="archivedItems">
|
|
221
|
+
<!-- archived services: hidden from the board, restorable with no expiry (board.write) -->
|
|
222
|
+
<UDropdownMenu v-if="archivedItems.length && access.canWriteBoard.value" :items="archivedItems">
|
|
219
223
|
<UButton
|
|
220
224
|
color="neutral"
|
|
221
225
|
variant="ghost"
|