@cat-factory/app 0.310.2 → 0.311.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/README.md +24 -10
- package/app/components/documents/DocumentSourceConnectModal.vue +1 -1
- package/app/components/foundational/FoundationalServiceCatalogList.vue +2 -0
- package/app/components/foundational/FoundationalServiceRegistry.vue +9 -2
- package/app/components/foundational/FoundationalServiceSources.vue +2 -0
- package/app/components/foundational/FoundationalSuppressions.vue +1 -0
- package/app/components/fragments/FragmentLibraryManager.vue +4 -0
- package/app/components/github/GitHubOnboarding.vue +9 -1
- package/app/components/github/RepoTreeBrowser.vue +3 -0
- package/app/components/guidedReview/GuidedReviewDraftCard.vue +16 -4
- package/app/components/guidedReview/GuidedReviewModal.vue +1 -0
- package/app/components/guidedReview/GuidedReviewOverview.vue +1 -0
- package/app/components/humanTest/HumanTestWindow.vue +3 -2
- package/app/components/initiative/InitiativePlanningWindow.vue +1 -1
- package/app/components/judge/JudgeResultView.vue +1 -0
- package/app/components/layout/AccountDeploymentSettings.vue +8 -8
- package/app/components/layout/AccountTeamSettings.vue +2 -2
- package/app/components/layout/BoardSwitcher.vue +33 -14
- package/app/components/layout/DefaultTestEnvBanner.vue +4 -1
- package/app/components/layout/InfraSetupBanner.vue +26 -2
- package/app/components/layout/InfraSetupNotice.vue +69 -0
- package/app/components/layout/ProviderConfigBanner.vue +8 -1
- package/app/components/panels/AgentStepDetail.vue +2 -0
- package/app/components/panels/InspectorPanel.vue +1 -1
- package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +1 -1
- package/app/components/panels/inspector/ServiceTestSecrets.vue +1 -1
- package/app/components/panels/inspector/ServiceValidationConfig.vue +1 -1
- package/app/components/panels/inspector/TaskExecution.vue +1 -1
- package/app/components/pipeline/PipelineBuilder.vue +1 -1
- package/app/components/pipeline/PipelineHealthModal.vue +2 -2
- package/app/components/providers/AiProviderOnboardingModal.vue +7 -1
- package/app/components/providers/ApiKeysSection.vue +21 -2
- package/app/components/provisioning/ProvisioningLogsDrawer.vue +1 -0
- package/app/components/sandbox/SandboxPanel.vue +1 -0
- package/app/components/settings/InfraHandlersConfigurator.vue +1 -1
- package/app/components/settings/ProviderConnectionTab.vue +1 -0
- package/app/components/settings/ToolServerChecklist.vue +1 -1
- package/app/components/skills/SkillLibraryManager.vue +2 -0
- package/app/components/slack/SlackPanel.vue +1 -1
- package/app/components/tasks/TaskSourceConnectModal.vue +1 -1
- package/app/components/testing/TestReportWindow.vue +2 -0
- package/app/components/vcs/VcsConnectSurfaces.vue +30 -14
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +1 -0
- package/app/composables/useSetupActor.ts +25 -0
- package/app/composables/useWorkspaceAccess.ts +1 -1
- package/app/modular/nav-gates.ts +1 -2
- package/app/stores/accounts.ts +8 -0
- package/app/stores/workspace/infraSetup.ts +11 -3
- package/app/stores/workspace.ts +3 -1
- package/app/types/domain.ts +2 -0
- package/app/utils/setupAudience.spec.ts +115 -0
- package/app/utils/setupAudience.ts +102 -0
- package/i18n/locales/de.json +15 -1
- package/i18n/locales/en.json +15 -1
- package/i18n/locales/es.json +15 -1
- package/i18n/locales/fr.json +15 -1
- package/i18n/locales/he.json +15 -1
- package/i18n/locales/it.json +15 -1
- package/i18n/locales/ja.json +15 -1
- package/i18n/locales/pl.json +15 -1
- package/i18n/locales/tr.json +15 -1
- package/i18n/locales/uk.json +15 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -334,16 +334,22 @@ Every CONTROL in the SPA is the Nuxt UI component for it, never the raw HTML ele
|
|
|
334
334
|
|
|
335
335
|
What a primary, a secondary or a destructive action looks like is decided once, here, and not per call site. The weights are Nuxt UI's own [variant table](https://github.com/nuxt/ui/blob/v4/skills/nuxt-ui/references/design-system.md) (`solid` highest, then `outline`, `soft`, `subtle`, `ghost`, and `link` for a text link inside content), mapped onto the app's kinds of action:
|
|
336
336
|
|
|
337
|
-
| Kind
|
|
338
|
-
|
|
|
339
|
-
| Primary action, ONE per view
|
|
340
|
-
|
|
|
341
|
-
|
|
|
342
|
-
|
|
|
343
|
-
|
|
|
344
|
-
|
|
|
345
|
-
|
|
346
|
-
|
|
337
|
+
| Kind | Props |
|
|
338
|
+
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
339
|
+
| Primary action, ONE per view | none (the default, `primary solid`) |
|
|
340
|
+
| Dismissal in a dialog or form (Cancel, Close) | `color="neutral" variant="ghost"` |
|
|
341
|
+
| Real secondary action (Save draft, Back) | `color="neutral" variant="outline"` |
|
|
342
|
+
| Bulk action in a dialog footer, beside Close (Reseed all, Update all) | `variant="ghost"` (primary colour): the dialog's main action, kept light because each row above it has its own button |
|
|
343
|
+
| Send-back on a gate (bounce, request a fix) | `color="warning" variant="soft"`, so the approve beside it stays the one primary |
|
|
344
|
+
| Inline, navigation and chrome actions | `color="neutral" variant="ghost"`, usually with `size="xs"` |
|
|
345
|
+
| Icon-only action | `IconButton` with `color="neutral" variant="ghost"` |
|
|
346
|
+
| Destructive action, labelled | `color="error" variant="soft"`; `color="error"` alone (solid) only as the confirm step of a destructive action (a dialog's confirm, or an armed inline confirm such as a step's reject) |
|
|
347
|
+
| Destructive action, icon-only (a row's trash icon) | `IconButton` with `color="error" variant="ghost"` |
|
|
348
|
+
| A banner's call to action | solid, in the banner's colour (`color="warning"`, `color="error"`) |
|
|
349
|
+
| Status chip | `UBadge` with `variant="subtle" size="sm"`, and a `BadgeColor` map for the colour |
|
|
350
|
+
|
|
351
|
+
- **The defaults are Nuxt UI's own**: `primary solid md` for both `UButton` and `UBadge`, stated in `app.config.ts` (`ui.button.defaultVariants`, `ui.badge.defaultVariants`) so the value has one visible home and the guard can read it. A call site states only what DIFFERS.
|
|
352
|
+
- **One solid button per view.** Two solid buttons side by side compete for the same attention, so the second action takes a lighter variant from the table. The guard refuses two solid buttons under one parent element; it counts a `v-if` chain as one button and a `v-for` list as one, looks through a plain `<template>` wrapper, and treats a slot `<template #name>` as its own region. A file holding several forms that each end in a Save is several views and passes. A site that genuinely needs two says why with a `solid-ok:` comment on its own line, directly above the second button.
|
|
347
353
|
- **The unstyled group is the one a theme restyles, so it is the primary tier.** A theme document's `style.defaults` (a size, a button variant, a button colour) REPLACES the default for every control that does not state that prop; it does not scale or shift the others. So whichever group is unstyled is the group a theme moves. With the primary actions unstyled, a theme with `size: 'lg'` or `variants: { buttons: 'outline' }` restyles the actions and leaves the chrome alone. With the quiet chrome unstyled, the same theme would grow or fill every toolbar and icon button past the actions and invert the hierarchy. This is why the dense, quiet chrome carries its props and the primary action carries none, and why Nuxt UI's own internal buttons (a modal's close button, a toast's actions) agree with the app's.
|
|
348
354
|
- **A restated default is a bug, not noise.** It pins the value against a theme that changes the default, and it hides which props are the reason a control looks the way it does. `scripts/check-frontend-variants.mjs` (CI) refuses a value equal to the default on `UButton` and `UBadge` (either casing, either quote style, or a key of a `v-bind="{ … }"` object) and on every component that spreads its attributes into one (`IconButton`; the list is derived, so a new wrapper is covered), reading the defaults from `app.config.ts` itself. Inside a `UFieldGroup` or `UButtonGroup` it does not check `size`: the group sets the size there, so an explicit `size` overrides the group rather than restating the default. It reads template tags only, so a `ButtonProps` object (a toast's `actions`, a modal's `:close`) is not checked. A site that must keep a literal says why with a `variant-default-ok:` comment on its own line, directly above the tag; it waives that tag only.
|
|
349
355
|
|
|
@@ -605,6 +611,14 @@ The seams, and what a new feature should use rather than reading the store ad ho
|
|
|
605
611
|
- **A surface that narrows inline** reads `useUiRoleStore().fullSurface` (the frame header's bug-hunt
|
|
606
612
|
button, the palette's per-connection integration commands). Same rule as the tier: what remains
|
|
607
613
|
must be exactly what the full surface would have shown, only less of it.
|
|
614
|
+
- **A setup prompt** (a "configure this" card over the board) goes through `canActOnSetup`
|
|
615
|
+
(`app/utils/setupAudience.ts`) with the caller from `useSetupActor()`: it combines the role's
|
|
616
|
+
surface with the board and account grants and the owner the server reports for the gap
|
|
617
|
+
(`infraSetupOwners`). A caller who cannot act gets no card. If the gap stops runs they could start,
|
|
618
|
+
it reaches them as one line saying how it gets fixed (`blockingSetupNotice`): the owner to ask, or
|
|
619
|
+
the role switch when they hold the grant and only the designer surface hides the card. A viewer
|
|
620
|
+
gets nothing, and the line honours the card's dismissals. Never a button that opens a screen the
|
|
621
|
+
caller's save is refused on.
|
|
608
622
|
- **A tutorial tour whose step clicks a non-`intake` nav entry declares
|
|
609
623
|
`TUTORIAL_REQUIREMENTS.fullSurface`.** Which tours those are is not a judgement call:
|
|
610
624
|
`tutorial-tours.spec.ts` derives the pairing from `navItemVisible`, so a tour that gains such a
|
|
@@ -105,6 +105,7 @@ async function suppress(serviceId: string) {
|
|
|
105
105
|
|
|
106
106
|
<div class="mt-2 flex items-center gap-2">
|
|
107
107
|
<UButton
|
|
108
|
+
color="neutral"
|
|
108
109
|
v-if="s.contracts.length"
|
|
109
110
|
size="xs"
|
|
110
111
|
variant="ghost"
|
|
@@ -141,6 +142,7 @@ async function suppress(serviceId: string) {
|
|
|
141
142
|
deployment registered in code; the board's own row is managed in the registry tab,
|
|
142
143
|
where deleting it is the honest action. -->
|
|
143
144
|
<IconButton
|
|
145
|
+
color="neutral"
|
|
144
146
|
v-if="s.tier !== 'workspace'"
|
|
145
147
|
icon="i-lucide-eye-off"
|
|
146
148
|
size="xs"
|
|
@@ -205,6 +205,7 @@ async function remove(service: FoundationalService) {
|
|
|
205
205
|
</div>
|
|
206
206
|
<div class="flex shrink-0 gap-1">
|
|
207
207
|
<IconButton
|
|
208
|
+
color="neutral"
|
|
208
209
|
v-if="!s.sourceId"
|
|
209
210
|
icon="i-lucide-pencil"
|
|
210
211
|
size="xs"
|
|
@@ -310,7 +311,13 @@ async function remove(service: FoundationalService) {
|
|
|
310
311
|
:placeholder="t('foundational.registry.contractBodyPlaceholder')"
|
|
311
312
|
/>
|
|
312
313
|
</div>
|
|
313
|
-
<UButton
|
|
314
|
+
<UButton
|
|
315
|
+
color="neutral"
|
|
316
|
+
icon="i-lucide-plus"
|
|
317
|
+
size="xs"
|
|
318
|
+
variant="ghost"
|
|
319
|
+
@click="addContract"
|
|
320
|
+
>
|
|
314
321
|
{{ t('foundational.registry.addContract') }}
|
|
315
322
|
</UButton>
|
|
316
323
|
</div>
|
|
@@ -325,7 +332,7 @@ async function remove(service: FoundationalService) {
|
|
|
325
332
|
>
|
|
326
333
|
{{ t('foundational.registry.save') }}
|
|
327
334
|
</UButton>
|
|
328
|
-
<UButton size="sm" variant="ghost" @click="editing = null">
|
|
335
|
+
<UButton color="neutral" size="sm" variant="ghost" @click="editing = null">
|
|
329
336
|
{{ t('foundational.registry.cancel') }}
|
|
330
337
|
</UButton>
|
|
331
338
|
</div>
|
|
@@ -297,6 +297,7 @@ async function unlink(id: string) {
|
|
|
297
297
|
</UBadge>
|
|
298
298
|
<div class="ms-auto flex gap-1">
|
|
299
299
|
<IconButton
|
|
300
|
+
color="neutral"
|
|
300
301
|
icon="i-lucide-search-check"
|
|
301
302
|
size="xs"
|
|
302
303
|
variant="ghost"
|
|
@@ -305,6 +306,7 @@ async function unlink(id: string) {
|
|
|
305
306
|
@click="check(s.id)"
|
|
306
307
|
/>
|
|
307
308
|
<IconButton
|
|
309
|
+
color="neutral"
|
|
308
310
|
icon="i-lucide-refresh-cw"
|
|
309
311
|
size="xs"
|
|
310
312
|
variant="ghost"
|
|
@@ -752,6 +752,7 @@ async function unlinkSource(id: string) {
|
|
|
752
752
|
<!-- Editing a repo-SOURCED fragment locally would be overwritten on the next sync,
|
|
753
753
|
so only hand-authored fragments are editable here. -->
|
|
754
754
|
<IconButton
|
|
755
|
+
color="neutral"
|
|
755
756
|
v-if="!f.source"
|
|
756
757
|
icon="i-lucide-pencil"
|
|
757
758
|
size="xs"
|
|
@@ -868,6 +869,7 @@ async function unlinkSource(id: string) {
|
|
|
868
869
|
</div>
|
|
869
870
|
<div class="ms-auto flex gap-1">
|
|
870
871
|
<IconButton
|
|
872
|
+
color="neutral"
|
|
871
873
|
icon="i-lucide-refresh-cw"
|
|
872
874
|
size="xs"
|
|
873
875
|
variant="ghost"
|
|
@@ -1026,6 +1028,7 @@ async function unlinkSource(id: string) {
|
|
|
1026
1028
|
</UBadge>
|
|
1027
1029
|
<div class="ms-auto flex gap-1">
|
|
1028
1030
|
<UButton
|
|
1031
|
+
color="neutral"
|
|
1029
1032
|
icon="i-lucide-search-check"
|
|
1030
1033
|
size="xs"
|
|
1031
1034
|
variant="ghost"
|
|
@@ -1033,6 +1036,7 @@ async function unlinkSource(id: string) {
|
|
|
1033
1036
|
@click="checkSource(s.id)"
|
|
1034
1037
|
/>
|
|
1035
1038
|
<UButton
|
|
1039
|
+
color="neutral"
|
|
1036
1040
|
icon="i-lucide-refresh-cw"
|
|
1037
1041
|
size="xs"
|
|
1038
1042
|
variant="ghost"
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
// has no connection yet. cat-factory's whole flow runs on a connected repository host (agents
|
|
4
4
|
// open pull/merge requests on the user's repos), so the board is withheld until the workspace
|
|
5
5
|
// connects one. <VcsConnectSurfaces> renders whichever connect methods the deployment serves; a
|
|
6
|
-
// "Sign out" escape hatch avoids trapping a user who needs to switch accounts.
|
|
6
|
+
// "Sign out" escape hatch avoids trapping a user who needs to switch accounts. The board switcher is
|
|
7
|
+
// the other escape: the gate replaces the whole board, sidebar included, so without it a member
|
|
8
|
+
// who landed on a board they cannot connect had no way to reach a board they can.
|
|
7
9
|
import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
|
|
10
|
+
import BoardSwitcher from '~/components/layout/BoardSwitcher.vue'
|
|
8
11
|
import { VCS_PROVIDER_ICONS, VCS_PROVIDER_LABELS } from '~/utils/vcs'
|
|
9
12
|
|
|
10
13
|
const { t } = useI18n()
|
|
@@ -39,6 +42,11 @@ const title = computed(() =>
|
|
|
39
42
|
|
|
40
43
|
<VcsConnectSurfaces :app-intro="t('github.onboarding.appIntro')" />
|
|
41
44
|
|
|
45
|
+
<div class="mt-6 border-t border-default pt-4" data-testid="vcs-onboarding-switch-board">
|
|
46
|
+
<p class="mb-2 text-xs text-muted">{{ t('vcs.onboarding.switchBoard') }}</p>
|
|
47
|
+
<BoardSwitcher standalone />
|
|
48
|
+
</div>
|
|
49
|
+
|
|
42
50
|
<p
|
|
43
51
|
v-if="auth.required && auth.user"
|
|
44
52
|
class="mt-6 border-t border-default pt-4 text-center text-xs text-dimmed"
|
|
@@ -207,6 +207,7 @@ watch(
|
|
|
207
207
|
class="flex items-center justify-between gap-2 px-3 py-1.5"
|
|
208
208
|
>
|
|
209
209
|
<UButton
|
|
210
|
+
color="neutral"
|
|
210
211
|
variant="ghost"
|
|
211
212
|
class="flex items-center gap-2 truncate p-0 text-sm text-default hover:bg-transparent hover:text-primary"
|
|
212
213
|
@click="browseTo(entry.path)"
|
|
@@ -257,6 +258,7 @@ watch(
|
|
|
257
258
|
@update:model-value="toggleAllFiles"
|
|
258
259
|
/>
|
|
259
260
|
<UButton
|
|
261
|
+
color="neutral"
|
|
260
262
|
variant="ghost"
|
|
261
263
|
class="p-0 text-xs text-muted hover:bg-transparent hover:text-primary"
|
|
262
264
|
@click="toggleAllFiles"
|
|
@@ -283,6 +285,7 @@ watch(
|
|
|
283
285
|
@update:model-value="pick(entry.path)"
|
|
284
286
|
/>
|
|
285
287
|
<UButton
|
|
288
|
+
color="neutral"
|
|
286
289
|
variant="ghost"
|
|
287
290
|
class="flex items-center gap-2 truncate p-0 text-sm hover:bg-transparent hover:text-primary"
|
|
288
291
|
:class="isPicked(entry.path) ? 'text-primary' : 'text-toned'"
|
|
@@ -114,7 +114,13 @@ async function discard(): Promise<void> {
|
|
|
114
114
|
<UButton size="sm" :loading="saving" @click="save">
|
|
115
115
|
{{ t('guidedReview.drafts.save') }}
|
|
116
116
|
</UButton>
|
|
117
|
-
<UButton
|
|
117
|
+
<UButton
|
|
118
|
+
color="neutral"
|
|
119
|
+
size="sm"
|
|
120
|
+
variant="ghost"
|
|
121
|
+
:disabled="saving"
|
|
122
|
+
@click="editing = false"
|
|
123
|
+
>
|
|
118
124
|
{{ t('guidedReview.drafts.cancel') }}
|
|
119
125
|
</UButton>
|
|
120
126
|
</div>
|
|
@@ -126,13 +132,19 @@ async function discard(): Promise<void> {
|
|
|
126
132
|
{{ t('guidedReview.drafts.postFailed') }}: {{ draft.postError }}
|
|
127
133
|
</p>
|
|
128
134
|
<div v-if="editable" class="mt-2 flex gap-2">
|
|
129
|
-
<UButton
|
|
135
|
+
<UButton
|
|
136
|
+
color="neutral"
|
|
137
|
+
size="xs"
|
|
138
|
+
variant="ghost"
|
|
139
|
+
icon="i-lucide-pencil"
|
|
140
|
+
@click="startEdit"
|
|
141
|
+
>
|
|
130
142
|
{{ t('guidedReview.drafts.edit') }}
|
|
131
143
|
</UButton>
|
|
132
144
|
<UButton
|
|
145
|
+
color="error"
|
|
146
|
+
variant="soft"
|
|
133
147
|
size="xs"
|
|
134
|
-
variant="ghost"
|
|
135
|
-
color="neutral"
|
|
136
148
|
icon="i-lucide-trash-2"
|
|
137
149
|
:loading="saving"
|
|
138
150
|
@click="discard"
|
|
@@ -223,9 +223,9 @@ const canDestroy = computed(
|
|
|
223
223
|
{{ t('humanTest.actions.recreate') }}
|
|
224
224
|
</UButton>
|
|
225
225
|
<UButton
|
|
226
|
-
|
|
226
|
+
color="error"
|
|
227
227
|
variant="soft"
|
|
228
|
-
|
|
228
|
+
size="xs"
|
|
229
229
|
icon="i-lucide-trash-2"
|
|
230
230
|
:disabled="busy || !canDestroy || !access.canExecuteRuns.value"
|
|
231
231
|
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
@@ -281,6 +281,7 @@ const canDestroy = computed(
|
|
|
281
281
|
class="w-full"
|
|
282
282
|
/>
|
|
283
283
|
<UButton
|
|
284
|
+
variant="soft"
|
|
284
285
|
size="sm"
|
|
285
286
|
color="warning"
|
|
286
287
|
icon="i-lucide-wrench"
|
|
@@ -483,9 +483,9 @@ async function clearWeb() {
|
|
|
483
483
|
{{ t('common.save') }}
|
|
484
484
|
</UButton>
|
|
485
485
|
<UButton
|
|
486
|
+
color="error"
|
|
487
|
+
variant="soft"
|
|
486
488
|
v-if="summary?.slackOAuthConfigured"
|
|
487
|
-
color="neutral"
|
|
488
|
-
variant="ghost"
|
|
489
489
|
size="xs"
|
|
490
490
|
:loading="savingSlack"
|
|
491
491
|
@click="clearSlack"
|
|
@@ -538,9 +538,9 @@ async function clearWeb() {
|
|
|
538
538
|
{{ t('common.save') }}
|
|
539
539
|
</UButton>
|
|
540
540
|
<UButton
|
|
541
|
+
color="error"
|
|
542
|
+
variant="soft"
|
|
541
543
|
v-if="summary?.linearOAuthConfigured"
|
|
542
|
-
color="neutral"
|
|
543
|
-
variant="ghost"
|
|
544
544
|
size="xs"
|
|
545
545
|
:loading="savingLinear"
|
|
546
546
|
@click="clearLinear"
|
|
@@ -593,9 +593,9 @@ async function clearWeb() {
|
|
|
593
593
|
{{ t('common.save') }}
|
|
594
594
|
</UButton>
|
|
595
595
|
<UButton
|
|
596
|
+
color="error"
|
|
597
|
+
variant="soft"
|
|
596
598
|
v-if="summary?.figmaOAuthConfigured"
|
|
597
|
-
color="neutral"
|
|
598
|
-
variant="ghost"
|
|
599
599
|
size="xs"
|
|
600
600
|
:loading="savingFigma"
|
|
601
601
|
@click="clearFigma"
|
|
@@ -644,9 +644,9 @@ async function clearWeb() {
|
|
|
644
644
|
{{ t('common.save') }}
|
|
645
645
|
</UButton>
|
|
646
646
|
<UButton
|
|
647
|
+
color="error"
|
|
648
|
+
variant="soft"
|
|
647
649
|
v-if="summary?.webSearch"
|
|
648
|
-
color="neutral"
|
|
649
|
-
variant="ghost"
|
|
650
650
|
size="xs"
|
|
651
651
|
:loading="savingWeb"
|
|
652
652
|
@click="clearWeb"
|
|
@@ -53,7 +53,7 @@ function invitationStatusLabel(status: InvitationStatus): string {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** Whether the signed-in caller is an admin of this account (drives edit affordances). */
|
|
56
|
-
const isAdmin = computed(() => accounts.
|
|
56
|
+
const isAdmin = computed(() => accounts.isActiveAccountAdmin)
|
|
57
57
|
/**
|
|
58
58
|
* Members / roles / invitations are org-scoped — the backend rejects membership on a
|
|
59
59
|
* personal account. For a personal account we show a "create an organization" CTA in
|
|
@@ -328,7 +328,7 @@ async function disconnectEmail() {
|
|
|
328
328
|
</template>
|
|
329
329
|
<template #from>{{ accounts.emailConnection.fromAddress }}</template>
|
|
330
330
|
</i18n-t>
|
|
331
|
-
<UButton size="xs" color="error" variant="
|
|
331
|
+
<UButton size="xs" color="error" variant="soft" :loading="busy" @click="disconnectEmail">
|
|
332
332
|
{{ t('layout.accountTeam.email.disconnect') }}
|
|
333
333
|
</UButton>
|
|
334
334
|
</div>
|
|
@@ -12,7 +12,16 @@ import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
|
12
12
|
// account row folds away — it is a label with a menu duplicated inside the board menu's
|
|
13
13
|
// reach — and the board button keeps only its glyph. Both dropdowns are unchanged, so
|
|
14
14
|
// switching boards never requires expanding the sidebar first.
|
|
15
|
-
|
|
15
|
+
//
|
|
16
|
+
// `standalone` is for a host OUTSIDE the board page (the repository-host gate), where the
|
|
17
|
+
// page-level dialogs are not mounted. The items that open one are left out there: "Account
|
|
18
|
+
// settings" opens `AccountSettingsPanel` and "Delete board" awaits `ConfirmDialog`, so on the gate
|
|
19
|
+
// both would do nothing and then fire once a board page mounted, the delete against the board that
|
|
20
|
+
// was active when it was asked.
|
|
21
|
+
const props = withDefaults(defineProps<{ collapsed?: boolean; standalone?: boolean }>(), {
|
|
22
|
+
collapsed: false,
|
|
23
|
+
standalone: false,
|
|
24
|
+
})
|
|
16
25
|
|
|
17
26
|
const { t } = useI18n()
|
|
18
27
|
|
|
@@ -65,14 +74,19 @@ const accountItems = computed<DropdownMenuItem[][]>(() => [
|
|
|
65
74
|
},
|
|
66
75
|
// Account settings: the unified per-account panel (team + roles + invitations + email
|
|
67
76
|
// sender + account-tier fragment library). The panel itself handles personal accounts
|
|
68
|
-
// (prompting to create an org for the team tab), so this is not gated
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
// (prompting to create an org for the team tab), so this is not gated by role; it is left out
|
|
78
|
+
// only where the panel is not mounted (`standalone`).
|
|
79
|
+
...(props.standalone
|
|
80
|
+
? []
|
|
81
|
+
: [
|
|
82
|
+
{
|
|
83
|
+
label: t('layout.boardSwitcher.account.settings'),
|
|
84
|
+
icon: 'i-lucide-settings',
|
|
85
|
+
onSelect: () => ui.openAccountSettings(),
|
|
86
|
+
},
|
|
87
|
+
]),
|
|
74
88
|
// Admins can set the account-wide default provider new services inherit.
|
|
75
|
-
...(accounts.
|
|
89
|
+
...(accounts.isActiveAccountAdmin
|
|
76
90
|
? [
|
|
77
91
|
{
|
|
78
92
|
label: t('layout.boardSwitcher.account.defaultProvider'),
|
|
@@ -122,12 +136,17 @@ const boardItems = computed<DropdownMenuItem[][]>(() => [
|
|
|
122
136
|
icon: 'i-lucide-pencil',
|
|
123
137
|
onSelect: () => openPrompt('rename'),
|
|
124
138
|
},
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
// Delete confirms through the page's `ConfirmDialog`, absent in `standalone`.
|
|
140
|
+
...(props.standalone
|
|
141
|
+
? []
|
|
142
|
+
: [
|
|
143
|
+
{
|
|
144
|
+
label: t('layout.boardSwitcher.board.delete'),
|
|
145
|
+
icon: 'i-lucide-trash-2',
|
|
146
|
+
color: 'error' as const,
|
|
147
|
+
onSelect: () => void removeBoard(),
|
|
148
|
+
},
|
|
149
|
+
]),
|
|
131
150
|
]
|
|
132
151
|
: []),
|
|
133
152
|
],
|
|
@@ -22,14 +22,17 @@ const ui = useUiStore()
|
|
|
22
22
|
const workspace = useWorkspaceStore()
|
|
23
23
|
const settingsStore = useWorkspaceSettingsStore()
|
|
24
24
|
const { canManageSettings } = useWorkspaceAccess()
|
|
25
|
+
const uiRole = useUiRoleStore()
|
|
25
26
|
|
|
26
27
|
// Only prompt someone who can actually answer. A member/viewer cannot write workspace settings
|
|
27
28
|
// (the endpoint is `settings.manage`-gated), so for them this would be an un-actionable nag
|
|
28
|
-
// pointing at a screen that would refuse their save.
|
|
29
|
+
// pointing at a screen that would refuse their save. The designer surface never configures the
|
|
30
|
+
// platform, so it is kept off there too, whatever the grant.
|
|
29
31
|
const show = computed(
|
|
30
32
|
() =>
|
|
31
33
|
workspace.ready &&
|
|
32
34
|
canManageSettings.value &&
|
|
35
|
+
uiRole.fullSurface &&
|
|
33
36
|
needsDefaultProvisioningChoice(settingsStore.settings) &&
|
|
34
37
|
!ui.defaultProvisionDismissed,
|
|
35
38
|
)
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
// runtime whose account picked no backend — incl. Cloudflare without an ARTIFACT_BUCKET binding;
|
|
7
7
|
// ephemeral test environments on any runtime that wires the integration).
|
|
8
8
|
//
|
|
9
|
+
// Audience: a card renders only for a caller who can act on its area, per the server-reported
|
|
10
|
+
// owner (`infraSetupOwners`) and `canActOnSetup`. A caller who cannot (a member, a viewer, the
|
|
11
|
+
// designer surface, or anyone when the operator owns the gap) sees one compact line instead, and
|
|
12
|
+
// only for a gap that stops every run (`InfraSetupNotice`).
|
|
13
|
+
//
|
|
9
14
|
// Positioning/stacking against the sibling advisory banners (AI-readiness, provider-config) is
|
|
10
15
|
// owned by the shared, click-through banner column in `pages/index.vue` — so concurrent prompts
|
|
11
16
|
// stack vertically instead of drawing on top of each other. This component only stacks its OWN
|
|
@@ -52,11 +57,14 @@ import {
|
|
|
52
57
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
53
58
|
import type { InfraSetupArea, InfraSetupStatus } from '~/types/domain'
|
|
54
59
|
import { infraSetupDismissalKey, type InfraSetupCardKind } from '~/utils/infraSetup'
|
|
60
|
+
import { blockingSetupNotice, canActOnSetup } from '~/utils/setupAudience'
|
|
61
|
+
import InfraSetupNotice from '~/components/layout/InfraSetupNotice.vue'
|
|
55
62
|
|
|
56
63
|
const { t } = useI18n()
|
|
57
64
|
const ui = useUiStore()
|
|
58
65
|
const auth = useAuthStore()
|
|
59
66
|
const workspace = useWorkspaceStore()
|
|
67
|
+
const { actor } = useSetupActor()
|
|
60
68
|
|
|
61
69
|
// Severity order: no executor blocks EVERY agent, so it leads; a missing test environment blocks
|
|
62
70
|
// only testing agents; missing storage only degrades the UI-tester's screenshots.
|
|
@@ -140,6 +148,9 @@ const visible = computed<AreaCard[]>(() => {
|
|
|
140
148
|
return AREAS.filter((area) => {
|
|
141
149
|
const kind = cardKind(status[area])
|
|
142
150
|
if (!kind) return false
|
|
151
|
+
// A card is for the people who can act on it. Everyone else hears about a gap only when it
|
|
152
|
+
// stops their own work, through the one line below (`blockingSetupNotice`).
|
|
153
|
+
if (!canActOnSetup(workspace.infraSetupOwners?.[area], actor.value)) return false
|
|
143
154
|
// Both dismissals are keyed by the CLAIM, not by the area: silencing "you haven't configured
|
|
144
155
|
// this" must not also silence "you configured it and it is now down". The permanent dismissal
|
|
145
156
|
// only ever covers a setup gap (`dismissPermanently` is offered nowhere else).
|
|
@@ -152,6 +163,18 @@ const visible = computed<AreaCard[]>(() => {
|
|
|
152
163
|
}))
|
|
153
164
|
})
|
|
154
165
|
|
|
166
|
+
// The one line for a caller who cannot act on a gap that stops every run. It honours BOTH
|
|
167
|
+
// dismissals of the card it stands in for, keyed by the same claim: someone who said "don't
|
|
168
|
+
// notify me again" on the card (before their role changed, say) is not nagged again by the line.
|
|
169
|
+
const notice = computed(() => {
|
|
170
|
+
const found = blockingSetupNotice(workspace.infraSetup, workspace.infraSetupOwners, actor.value)
|
|
171
|
+
if (!found) return null
|
|
172
|
+
if (ui.infraSetupSessionDismissed.includes(infraSetupDismissalKey(found.area, found.kind))) {
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
return found.kind === 'setup' && dismissedForUser.value.includes(found.area) ? null : found
|
|
176
|
+
})
|
|
177
|
+
|
|
155
178
|
/** Which card an area's status raises, or null when it raises none (`configured`/`not_applicable`). */
|
|
156
179
|
function cardKind(status: InfraSetupStatus): InfraSetupCardKind | null {
|
|
157
180
|
if (isInfraSetupHealthStatus(status)) return 'outage'
|
|
@@ -169,7 +192,7 @@ function titleKey(card: AreaCard): string {
|
|
|
169
192
|
* The dismiss dropdown: the product wants the user asked WHICH kind of dismissal on close. An
|
|
170
193
|
* outage offers the session option ONLY — see the fork note at the top of this file.
|
|
171
194
|
*/
|
|
172
|
-
function dismissMenu(card: AreaCard): DropdownMenuItem[][] {
|
|
195
|
+
function dismissMenu(card: Pick<AreaCard, 'area' | 'kind'>): DropdownMenuItem[][] {
|
|
173
196
|
const session = {
|
|
174
197
|
label: t('layout.infraSetupBanner.dismiss.session'),
|
|
175
198
|
icon: 'i-lucide-clock',
|
|
@@ -195,11 +218,12 @@ function dismissMenu(card: AreaCard): DropdownMenuItem[][] {
|
|
|
195
218
|
than an assertive `role="alert"` per card — an advisory setup nag shouldn't interrupt a
|
|
196
219
|
screen reader, and up to three stacked alerts would spam it. -->
|
|
197
220
|
<div
|
|
198
|
-
v-if="visible.length > 0"
|
|
221
|
+
v-if="visible.length > 0 || notice"
|
|
199
222
|
class="flex w-full flex-col items-center gap-2"
|
|
200
223
|
role="status"
|
|
201
224
|
aria-live="polite"
|
|
202
225
|
>
|
|
226
|
+
<InfraSetupNotice v-if="notice" :notice="notice" :dismiss-menu="dismissMenu(notice)" />
|
|
203
227
|
<!-- An OUTAGE reads red, a setup gap amber: one is something breaking now, the other is
|
|
204
228
|
something never switched on, and a reader has to be able to tell at a glance. -->
|
|
205
229
|
<div
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The one line a caller sees for a setup gap they cannot fix from where they are, when it stops
|
|
3
|
+
// their runs. It replaces the full card (and its "Configure" button that would open a screen their
|
|
4
|
+
// save is refused on) with two facts: what does not work, and how it gets fixed. Compact on
|
|
5
|
+
// purpose: the caller has nothing to do here, so it must not cover the board the way an actionable
|
|
6
|
+
// card may.
|
|
7
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
8
|
+
import type { BlockingInfraArea, SetupNotice, SetupRemedy } from '~/utils/setupAudience'
|
|
9
|
+
|
|
10
|
+
defineProps<{
|
|
11
|
+
notice: SetupNotice
|
|
12
|
+
/** The card's own session / permanent choice, built by the banner so both close the same claim. */
|
|
13
|
+
dismissMenu: DropdownMenuItem[][]
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const { t } = useI18n()
|
|
17
|
+
|
|
18
|
+
// Exhaustive over the blocking areas and the remedies, so a new member of either fails typecheck
|
|
19
|
+
// here rather than rendering a raw key.
|
|
20
|
+
const STATE_KEYS: Record<BlockingInfraArea, Record<SetupNotice['kind'], string>> = {
|
|
21
|
+
agentExecutor: {
|
|
22
|
+
setup: 'layout.infraSetupNotice.agentExecutor.setup',
|
|
23
|
+
outage: 'layout.infraSetupNotice.agentExecutor.outage',
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
const REMEDY_KEYS: Record<SetupRemedy, string> = {
|
|
27
|
+
workspace_admin: 'layout.infraSetupNotice.owner.workspace_admin',
|
|
28
|
+
account_admin: 'layout.infraSetupNotice.owner.account_admin',
|
|
29
|
+
operator: 'layout.infraSetupNotice.owner.operator',
|
|
30
|
+
full_surface: 'layout.infraSetupNotice.fullSurface',
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<div
|
|
36
|
+
class="pointer-events-auto flex w-full max-w-3xl items-center gap-3 rounded-xl border px-4 py-2 shadow-lg backdrop-blur"
|
|
37
|
+
:class="
|
|
38
|
+
notice.kind === 'outage'
|
|
39
|
+
? 'border-app-error-500/50 bg-app-error-950/90'
|
|
40
|
+
: 'border-app-warning-500/50 bg-app-warning-950/90'
|
|
41
|
+
"
|
|
42
|
+
:data-testid="`infra-setup-notice-${notice.area}`"
|
|
43
|
+
:data-infra-remedy="notice.remedy"
|
|
44
|
+
>
|
|
45
|
+
<UIcon
|
|
46
|
+
:name="notice.kind === 'outage' ? 'i-lucide-plug-zap' : 'i-lucide-server-off'"
|
|
47
|
+
class="h-5 w-5 shrink-0"
|
|
48
|
+
:class="notice.kind === 'outage' ? 'text-app-error-400' : 'text-app-warning-400'"
|
|
49
|
+
/>
|
|
50
|
+
<p
|
|
51
|
+
class="min-w-0 flex-1 text-sm"
|
|
52
|
+
:class="notice.kind === 'outage' ? 'text-app-error-100' : 'text-app-warning-100'"
|
|
53
|
+
>
|
|
54
|
+
{{ t(STATE_KEYS[notice.area][notice.kind]) }}
|
|
55
|
+
<span class="opacity-80">{{ t(REMEDY_KEYS[notice.remedy]) }}</span>
|
|
56
|
+
</p>
|
|
57
|
+
<UDropdownMenu :items="dismissMenu" :content="{ align: 'end' }">
|
|
58
|
+
<UButton
|
|
59
|
+
color="neutral"
|
|
60
|
+
variant="ghost"
|
|
61
|
+
size="xs"
|
|
62
|
+
icon="i-lucide-x"
|
|
63
|
+
class="shrink-0"
|
|
64
|
+
:aria-label="t('common.close')"
|
|
65
|
+
:data-testid="`infra-setup-notice-dismiss-${notice.area}`"
|
|
66
|
+
/>
|
|
67
|
+
</UDropdownMenu>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|