@cat-factory/app 0.143.0 → 0.144.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/AddTaskModal.vue +71 -134
- package/app/components/fragments/FragmentSelector.vue +121 -0
- package/app/components/panels/inspector/ServiceFragments.vue +15 -91
- package/app/components/panels/inspector/TaskStructure.vue +13 -95
- package/app/stores/board.ts +4 -1
- package/i18n/locales/de.json +0 -1
- package/i18n/locales/en.json +0 -1
- package/i18n/locales/es.json +0 -1
- package/i18n/locales/fr.json +0 -1
- package/i18n/locales/he.json +0 -1
- package/i18n/locales/it.json +0 -1
- package/i18n/locales/ja.json +0 -1
- package/i18n/locales/pl.json +0 -1
- package/i18n/locales/tr.json +0 -1
- package/i18n/locales/uk.json +0 -1
- package/package.json +2 -2
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// Create a new task on the board. The user names the task and writes its
|
|
3
|
-
// description themselves (a REVIEW task is the one exception —
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// description themselves (a REVIEW task is the one exception — it shows neither Title
|
|
4
|
+
// nor Description: the target PR IS the subject, so the title is derived from the PR
|
|
5
|
+
// reference and any notes go in the dedicated "Review focus" field). The task lands in
|
|
6
|
+
// `planned` state; it is never launched here. The user starts a pipeline on it
|
|
7
|
+
// explicitly (and can keep editing it until they do).
|
|
7
8
|
//
|
|
8
9
|
// The form also shows ungated "Context documents" / "Context issues" sections
|
|
9
10
|
// (mirroring the task inspector): an inline search picker (ContextDocumentPicker /
|
|
@@ -20,12 +21,11 @@ import type {
|
|
|
20
21
|
TaskTypeFields,
|
|
21
22
|
} from '~/types/domain'
|
|
22
23
|
import { DOC_KINDS, DOC_KIND_FIELDS } from '~/types/domain'
|
|
23
|
-
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
24
24
|
import ContextDocumentPicker from '~/components/documents/ContextDocumentPicker.vue'
|
|
25
25
|
import ContextIssuePicker from '~/components/tasks/ContextIssuePicker.vue'
|
|
26
|
+
import FragmentSelector from '~/components/fragments/FragmentSelector.vue'
|
|
26
27
|
import { riskPolicyOptionLabel, riskPolicySummary } from '~/utils/riskPolicy'
|
|
27
28
|
import { pipelineAllowedForManualStart } from '~/utils/pipeline'
|
|
28
|
-
import { buildFragmentPickerGroups } from '~/utils/fragmentPicker'
|
|
29
29
|
|
|
30
30
|
const ui = useUiStore()
|
|
31
31
|
const board = useBoardStore()
|
|
@@ -36,7 +36,6 @@ const modelPresets = useModelPresetsStore()
|
|
|
36
36
|
const pipelines = usePipelinesStore()
|
|
37
37
|
const agentConfig = useAgentConfigStore()
|
|
38
38
|
const fragments = useFragmentsStore()
|
|
39
|
-
const accounts = useAccountsStore()
|
|
40
39
|
const toast = useToast()
|
|
41
40
|
const { t } = useI18n()
|
|
42
41
|
|
|
@@ -313,50 +312,10 @@ const selectedModelPresetLabel = computed(() => {
|
|
|
313
312
|
})
|
|
314
313
|
|
|
315
314
|
// ---- best-practice prompt fragments (pinned at creation) -------------------
|
|
316
|
-
// The
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
const
|
|
320
|
-
fragmentIds.value.map((id) => fragments.getFragment(id) ?? { id, title: id, summary: '' }),
|
|
321
|
-
)
|
|
322
|
-
// A trailing group linking out to the fragment library (board tier always; account tier when
|
|
323
|
-
// enabled) — managing fragments is open to every member, exactly like the inspector picker.
|
|
324
|
-
const fragmentManageItems = computed<DropdownMenuItem[]>(() => {
|
|
325
|
-
const items: DropdownMenuItem[] = [
|
|
326
|
-
{
|
|
327
|
-
label: t('inspector.fragments.manageBoard'),
|
|
328
|
-
icon: 'i-lucide-book-marked',
|
|
329
|
-
onSelect: () => ui.openFragmentLibrary(),
|
|
330
|
-
},
|
|
331
|
-
]
|
|
332
|
-
if (accounts.enabled) {
|
|
333
|
-
items.push({
|
|
334
|
-
label: t('inspector.fragments.manageAccount'),
|
|
335
|
-
icon: 'i-lucide-users',
|
|
336
|
-
onSelect: () => ui.openAccountSettings('fragments'),
|
|
337
|
-
})
|
|
338
|
-
}
|
|
339
|
-
return items
|
|
340
|
-
})
|
|
341
|
-
// Picker menu: fragments appropriate to the enclosing frame's block type (the "scope"), not
|
|
342
|
-
// already selected, grouped by category, with the management links appended as the final group.
|
|
343
|
-
// Falls back to `service` before a frame resolves so the catalog is still browsable.
|
|
344
|
-
const fragmentMenu = computed<DropdownMenuItem[][]>(() => {
|
|
345
|
-
const selected = new Set(fragmentIds.value)
|
|
346
|
-
return [
|
|
347
|
-
...buildFragmentPickerGroups(
|
|
348
|
-
fragments.forBlockType(frame.value?.type ?? 'service'),
|
|
349
|
-
(id) => selected.has(id),
|
|
350
|
-
(id) => {
|
|
351
|
-
if (!fragmentIds.value.includes(id)) fragmentIds.value = [...fragmentIds.value, id]
|
|
352
|
-
},
|
|
353
|
-
),
|
|
354
|
-
fragmentManageItems.value,
|
|
355
|
-
]
|
|
356
|
-
})
|
|
357
|
-
function removeFragment(id: string) {
|
|
358
|
-
fragmentIds.value = fragmentIds.value.filter((x) => x !== id)
|
|
359
|
-
}
|
|
315
|
+
// The pool the shared <FragmentSelector> offers: fragments appropriate to the enclosing frame's
|
|
316
|
+
// block type (the "scope"). Falls back to `service` before a frame resolves so the catalog is
|
|
317
|
+
// still browsable.
|
|
318
|
+
const fragmentPool = computed(() => fragments.forBlockType(frame.value?.type ?? 'service'))
|
|
360
319
|
|
|
361
320
|
// Hide UI-testing pipelines (`tester-ui` / `visual-confirmation`) when the target frame has no
|
|
362
321
|
// UI to exercise — they'd be refused server-side (see utils/pipeline + the backend gate). Also
|
|
@@ -543,7 +502,11 @@ watch(open, (isOpen) => {
|
|
|
543
502
|
docOutlineHints.value = ''
|
|
544
503
|
reviewPrRef.value = ''
|
|
545
504
|
reviewFocus.value = ''
|
|
546
|
-
|
|
505
|
+
// Pre-seed the best-practice fragments from the enclosing service's standards, so a new task
|
|
506
|
+
// ships with its service's fragments already selected (and freely add/removable here). The task
|
|
507
|
+
// OWNS this selection from creation — the engine folds exactly these, without re-unioning the
|
|
508
|
+
// service's set, so removing one here actually drops it for this task.
|
|
509
|
+
fragmentIds.value = [...(frame.value?.serviceFragmentIds ?? [])]
|
|
547
510
|
for (const key of Object.keys(docKindFieldValues) as DocKindFieldKey[])
|
|
548
511
|
delete docKindFieldValues[key]
|
|
549
512
|
riskPolicyId.value = ''
|
|
@@ -663,7 +626,10 @@ async function add() {
|
|
|
663
626
|
...(Object.keys(agentConfigValues.value).length
|
|
664
627
|
? { agentConfig: agentConfigValues.value }
|
|
665
628
|
: {}),
|
|
666
|
-
|
|
629
|
+
// Always send the (service-seeded, then user-edited) selection — including an empty list,
|
|
630
|
+
// which means "the user cleared the inherited picks" and must be honoured rather than
|
|
631
|
+
// re-seeded from the service. The task owns its fragments from here.
|
|
632
|
+
fragmentIds: [...fragmentIds.value],
|
|
667
633
|
...(technical.value ? { technical: true } : {}),
|
|
668
634
|
})
|
|
669
635
|
if (block) {
|
|
@@ -732,65 +698,62 @@ async function add() {
|
|
|
732
698
|
</div>
|
|
733
699
|
|
|
734
700
|
<template v-if="!isRecurring">
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
>
|
|
701
|
+
<!-- A review task shows neither Title nor Description: the target PR is the
|
|
702
|
+
subject (the title is derived from the PR reference), and any notes go in
|
|
703
|
+
the dedicated "Review focus" field below. -->
|
|
704
|
+
<UFormField v-if="!isReview" :label="t('board.addTask.titleField')" required>
|
|
740
705
|
<UInput
|
|
741
706
|
v-model="title"
|
|
742
707
|
data-testid="add-task-title"
|
|
743
|
-
:placeholder="
|
|
744
|
-
isReview
|
|
745
|
-
? t('board.addTask.titlePlaceholderReview')
|
|
746
|
-
: t('board.addTask.titlePlaceholder')
|
|
747
|
-
"
|
|
708
|
+
:placeholder="t('board.addTask.titlePlaceholder')"
|
|
748
709
|
autofocus
|
|
749
710
|
class="w-full"
|
|
750
711
|
@keydown.enter="add"
|
|
751
712
|
/>
|
|
752
713
|
</UFormField>
|
|
753
714
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
715
|
+
<template v-if="!isReview">
|
|
716
|
+
<!-- Linked issue description(s), read-only: shown so the user sees the original
|
|
717
|
+
issue description is included in the task. It's folded into the saved
|
|
718
|
+
description (before their notes) on add. -->
|
|
719
|
+
<UFormField
|
|
720
|
+
v-for="issue in linkedIssueBodies"
|
|
721
|
+
:key="issue.key"
|
|
722
|
+
:label="t('board.addTask.issueIncluded', { title: issue.title })"
|
|
723
|
+
>
|
|
724
|
+
<UTextarea
|
|
725
|
+
:model-value="issue.body"
|
|
726
|
+
:rows="4"
|
|
727
|
+
autoresize
|
|
728
|
+
readonly
|
|
729
|
+
class="w-full"
|
|
730
|
+
:ui="{ base: 'cursor-default text-slate-300' }"
|
|
731
|
+
/>
|
|
732
|
+
</UFormField>
|
|
733
|
+
<p v-if="resolvingIssueBodies" class="text-[11px] text-slate-500">
|
|
734
|
+
{{ t('board.addTask.loadingIssue') }}
|
|
735
|
+
</p>
|
|
774
736
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
hasLinkedIssueBody
|
|
778
|
-
? t('board.addTask.additionalNotes')
|
|
779
|
-
: t('board.addTask.description')
|
|
780
|
-
"
|
|
781
|
-
>
|
|
782
|
-
<UTextarea
|
|
783
|
-
v-model="description"
|
|
784
|
-
:rows="4"
|
|
785
|
-
autoresize
|
|
786
|
-
:placeholder="
|
|
737
|
+
<UFormField
|
|
738
|
+
:label="
|
|
787
739
|
hasLinkedIssueBody
|
|
788
|
-
? t('board.addTask.
|
|
789
|
-
: t('board.addTask.
|
|
740
|
+
? t('board.addTask.additionalNotes')
|
|
741
|
+
: t('board.addTask.description')
|
|
790
742
|
"
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
743
|
+
>
|
|
744
|
+
<UTextarea
|
|
745
|
+
v-model="description"
|
|
746
|
+
:rows="4"
|
|
747
|
+
autoresize
|
|
748
|
+
:placeholder="
|
|
749
|
+
hasLinkedIssueBody
|
|
750
|
+
? t('board.addTask.notesPlaceholder')
|
|
751
|
+
: t('board.addTask.descriptionPlaceholder')
|
|
752
|
+
"
|
|
753
|
+
class="w-full"
|
|
754
|
+
/>
|
|
755
|
+
</UFormField>
|
|
756
|
+
</template>
|
|
794
757
|
|
|
795
758
|
<UCheckbox v-model="technical" name="technical">
|
|
796
759
|
<template #label>
|
|
@@ -1060,41 +1023,15 @@ async function add() {
|
|
|
1060
1023
|
</div>
|
|
1061
1024
|
</div>
|
|
1062
1025
|
|
|
1063
|
-
<!-- Best-practice fragments pinned on the task at creation, scoped to the frame's type.
|
|
1026
|
+
<!-- Best-practice fragments pinned on the task at creation, scoped to the frame's type.
|
|
1027
|
+
Pre-seeded from the enclosing service's standards; the task owns them from here. -->
|
|
1064
1028
|
<div class="space-y-2">
|
|
1065
|
-
<
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
color="neutral"
|
|
1072
|
-
variant="soft"
|
|
1073
|
-
size="xs"
|
|
1074
|
-
icon="i-lucide-plus"
|
|
1075
|
-
trailing-icon="i-lucide-chevron-down"
|
|
1076
|
-
>
|
|
1077
|
-
{{ t('board.addTask.attach') }}
|
|
1078
|
-
</UButton>
|
|
1079
|
-
</UDropdownMenu>
|
|
1080
|
-
</div>
|
|
1081
|
-
<div v-if="selectedFragments.length" class="flex flex-wrap gap-1">
|
|
1082
|
-
<UBadge
|
|
1083
|
-
v-for="f in selectedFragments"
|
|
1084
|
-
:key="f.id"
|
|
1085
|
-
color="primary"
|
|
1086
|
-
variant="subtle"
|
|
1087
|
-
size="sm"
|
|
1088
|
-
class="cursor-pointer"
|
|
1089
|
-
:title="f.summary"
|
|
1090
|
-
@click="removeFragment(f.id)"
|
|
1091
|
-
>
|
|
1092
|
-
{{ f.title }}<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
|
|
1093
|
-
</UBadge>
|
|
1094
|
-
</div>
|
|
1095
|
-
<p v-else class="text-[11px] text-slate-500">
|
|
1096
|
-
{{ t('board.addTask.bestPracticesHint') }}
|
|
1097
|
-
</p>
|
|
1029
|
+
<FragmentSelector
|
|
1030
|
+
v-model="fragmentIds"
|
|
1031
|
+
:pool="fragmentPool"
|
|
1032
|
+
:label="t('board.addTask.bestPractices')"
|
|
1033
|
+
:empty-text="t('board.addTask.bestPracticesHint')"
|
|
1034
|
+
/>
|
|
1098
1035
|
</div>
|
|
1099
1036
|
|
|
1100
1037
|
<!-- Context documents (ungated; Attach disabled until a source is connected). -->
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Shared best-practice prompt-fragment picker: a category-grouped "add" dropdown (with links out
|
|
3
|
+
// to the fragment library) plus the currently-selected fragments as removable badges.
|
|
4
|
+
// Presentational + `v-model`-driven — the caller owns WHERE the selection lives (a task's
|
|
5
|
+
// `fragmentIds`, a service's `serviceFragmentIds`, or a not-yet-created task's local draft) and
|
|
6
|
+
// binds the id list. Authored once and reused by the create-task form and the task/service
|
|
7
|
+
// inspectors so the three pickers can't drift. Ids the catalog no longer resolves still render
|
|
8
|
+
// (labelled by their raw id) so they stay visible and removable.
|
|
9
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
10
|
+
import type { PromptFragment } from '~/types/domain'
|
|
11
|
+
import { buildFragmentPickerGroups } from '~/utils/fragmentPicker'
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(
|
|
14
|
+
defineProps<{
|
|
15
|
+
/** The selected fragment ids (`v-model`). */
|
|
16
|
+
modelValue: string[]
|
|
17
|
+
/** Candidate pool to offer — e.g. `fragments.forBlockType(type)` or the whole catalog. */
|
|
18
|
+
pool: PromptFragment[]
|
|
19
|
+
/** Optional label rendered to the left of the add button (omit when a section supplies it). */
|
|
20
|
+
label?: string
|
|
21
|
+
/** Optional text shown when nothing is selected. */
|
|
22
|
+
emptyText?: string
|
|
23
|
+
}>(),
|
|
24
|
+
{ label: '', emptyText: '' },
|
|
25
|
+
)
|
|
26
|
+
const emit = defineEmits<{ 'update:modelValue': [string[]] }>()
|
|
27
|
+
|
|
28
|
+
const fragments = useFragmentsStore()
|
|
29
|
+
const ui = useUiStore()
|
|
30
|
+
const accounts = useAccountsStore()
|
|
31
|
+
const { t } = useI18n()
|
|
32
|
+
|
|
33
|
+
// The catalog is per-board and invalidated on a workspace switch; (re)load it lazily — a no-op
|
|
34
|
+
// while current.
|
|
35
|
+
onMounted(() => fragments.ensureLoaded())
|
|
36
|
+
|
|
37
|
+
const selectedFragments = computed(() =>
|
|
38
|
+
props.modelValue.map((id) => fragments.getFragment(id) ?? { id, title: id, summary: '' }),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// A trailing group that jumps from "attach a fragment" to authoring/editing the library itself
|
|
42
|
+
// (board tier always; account tier when accounts are enabled). Open to every member.
|
|
43
|
+
const manageItems = computed<DropdownMenuItem[]>(() => {
|
|
44
|
+
const items: DropdownMenuItem[] = [
|
|
45
|
+
{
|
|
46
|
+
label: t('inspector.fragments.manageBoard'),
|
|
47
|
+
icon: 'i-lucide-book-marked',
|
|
48
|
+
onSelect: () => ui.openFragmentLibrary(),
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
if (accounts.enabled) {
|
|
52
|
+
items.push({
|
|
53
|
+
label: t('inspector.fragments.manageAccount'),
|
|
54
|
+
icon: 'i-lucide-users',
|
|
55
|
+
onSelect: () => ui.openAccountSettings('fragments'),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
return items
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Picker menu: pool fragments not already selected, grouped into labelled per-category sections,
|
|
62
|
+
// with the management links appended as the final group.
|
|
63
|
+
const fragmentMenu = computed<DropdownMenuItem[][]>(() => {
|
|
64
|
+
const selected = new Set(props.modelValue)
|
|
65
|
+
return [
|
|
66
|
+
...buildFragmentPickerGroups(props.pool, (id) => selected.has(id), addFragment),
|
|
67
|
+
manageItems.value,
|
|
68
|
+
]
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
function addFragment(id: string) {
|
|
72
|
+
if (props.modelValue.includes(id)) return
|
|
73
|
+
emit('update:modelValue', [...props.modelValue, id])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function removeFragment(id: string) {
|
|
77
|
+
emit(
|
|
78
|
+
'update:modelValue',
|
|
79
|
+
props.modelValue.filter((x) => x !== id),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<div>
|
|
86
|
+
<div class="mb-1 flex items-center justify-between gap-2">
|
|
87
|
+
<span v-if="label" class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
88
|
+
{{ label }}
|
|
89
|
+
</span>
|
|
90
|
+
<span v-else />
|
|
91
|
+
<UDropdownMenu :items="fragmentMenu">
|
|
92
|
+
<UButton
|
|
93
|
+
size="xs"
|
|
94
|
+
variant="ghost"
|
|
95
|
+
color="neutral"
|
|
96
|
+
icon="i-lucide-plus"
|
|
97
|
+
trailing-icon="i-lucide-chevron-down"
|
|
98
|
+
data-testid="fragment-add"
|
|
99
|
+
/>
|
|
100
|
+
</UDropdownMenu>
|
|
101
|
+
</div>
|
|
102
|
+
<div v-if="selectedFragments.length" class="flex flex-wrap gap-1">
|
|
103
|
+
<UBadge
|
|
104
|
+
v-for="f in selectedFragments"
|
|
105
|
+
:key="f.id"
|
|
106
|
+
color="primary"
|
|
107
|
+
variant="subtle"
|
|
108
|
+
size="sm"
|
|
109
|
+
class="cursor-pointer"
|
|
110
|
+
:title="f.summary"
|
|
111
|
+
data-testid="fragment-badge"
|
|
112
|
+
@click="removeFragment(f.id)"
|
|
113
|
+
>
|
|
114
|
+
{{ f.title }}<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
|
|
115
|
+
</UBadge>
|
|
116
|
+
</div>
|
|
117
|
+
<div v-else-if="emptyText" class="text-[11px] text-slate-500">
|
|
118
|
+
{{ emptyText }}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</template>
|
|
@@ -1,76 +1,22 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
3
2
|
import type { Block } from '~/types/domain'
|
|
4
3
|
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
5
|
-
import
|
|
4
|
+
import FragmentSelector from '~/components/fragments/FragmentSelector.vue'
|
|
6
5
|
|
|
7
6
|
// Service-level best-practice fragments (frame blocks). These are the programming
|
|
8
|
-
// standards/guidelines for the whole service
|
|
9
|
-
// into the
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// (the add-service modal); the inspector leaves it collapsed.
|
|
7
|
+
// standards/guidelines for the whole service: they SEED a new task's own selection at creation,
|
|
8
|
+
// and at run time their bodies fold into the frame's own `code-aware` runs. Drawn from the
|
|
9
|
+
// board's merged fragment catalog (built-in ∪ registered ∪ account ∪ workspace). `defaultOpen`
|
|
10
|
+
// expands the section on surfaces that embed this as the primary content (the add-service
|
|
11
|
+
// modal); the inspector leaves it collapsed. The shared <FragmentSelector> renders the picker.
|
|
14
12
|
const props = defineProps<{ block: Block; defaultOpen?: boolean }>()
|
|
15
13
|
|
|
16
14
|
const board = useBoardStore()
|
|
17
15
|
const fragments = useFragmentsStore()
|
|
18
|
-
const ui = useUiStore()
|
|
19
|
-
const accounts = useAccountsStore()
|
|
20
16
|
const { t } = useI18n()
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// An id the catalog no longer resolves (removed/suppressed after selection) still
|
|
25
|
-
// renders — labelled by its raw id — so it stays visible and removable.
|
|
26
|
-
const selectedFragments = computed(() =>
|
|
27
|
-
(props.block.serviceFragmentIds ?? []).map(
|
|
28
|
-
(id) => fragments.getFragment(id) ?? { id, title: id, summary: '' },
|
|
29
|
-
),
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
// A trailing group that jumps from "attach a fragment" to authoring/editing the
|
|
33
|
-
// library itself (board tier always; account tier when accounts are enabled).
|
|
34
|
-
// Open to every member — managing fragments is not an admin-only action.
|
|
35
|
-
const manageItems = computed<DropdownMenuItem[]>(() => {
|
|
36
|
-
const items: DropdownMenuItem[] = [
|
|
37
|
-
{
|
|
38
|
-
label: t('inspector.fragments.manageBoard'),
|
|
39
|
-
icon: 'i-lucide-book-marked',
|
|
40
|
-
onSelect: () => ui.openFragmentLibrary(),
|
|
41
|
-
},
|
|
42
|
-
]
|
|
43
|
-
if (accounts.enabled) {
|
|
44
|
-
items.push({
|
|
45
|
-
label: t('inspector.fragments.manageAccount'),
|
|
46
|
-
icon: 'i-lucide-users',
|
|
47
|
-
onSelect: () => ui.openAccountSettings('fragments'),
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
return items
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
// Picker menu: every pool fragment not already selected, grouped into labelled
|
|
54
|
-
// per-category sections, with the management links appended as the final group.
|
|
55
|
-
const fragmentMenu = computed<DropdownMenuItem[][]>(() => {
|
|
56
|
-
const selected = new Set(props.block.serviceFragmentIds ?? [])
|
|
57
|
-
return [
|
|
58
|
-
...buildFragmentPickerGroups(fragments.fragments, (id) => selected.has(id), addFragment),
|
|
59
|
-
manageItems.value,
|
|
60
|
-
]
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
function addFragment(id: string) {
|
|
64
|
-
const list = props.block.serviceFragmentIds ? [...props.block.serviceFragmentIds] : []
|
|
65
|
-
if (!list.includes(id)) list.push(id)
|
|
66
|
-
board.updateBlock(props.block.id, { serviceFragmentIds: list })
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function removeFragment(id: string) {
|
|
70
|
-
if (!props.block.serviceFragmentIds) return
|
|
71
|
-
board.updateBlock(props.block.id, {
|
|
72
|
-
serviceFragmentIds: props.block.serviceFragmentIds.filter((x) => x !== id),
|
|
73
|
-
})
|
|
18
|
+
function setFragments(ids: string[]) {
|
|
19
|
+
board.updateBlock(props.block.id, { serviceFragmentIds: ids })
|
|
74
20
|
}
|
|
75
21
|
</script>
|
|
76
22
|
|
|
@@ -78,36 +24,14 @@ function removeFragment(id: string) {
|
|
|
78
24
|
<InspectorSection
|
|
79
25
|
:title="t('inspector.fragments.serviceTitle')"
|
|
80
26
|
:hint="t('inspector.fragments.serviceHint')"
|
|
81
|
-
:count="
|
|
27
|
+
:count="(block.serviceFragmentIds ?? []).length"
|
|
82
28
|
:default-open="props.defaultOpen"
|
|
83
29
|
>
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
icon="i-lucide-plus"
|
|
91
|
-
trailing-icon="i-lucide-chevron-down"
|
|
92
|
-
/>
|
|
93
|
-
</UDropdownMenu>
|
|
94
|
-
</template>
|
|
95
|
-
<div v-if="selectedFragments.length" class="flex flex-wrap gap-1">
|
|
96
|
-
<UBadge
|
|
97
|
-
v-for="f in selectedFragments"
|
|
98
|
-
:key="f.id"
|
|
99
|
-
color="primary"
|
|
100
|
-
variant="subtle"
|
|
101
|
-
size="sm"
|
|
102
|
-
class="cursor-pointer"
|
|
103
|
-
:title="f.summary"
|
|
104
|
-
@click="removeFragment(f.id)"
|
|
105
|
-
>
|
|
106
|
-
{{ f.title }}<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
|
|
107
|
-
</UBadge>
|
|
108
|
-
</div>
|
|
109
|
-
<div v-else class="text-[11px] text-slate-500">
|
|
110
|
-
{{ t('inspector.fragments.serviceEmpty') }}
|
|
111
|
-
</div>
|
|
30
|
+
<FragmentSelector
|
|
31
|
+
:model-value="block.serviceFragmentIds ?? []"
|
|
32
|
+
:pool="fragments.fragments"
|
|
33
|
+
:empty-text="t('inspector.fragments.serviceEmpty')"
|
|
34
|
+
@update:model-value="setFragments"
|
|
35
|
+
/>
|
|
112
36
|
</InspectorSection>
|
|
113
37
|
</template>
|
|
@@ -1,78 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
3
2
|
import type { Block } from '~/types/domain'
|
|
4
3
|
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
5
|
-
import
|
|
4
|
+
import FragmentSelector from '~/components/fragments/FragmentSelector.vue'
|
|
6
5
|
|
|
7
6
|
const props = defineProps<{ block: Block }>()
|
|
8
7
|
|
|
9
8
|
const board = useBoardStore()
|
|
10
9
|
const fragments = useFragmentsStore()
|
|
11
|
-
const ui = useUiStore()
|
|
12
|
-
const accounts = useAccountsStore()
|
|
13
10
|
const { t } = useI18n()
|
|
14
11
|
|
|
15
|
-
// The catalog is per-board and invalidated on a workspace switch, so (re)load it when the
|
|
16
|
-
// task inspector mounts — mirrors ServiceFragments; ensureLoaded is a no-op while current.
|
|
17
|
-
onMounted(() => fragments.ensureLoaded())
|
|
18
|
-
|
|
19
12
|
// ---- best-practice prompt fragments ----------------------------------------
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
(props.block.fragmentIds
|
|
25
|
-
(id) => fragments.getFragment(id) ?? { id, title: id, summary: '' },
|
|
26
|
-
),
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
// A trailing group that jumps from "attach a fragment" to authoring/editing the
|
|
30
|
-
// library itself (board tier always; account tier when accounts are enabled).
|
|
31
|
-
// Open to every member — managing fragments is not an admin-only action.
|
|
32
|
-
const manageItems = computed<DropdownMenuItem[]>(() => {
|
|
33
|
-
const items: DropdownMenuItem[] = [
|
|
34
|
-
{
|
|
35
|
-
label: t('inspector.fragments.manageBoard'),
|
|
36
|
-
icon: 'i-lucide-book-marked',
|
|
37
|
-
onSelect: () => ui.openFragmentLibrary(),
|
|
38
|
-
},
|
|
39
|
-
]
|
|
40
|
-
if (accounts.enabled) {
|
|
41
|
-
items.push({
|
|
42
|
-
label: t('inspector.fragments.manageAccount'),
|
|
43
|
-
icon: 'i-lucide-users',
|
|
44
|
-
onSelect: () => ui.openAccountSettings('fragments'),
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
return items
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
// Picker menu: fragments suitable for this block's type, not already selected,
|
|
51
|
-
// grouped into labelled per-category sections so the dropdown reads like the catalog,
|
|
52
|
-
// with the management links appended as the final group.
|
|
53
|
-
const fragmentMenu = computed<DropdownMenuItem[][]>(() => {
|
|
54
|
-
const selected = new Set(props.block.fragmentIds ?? [])
|
|
55
|
-
return [
|
|
56
|
-
...buildFragmentPickerGroups(
|
|
57
|
-
fragments.forBlockType(props.block.type),
|
|
58
|
-
(id) => selected.has(id),
|
|
59
|
-
addFragment,
|
|
60
|
-
),
|
|
61
|
-
manageItems.value,
|
|
62
|
-
]
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
function addFragment(id: string) {
|
|
66
|
-
const list = props.block.fragmentIds ? [...props.block.fragmentIds] : []
|
|
67
|
-
if (!list.includes(id)) list.push(id)
|
|
68
|
-
board.updateBlock(props.block.id, { fragmentIds: list })
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function removeFragment(id: string) {
|
|
72
|
-
if (!props.block.fragmentIds) return
|
|
73
|
-
board.updateBlock(props.block.id, {
|
|
74
|
-
fragmentIds: props.block.fragmentIds.filter((x) => x !== id),
|
|
75
|
-
})
|
|
13
|
+
// The task's OWN selection (seeded from its service at creation, then editable per task). The
|
|
14
|
+
// shared <FragmentSelector> renders the picker; a change persists via updateBlock.
|
|
15
|
+
const fragmentPool = computed(() => fragments.forBlockType(props.block.type))
|
|
16
|
+
function setFragments(ids: string[]) {
|
|
17
|
+
board.updateBlock(props.block.id, { fragmentIds: ids })
|
|
76
18
|
}
|
|
77
19
|
</script>
|
|
78
20
|
|
|
@@ -97,37 +39,13 @@ function removeFragment(id: string) {
|
|
|
97
39
|
|
|
98
40
|
<!-- best practices (prompt fragments) -->
|
|
99
41
|
<div>
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
variant="ghost"
|
|
108
|
-
color="neutral"
|
|
109
|
-
icon="i-lucide-plus"
|
|
110
|
-
trailing-icon="i-lucide-chevron-down"
|
|
111
|
-
/>
|
|
112
|
-
</UDropdownMenu>
|
|
113
|
-
</div>
|
|
114
|
-
<div v-if="selectedFragments.length" class="mb-1 flex flex-wrap gap-1">
|
|
115
|
-
<UBadge
|
|
116
|
-
v-for="f in selectedFragments"
|
|
117
|
-
:key="f.id"
|
|
118
|
-
color="primary"
|
|
119
|
-
variant="subtle"
|
|
120
|
-
size="sm"
|
|
121
|
-
class="cursor-pointer"
|
|
122
|
-
:title="f.summary"
|
|
123
|
-
@click="removeFragment(f.id)"
|
|
124
|
-
>
|
|
125
|
-
{{ f.title }}<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
|
|
126
|
-
</UBadge>
|
|
127
|
-
</div>
|
|
128
|
-
<div v-else class="text-[11px] text-slate-500">
|
|
129
|
-
{{ t('inspector.structure.bestPracticesEmpty') }}
|
|
130
|
-
</div>
|
|
42
|
+
<FragmentSelector
|
|
43
|
+
:model-value="block.fragmentIds ?? []"
|
|
44
|
+
:pool="fragmentPool"
|
|
45
|
+
:label="t('inspector.structure.bestPractices')"
|
|
46
|
+
:empty-text="t('inspector.structure.bestPracticesEmpty')"
|
|
47
|
+
@update:model-value="setFragments"
|
|
48
|
+
/>
|
|
131
49
|
<p class="mt-1 text-[11px] leading-snug text-slate-500">
|
|
132
50
|
{{ t('inspector.structure.bestPracticesHint') }}
|
|
133
51
|
</p>
|
package/app/stores/board.ts
CHANGED
|
@@ -241,7 +241,10 @@ export const useBoardStore = defineStore('board', () => {
|
|
|
241
241
|
...(options?.modelPresetId ? { modelPresetId: options.modelPresetId } : {}),
|
|
242
242
|
...(options?.pipelineId ? { pipelineId: options.pipelineId } : {}),
|
|
243
243
|
...(options?.agentConfig ? { agentConfig: options.agentConfig } : {}),
|
|
244
|
-
|
|
244
|
+
// Forward the selection when the caller provides one (the create form always does, even
|
|
245
|
+
// when empty — an explicit clear the backend must honour rather than re-seed); omit only
|
|
246
|
+
// when a caller doesn't manage fragments at all (then the backend seeds from the service).
|
|
247
|
+
...(options?.fragmentIds !== undefined ? { fragmentIds: options.fragmentIds } : {}),
|
|
245
248
|
...(options?.technical ? { technical: true } : {}),
|
|
246
249
|
})
|
|
247
250
|
upsert(block)
|
package/i18n/locales/de.json
CHANGED
|
@@ -2084,7 +2084,6 @@
|
|
|
2084
2084
|
"recurringNoFrame": "Eine wiederkehrende Aufgabe muss auf einem Service liegen. Fügen Sie sie aus einem Service-Frame (oder einem darin enthaltenen Modul) hinzu.",
|
|
2085
2085
|
"titleField": "Titel",
|
|
2086
2086
|
"titlePlaceholder": "Was muss getan werden?",
|
|
2087
|
-
"titlePlaceholderReview": "Optional – wird aus dem Pull Request abgeleitet",
|
|
2088
2087
|
"issueIncluded": "{title} (aus Issue, enthalten)",
|
|
2089
2088
|
"loadingIssue": "Beschreibung des verknüpften Issues wird geladen…",
|
|
2090
2089
|
"additionalNotes": "Zusätzliche Notizen",
|
package/i18n/locales/en.json
CHANGED
|
@@ -203,7 +203,6 @@
|
|
|
203
203
|
"recurringNoFrame": "A recurring task must live on a service. Add it from a service frame (or a module inside one).",
|
|
204
204
|
"titleField": "Title",
|
|
205
205
|
"titlePlaceholder": "What needs to be done?",
|
|
206
|
-
"titlePlaceholderReview": "Optional — derived from the pull request",
|
|
207
206
|
"issueIncluded": "{title} (from issue, included)",
|
|
208
207
|
"loadingIssue": "Loading the linked issue's description…",
|
|
209
208
|
"additionalNotes": "Additional notes",
|
package/i18n/locales/es.json
CHANGED
|
@@ -182,7 +182,6 @@
|
|
|
182
182
|
"recurringNoFrame": "Una tarea recurrente debe vivir en un servicio. Añádela desde un marco de servicio (o un módulo dentro de él).",
|
|
183
183
|
"titleField": "Título",
|
|
184
184
|
"titlePlaceholder": "¿Qué hay que hacer?",
|
|
185
|
-
"titlePlaceholderReview": "Opcional: se deriva de la pull request",
|
|
186
185
|
"issueIncluded": "{title} (de la incidencia, incluida)",
|
|
187
186
|
"loadingIssue": "Cargando la descripción de la incidencia vinculada…",
|
|
188
187
|
"additionalNotes": "Notas adicionales",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -182,7 +182,6 @@
|
|
|
182
182
|
"recurringNoFrame": "Une tâche récurrente doit appartenir à un service. Ajoutez-la depuis un cadre de service (ou un module à l’intérieur).",
|
|
183
183
|
"titleField": "Titre",
|
|
184
184
|
"titlePlaceholder": "Que faut-il faire ?",
|
|
185
|
-
"titlePlaceholderReview": "Facultatif — dérivé de la pull request",
|
|
186
185
|
"issueIncluded": "{title} (depuis le ticket, incluse)",
|
|
187
186
|
"loadingIssue": "Chargement de la description du ticket lié…",
|
|
188
187
|
"additionalNotes": "Notes supplémentaires",
|
package/i18n/locales/he.json
CHANGED
|
@@ -182,7 +182,6 @@
|
|
|
182
182
|
"recurringNoFrame": "משימה מחזורית חייבת להתקיים על שירות. הוסף אותה ממסגרת שירות (או ממודול בתוכה).",
|
|
183
183
|
"titleField": "כותרת",
|
|
184
184
|
"titlePlaceholder": "מה צריך לעשות?",
|
|
185
|
-
"titlePlaceholderReview": "אופציונלי — נגזר מבקשת המשיכה",
|
|
186
185
|
"issueIncluded": "{title} (מהאישיו, כלול)",
|
|
187
186
|
"loadingIssue": "טוען את תיאור האישיו המקושר…",
|
|
188
187
|
"additionalNotes": "הערות נוספות",
|
package/i18n/locales/it.json
CHANGED
|
@@ -2084,7 +2084,6 @@
|
|
|
2084
2084
|
"recurringNoFrame": "Un'attività ricorrente deve risiedere su un servizio. Aggiungila da un frame di servizio (o da un modulo al suo interno).",
|
|
2085
2085
|
"titleField": "Titolo",
|
|
2086
2086
|
"titlePlaceholder": "Cosa bisogna fare?",
|
|
2087
|
-
"titlePlaceholderReview": "Facoltativo — derivato dalla pull request",
|
|
2088
2087
|
"issueIncluded": "{title} (dall'issue, incluso)",
|
|
2089
2088
|
"loadingIssue": "Caricamento della descrizione dell'issue collegata…",
|
|
2090
2089
|
"additionalNotes": "Note aggiuntive",
|
package/i18n/locales/ja.json
CHANGED
|
@@ -182,7 +182,6 @@
|
|
|
182
182
|
"recurringNoFrame": "繰り返しタスクはサービス上に配置する必要があります。サービスフレーム(またはその中のモジュール)から追加してください。",
|
|
183
183
|
"titleField": "タイトル",
|
|
184
184
|
"titlePlaceholder": "何を実施する必要がありますか?",
|
|
185
|
-
"titlePlaceholderReview": "任意 — プルリクエストから生成されます",
|
|
186
185
|
"issueIncluded": "{title}(issue から、含む)",
|
|
187
186
|
"loadingIssue": "リンクされた issue の説明を読み込み中…",
|
|
188
187
|
"additionalNotes": "追加メモ",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -182,7 +182,6 @@
|
|
|
182
182
|
"recurringNoFrame": "Zadanie cykliczne musi należeć do usługi. Dodaj je z ramki usługi (lub modułu w jej obrębie).",
|
|
183
183
|
"titleField": "Tytuł",
|
|
184
184
|
"titlePlaceholder": "Co trzeba zrobić?",
|
|
185
|
-
"titlePlaceholderReview": "Opcjonalnie — tworzony na podstawie pull requesta",
|
|
186
185
|
"issueIncluded": "{title} (ze zgłoszenia, dołączone)",
|
|
187
186
|
"loadingIssue": "Wczytywanie opisu powiązanego zgłoszenia…",
|
|
188
187
|
"additionalNotes": "Dodatkowe uwagi",
|
package/i18n/locales/tr.json
CHANGED
|
@@ -182,7 +182,6 @@
|
|
|
182
182
|
"recurringNoFrame": "Yinelenen bir görev bir serviste bulunmalıdır. Bir servis çerçevesinden (veya içindeki bir modülden) ekleyin.",
|
|
183
183
|
"titleField": "Başlık",
|
|
184
184
|
"titlePlaceholder": "Ne yapılması gerekiyor?",
|
|
185
|
-
"titlePlaceholderReview": "İsteğe bağlı — pull request'ten türetilir",
|
|
186
185
|
"issueIncluded": "{title} (sorundan, dahil edildi)",
|
|
187
186
|
"loadingIssue": "Bağlantılı sorunun açıklaması yükleniyor…",
|
|
188
187
|
"additionalNotes": "Ek notlar",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -182,7 +182,6 @@
|
|
|
182
182
|
"recurringNoFrame": "Періодичне завдання має належати сервісу. Додайте його з рамки сервісу (або модуля всередині неї).",
|
|
183
183
|
"titleField": "Назва",
|
|
184
184
|
"titlePlaceholder": "Що потрібно зробити?",
|
|
185
|
-
"titlePlaceholderReview": "Необов’язково — формується з pull request",
|
|
186
185
|
"issueIncluded": "{title} (зі звернення, включено)",
|
|
187
186
|
"loadingIssue": "Завантаження опису пов’язаного звернення…",
|
|
188
187
|
"additionalNotes": "Додаткові примітки",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.144.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"valibot": "^1.4.2",
|
|
41
41
|
"vue": "3.5.40",
|
|
42
42
|
"wretch": "^3.0.9",
|
|
43
|
-
"@cat-factory/contracts": "0.152.
|
|
43
|
+
"@cat-factory/contracts": "0.152.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@toad-contracts/testing": "0.3.2",
|