first commit for create issue Template UI#4
Conversation
📝 WalkthroughWalkthroughAdds comprehensive issue-template management: a multi-step Create Issue Template dialog, templates listing and detail routes/views, new templated-field editor and card components, SVG/icon catalog and wrapper, GraphQL schema+client additions for templates/issues, event-bus typings for create-* events, and minor sidebar/style tweaks. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant App as Root.vue
participant EB as EventBus
participant Dialog as CreateIssueTemplateDialog.vue
participant API as GraphQL
participant List as IssueTemplates.vue
participant Router as VueRouter
User->>App: Click "Create Issue Template"
App->>EB: emit("create-issue-template")
EB-->>Dialog: open dialog (reset state)
User->>Dialog: fill steps, select icons, configure fields
Dialog->>API: mutation createIssueTemplate
API-->>Dialog: return IssueTemplate
Dialog->>List: emit created / request refresh
Dialog->>Router: navigate to template details route
List-->>App: update list view
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (19)
src/components/SideBarButton.vue (3)
3-3: Emit the declared click event (and keep navigation).Currently defineEmits is unused; click isn’t emitted. Emit it and preserve router navigation.
- <button class="sidebar-button" :class="{ active: isActive }" @click="navigate"> + <button + type="button" + class="sidebar-button" + :class="{ active: isActive }" + @click="(e) => { emit('click', e); navigate(e); }" + >-defineEmits(["click"]); +const emit = defineEmits<{ + (e: "click", ev: MouseEvent): void; +}>();Also applies to: 51-51
4-4: Avoid double centering (utility classes vs. CSS).You’re centering via classes and scoped CSS; keep one to reduce redundancy.
Option A (keep utilities): remove CSS rules
.icon-container { - display: flex; - align-items: center; - justify-content: center; overflow: hidden;Option B (keep CSS): drop the utility classes on the container
- <div class="icon-container d-flex align-center justify-center"> + <div class="icon-container">Also applies to: 71-74
3-3: Use type="button" to prevent unintended form submits.Small safety for usage inside forms.
- <button class="sidebar-button" :class="{ active: isActive }" @click="navigate"> + <button type="button" class="sidebar-button" :class="{ active: isActive }" @click="navigate">src/components/SideBar.vue (2)
7-7: Prefer stable keys over index to avoid unnecessary re-renders.- <div v-for="(item, index) in itemGroup" :key="index" class="sidebar-item mb-2 d-flex align-center justify-center"> + <div + v-for="(item, index) in itemGroup" + :key="('name' in item) ? `icon-${item.name}` : `fab-${item.icon}`" + class="sidebar-item mb-2 d-flex align-center justify-center" + >
17-17: Guard optional color to avoid “undefined-container”.When item.color is missing, default to a theme color.
- :color="`${item.color}-container`" + :color="`${(item.color ?? 'secondary')}-container`"src/assets/icons.ts (1)
2-2: Add typing for iconList for safer consumption.-export const iconList = [ +export interface IconDef { name: string; iconPath: string } +export const iconList: ReadonlyArray<IconDef> = [src/components/SvgIcon.vue (1)
3-13: Optional a11y: accept aria-label and role when needed.If used non-decoratively, allow overriding aria-hidden and role.
- <svg + <svg xmlns="http://www.w3.org/2000/svg" :viewBox="viewBox" fill="currentColor" class="svg-icon" :width="size" :height="size" - aria-hidden="true" + aria-hidden="true" + role="img" >src/components/ExpandableCard.vue (2)
58-61: Trim inputs before emitting confirm.Prevents accidental leading/trailing spaces in stored values.
- @click="emit('confirm', { name: localName, description: localDescription })" + @click="emit('confirm', { name: localName.trim(), description: localDescription.trim() })"
99-108: Keep local fields in sync when props change while expanded.watch( () => props.expandedCardKey, () => { // reset fields when expanded if (isExpanded.value) { localName.value = props.name; localDescription.value = props.description; } } ); + +watch( + () => [props.name, props.description], + () => { + if (isExpanded.value) { + localName.value = props.name; + localDescription.value = props.description; + } + } +);src/router/index.ts (2)
36-67: Avoid repeating component logic; drive variants via meta.All children render the same component. Add a meta templateType (issue|artefact|component|interface-specification|relation) so the view can switch behavior without route-name switches.
- { - path: "component", - name: "templates-component", - component: () => import("../views/home/Templates.vue"), - }, + { + path: "component", + name: "templates-component", + component: () => import("../views/home/Templates.vue"), + meta: { templateType: "component" as const }, + },(Apply similarly to the other children.)
36-39: Drop parent route name to prevent accidental navigation.The parent “templates” named route is unused and can be confusing when navigating by name. Remove name or add a redirect to the default child.
- { - path: "templates", - name: "templates", - component: () => import("../views/RouterOnly.vue"), + { + path: "templates", + // name: "templates", + component: () => import("../views/RouterOnly.vue"), children: [src/views/home/Templates.vue (1)
23-25: Remove non-English TODO comment from template.Avoid shipping internal notes in UI-facing markup.
- <!-- muss definitiv noch überabeitet werden und an die template Typen angepasst werde. das ne theo sesh -->src/components/dialog/CreateTemplateDialog.vue (3)
67-69: Emit payload type/name mismatch.Rename emitted argument to template for clarity.
-const emit = defineEmits<{ - (event: "created-template", project: IdObject): void; -}>(); +const emit = defineEmits<{ + (event: "created-template", template: IdObject): void; +}>();
71-78: Tighten validation.Constrain templateType to allowed values and validate repositoryURL as URL.
- yup.object().shape({ + yup.object().shape({ name: yup.string().required().label("Name"), - templateType: yup.string().required().label("Template type"), + templateType: yup + .mixed<"Artefact"|"Component"|"InterfaceSpecification"|"Issue"|"Relation">() + .oneOf(["Artefact","Component","InterfaceSpecification","Issue","Relation"]) + .required() + .label("Template type"), description: yup.string().notRequired().label("Description"), - repositoryURL: yup.string().notRequired().label("Repository URL") + repositoryURL: yup.string().url().notRequired().label("Repository URL") })
25-35: Avoid empty color prop.Remove color="" to inherit default text color.
-<DefaultButton variant="text" color="" @click="!isDirty && cancelCreateTemplate()"> +<DefaultButton variant="text" @click="!isDirty && cancelCreateTemplate()">src/components/dialog/CreateIssueTemplateDialog.vue (4)
255-257: Typo: “Telation”.Fix label/comment typo.
- <!-- Step 3: Assignment & Telation Types --> + <!-- Step 3: Assignment & Relation Types -->
632-633: Remove debug logging.Leftover console.log in production code.
- console.log(issuePriorities.value)
132-134: Translate/remove German warning in UI.Keep user-facing text in English and actionable.
- ⚠️ Path möglicherweise außerhalb von 100x100 ViewBox + ⚠️ Path may exceed the 100x100 viewBox; ensure it fits.
767-771: Unused field definitions.issueTypeName fields are unused; remove to reduce noise.
-const [issueTypeName, issueTypeNameProps ] = defineField('issueTypeName', fieldConfig);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (10)
src/assets/icons.ts(1 hunks)src/components/ExpandableCard.vue(1 hunks)src/components/SideBar.vue(1 hunks)src/components/SideBarButton.vue(1 hunks)src/components/SvgIcon.vue(1 hunks)src/components/dialog/CreateIssueTemplateDialog.vue(1 hunks)src/components/dialog/CreateTemplateDialog.vue(1 hunks)src/router/index.ts(1 hunks)src/views/home/Root.vue(3 hunks)src/views/home/Templates.vue(1 hunks)
🔇 Additional comments (7)
src/components/ExpandableCard.vue (1)
21-26: Confirm IconButton availability (global or import).IconButton isn’t imported locally; ensure it’s globally registered to avoid runtime errors.
src/views/home/Root.vue (3)
33-35: Tabs addition LGTM.Templates tab placement and path look consistent with routes.
69-98: Right-bar actions likely unhandled for non-issue types.Only CreateIssueTemplateDialog is mounted in Templates.vue, and CreateTemplateDialog listens to create-issue-template. The four other events (artefact/component/interface-specification/relation) won’t open anything.
Would you like me to wire CreateTemplateDialog to these events and mount it in Templates.vue?
138-175: Verify custom icon tokens exist.Confirm "$issue", "$component", "$interface" are registered; otherwise Vuetify will render blanks. If not, point them to SvgIcon or mdi equivalents.
src/views/home/Templates.vue (2)
41-45: Confirm sort field typing.Template maps to an array of fields; ensure PaginatedList accepts Field | Field[]; otherwise split into separate named sorts.
71-82: Route helper still navigates to component detail.If this page will manage templates, consider routing to a template detail instead (or annotate as intentional placeholder).
src/components/dialog/CreateIssueTemplateDialog.vue (1)
366-367: Combobox model is array of objects; define item-title/value or map to strings.Otherwise chips will display “[object Object]”.
-<v-combobox v-model="issueStates" label="Issue States" multiple chips clearable class="mb-2" /> +<v-combobox + v-model="issueStates" + :items="issueStates" + item-title="name" + item-value="name" + label="Issue States" multiple chips clearable class="mb-2" />(Or maintain a separate string[] model.)
nk-coding
left a comment
There was a problem hiding this comment.
pls also run the formatter (npm run format)
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (6)
src/components/dialog/CreateIssueTemplateDialog.vue (4)
575-582: Validate SVG path before adding to iconList.The
confirmAddIconfunction accepts arbitrary SVG path data without validation, which could lead to malformed or malicious SVG being rendered.Apply this diff to add validation:
function confirmAddIcon() { - if (newIcon.value.name && newIcon.value.iconPath) { + const allowed = /^[MmLlHhVvCcSsQqTtAaZz0-9 ,.\-]*$/; + const cleanedPath = newIcon.value.iconPath.trim().replace(/"/g, ""); + if (newIcon.value.name && cleanedPath && allowed.test(cleanedPath)) { - iconList.value.push({ name: newIcon.value.name, iconPath: newIcon.value.iconPath.trim().replace(/\"/g, "") }); - selectedIcon.value = { name: newIcon.value.name, iconPath: newIcon.value.iconPath.trim().replace(/\"/g, "") }; + iconList.value.push({ name: newIcon.value.name, iconPath: cleanedPath }); + selectedIcon.value = { name: newIcon.value.name, iconPath: cleanedPath }; newIcon.value = { name: "", iconPath: "" }; activeTab.value = "select"; } }
503-506: expandedCardKey type is incomplete.The type union only allows
"type" | "priority" | "state"but the code sets it to"assignment"(line 329) and"relation"(line 379) elsewhere.Apply this diff:
const expandedCardKey = ref<{ nameID: string; - type: "type" | "priority" | "state"; + type: "type" | "priority" | "state" | "assignment" | "relation"; } | null>(null);
719-731: Duplicate-name check uses wrong collection.The duplicate check compares
newNameagainstissueStatesinstead ofassignmentTypes.Apply this diff:
if (previousName.trim().toLowerCase() !== newName.trim().toLowerCase()) { - if (issueStates.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { + if (assignmentTypes.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { nameErrorMessage.value = "Name already exists"; return; } else { nameErrorMessage.value = ""; } }
746-758: Duplicate-name check uses wrong collection.The duplicate check compares
newNameagainstissueStatesinstead ofrelationTypes.Apply this diff:
if (previousName.trim().toLowerCase() !== newName.trim().toLowerCase()) { - if (issueStates.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { + if (relationTypes.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { nameErrorMessage.value = "Name already exists"; return; } else { nameErrorMessage.value = ""; } }src/views/home/Templates.vue (2)
23-24: Broken event handler: missing selectTemplate().The dialog emits
created-templatebut the handler calls an undefinedselectTemplate. Use the existingselectComponentor implementselectTemplate.Apply this diff to use the existing handler:
- <CreateIssueTemplateDialog @created-template="(template: IdObject) => selectTemplate(template)" /> + <CreateIssueTemplateDialog @created-template="(template: IdObject) => selectComponent(template)" />
53-66: Filtering and pagination are incorrect.The current logic treats an empty string as a valid filter and skips pagination for filtered results. Trim the filter to decide the path, and pass
skipfor filtered queries.Apply this diff to fix the filtering logic:
- if (filter == undefined) { + const q = (filter ?? "").trim(); + if (!q) { const res = await client.getComponentList({ orderBy, count, skip: page * count }); return [res.components.nodes, res.components.totalCount]; } else { const res = await client.getFilteredComponentList({ - query: filter, - count + query: q, + count, + skip: page * count, + orderBy }); - return [res.searchComponents, res.searchComponents.length]; + return [res.searchComponents.nodes ?? res.searchComponents, res.searchComponents.totalCount ?? res.searchComponents.length]; }(Adjust field access to match your GraphQL schema.)
🧹 Nitpick comments (1)
src/components/SvgWrapper.vue (1)
12-12: Validate SVG path data to prevent malformed or malicious paths.The current normalization only trims whitespace and removes quotes, which doesn't protect against malformed or potentially malicious SVG path commands. Consider adding a lightweight allowlist regex to validate that the path contains only valid SVG path syntax.
Apply this diff to add validation:
-const normalizedPath = computed(() => props.path.trim().replace(/"/g, "")); +const normalizedPath = computed(() => { + const cleaned = props.path.trim().replace(/"/g, ""); + const allowed = /^[MmLlHhVvCcSsQqTtAaZz0-9 ,.\-]*$/; + return allowed.test(cleaned) ? cleaned : ""; +});
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
src/components/ExpandableCard.vue(1 hunks)src/components/SideBar.vue(1 hunks)src/components/SideBarButton.vue(1 hunks)src/components/SvgWrapper.vue(1 hunks)src/components/dialog/CreateIssueTemplateDialog.vue(1 hunks)src/components/icons.ts(1 hunks)src/router/index.ts(1 hunks)src/views/home/Root.vue(3 hunks)src/views/home/Templates.vue(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/components/SideBarButton.vue
- src/components/SideBar.vue
- src/router/index.ts
🔇 Additional comments (3)
src/views/home/Root.vue (1)
33-34: LGTM!The Templates navigation integration follows the existing pattern for projects, components, and IMSs. The permission checks, event bus wiring, and sidebar items are consistent with the established codebase conventions.
Also applies to: 69-98, 138-173
src/components/icons.ts (1)
118-278: LGTM! Icon catalog follows best practices.The use of
@mdi/jsfor Material Design icons is the recommended approach for tree-shakeable SVG paths, as noted in the learnings. The custom Gropius icons are included with inline SVG path data, which provides a centralized icon asset catalog for the UI.Based on learnings
src/components/ExpandableCard.vue (1)
1-97: LGTM!The ExpandableCard component is well-designed with proper state management. The computed
isExpandedprevents multiple cards from expanding simultaneously, and the watcher correctly resets local fields when a card is expanded. The component follows Vue 3 composition API best practices.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
src/components/dialog/CreateIssueTemplateDialog.vue (3)
538-541: Expanded-card type union misses valid valuesLine 335 and below assign
"assignment"/"relation"toexpandedCardKey.type, but the union on Lines 538-541 only allows"type" | "priority" | "state", so TypeScript rejects those writes. Extend the union to cover every runtime value.const expandedCardKey = ref<{ nameID: string; - type: "type" | "priority" | "state"; + type: "type" | "priority" | "state" | "assignment" | "relation"; } | null>(null);
760-764: Assignment duplicate check hits the wrong collectionLine 760 still checks
issueStatesfor name collisions, so duplicate assignment types slip through. Compare againstassignmentTypesinstead before pushing.- if (issueStates.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { + if (assignmentTypes.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) {
787-791: Relation duplicate guard points to issue statesSame problem here: Lines 787-791 consult
issueStates, so duplicate relation type names aren’t caught. Guard againstrelationTypesinstead.- if (issueStates.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { + if (relationTypes.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/components/dialog/CreateIssueTemplateDialog.vue(1 hunks)src/components/icons.ts(1 hunks)src/router/index.ts(2 hunks)src/views/home/Root.vue(3 hunks)src/views/home/Templates/IssueTemplates.vue(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/router/index.ts
- src/views/home/Root.vue
nk-coding
left a comment
There was a problem hiding this comment.
@LLemonLauncher there are still a lot of AI comments left, pls check them and resolve the ones which you say don't make sense (ideally with a comment) and fix the others
also there is 1 old comment from me remaining
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
package.json (1)
62-62: Likely invalid semver for @types/react."^19.1.01" will not resolve as intended. Use "^19.1.1".
Apply this diff:
- "@types/react": "^19.1.01", + "@types/react": "^19.1.1",
🧹 Nitpick comments (5)
package.json (2)
38-38: Duplicate Prettier entry (deps vs devDeps).Prettier should be a devDependency only. Remove it from dependencies to avoid bloating prod installs.
Apply this diff:
"dependencies": { ... - "prettier": "^3.6.2", "react": "^19.1.1", ... }, "devDependencies": { ... "prettier": "^3.6.2",Also applies to: 67-67
19-21: Remove @mdi/font dependency
No MDI font CSS classes are referenced—icons use inline SVG via @mdi/js—so@mdi/fontcan be dropped from package.json to slim the bundle.src/components/icons.ts (3)
128-313: Add lightweight types, readonly, and a name→path map for O(1) lookups.Helps DX and prevents accidental mutation without changing current usage.
Apply this diff near the iconList declaration:
-export const iconList = [ +export interface IconSpec { name: string; iconPath: string } +export const iconList: ReadonlyArray<IconSpec> = [ { /* ...existing items... */ } -]; +]; + +export type IconName = (typeof iconList)[number]['name']; +export const iconMap: ReadonlyMap<IconName, string> = + new Map(iconList.map(i => [i.name as IconName, i.iconPath])); +export const getIconPath = (name: IconName) => iconMap.get(name);
193-313: Normalize naming style for icon “name”.Names mix PascalCase, kebab-case, and words with hyphens (e.g., "Chart-bar", "Component-Version", "GropiusBug"). Pick one convention (suggest kebab-case ids) and, if needed, add a separate human‑readable label.
Example approach:
type IconId = 'gropius-bug' | 'feature' | 'task' | ...; interface IconSpec { id: IconId; label: string; iconPath: string }If changing would be noisy now, add a TODO and follow up.
130-153: Optional: extract very long custom SVG paths into named constants.Improves readability and reduces diff churn in future edits.
Example:
const PATH_GROPIUS_BUG = "M12 3.75C14.071 ... 12 5.25Z"; const PATH_ISSUE = "M 12 3.255 C 14.1954 ... 12 4.845 Z"; export const iconList = [ { name: "GropiusBug", iconPath: PATH_GROPIUS_BUG }, { name: "Issue", iconPath: PATH_ISSUE }, // ... ];
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (4)
package.json(1 hunks)src/components/ExpandableCard.vue(1 hunks)src/components/dialog/CreateIssueTemplateDialog.vue(1 hunks)src/components/icons.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/components/dialog/CreateIssueTemplateDialog.vue
- src/components/ExpandableCard.vue
🔇 Additional comments (2)
package.json (1)
20-20: Add of @mdi/js looks good.Required for the new icon catalog in src/components/icons.ts. No concerns.
src/components/icons.ts (1)
1-126: Import block is fine; tree‑shaken named imports.Given Vite + ESM, only referenced icon constants should be included. No action required.
If bundle size matters, consider lazy-loading large icon sets per route. Verify via bundle analyzer before acting.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/components/icons.ts (2)
1-125: Consider type annotation for better IntelliSense.The exported
iconListlacks an explicit type, which limits editor autocompletion and type safety. Add a type annotation or useas constto enable better type inference.Apply this diff to add type safety:
+type IconEntry = { + readonly name: string; + readonly iconPath: string; +}; + -export const iconList = [ +export const iconList: readonly IconEntry[] = [Alternatively, use
as constfor literal type inference:-export const iconList = [ +export const iconList = [ // ... entries -]; +] as const;
128-189: Extract lengthy SVG paths to improve readability.The custom icon paths (especially
Gropius-Bug,Interface-Specification,Interface-Specification-Version, andInterface-Part) contain very long inline SVG path strings that reduce code readability and make maintenance difficult.Consider extracting these to named constants:
const GROPIUS_BUG_PATH = "M12 3.75C14.071 3.75..." // existing path export const iconList = [ { name: "Gropius-Bug", iconPath: GROPIUS_BUG_PATH }, // ... ];Or move complex custom icons to a separate file (e.g.,
customIcons.ts) and import them.src/components/dialog/CreateIssueTemplateDialog.vue (1)
609-624: Minor inconsistency in path validation.The validation regex (line 610) allows double quotes
"but then strips them duringconfirmAddIcon(lines 619-620). Consider removing quotes from the allowed pattern if they're not needed in the final path.-const originalAllowedPathElements = (path: string) => /^[MmLlHhVvCcSsQqTtAaZz0-9 ,."\-]+$/g.test(path); +const originalAllowedPathElements = (path: string) => /^[MmLlHhVvCcSsQqTtAaZz0-9 ,.\-]+$/g.test(path);Or accept quotes but don't strip them if validation already passed.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/components/dialog/CreateIssueTemplateDialog.vue(1 hunks)src/components/icons.ts(1 hunks)src/router/index.ts(2 hunks)src/views/home/Root.vue(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/views/home/Root.vue
- src/router/index.ts
🔇 Additional comments (9)
src/components/icons.ts (1)
190-310: LGTM! Well-structured icon catalog.The standard icon mappings are clean, well-organized, and use consistent naming conventions. Leveraging @mdi/js is a solid choice for comprehensive icon coverage.
src/components/dialog/CreateIssueTemplateDialog.vue (8)
1-36: LGTM! Clean form structure with good validation feedback.The general information step properly uses vee-validate bindings and provides helpful inline warnings for duplicate template names.
97-193: Well-implemented icon management with proper security measures.The icon picker includes search, lazy loading for performance, and proper SVG path validation (line 610). The use of
SvgWrapperinstead ofv-htmladdresses the previous XSS concerns.
539-564: LGTM! Proper form validation setup.The vee-validate integration with yup schema is correctly implemented, and the async duplicate check on template name provides good UX feedback.
626-663: LGTM! Sensible defaults for issue template entities.The pre-populated defaults for issue types, priorities, states, and relation types provide a good starting point for users.
665-689: LGTM! Correct implementation with proper validation.The function properly validates names, checks for duplicates, and maintains sorted order.
695-727: LGTM! Proper numeric validation for priority values.The function correctly validates both the name and numeric value fields with appropriate error messages.
823-833: LGTM! Proper cleanup on cancel.The function correctly resets all error states and removes any uncommitted empty entries.
841-854: Placeholder submission needs implementation.Line 851 has a
console.logplaceholder instead of actual GraphQL mutation to create the template. Ensure this is implemented before the feature is complete.Do you want me to help generate the GraphQL mutation code for submitting the template, or is this tracked in a separate issue?
| @click=" | ||
| () => { | ||
| createIssueTypeInput('', '', '', ''); | ||
| expandedCardKey = { nameID: '', type: 'type' }; | ||
| } | ||
| " | ||
| >+ Add Issue Type | ||
| </v-btn> | ||
| <ExpandableCard | ||
| v-for="IssueTypeInput in IssueTypes" | ||
| :key="IssueTypeInput.name" | ||
| :name="IssueTypeInput.name" | ||
| :description="IssueTypeInput.description" | ||
| :expandedCardKey="expandedCardKey" | ||
| type="type" | ||
| :nameErrorMessage="nameErrorMessage" | ||
| @expand=" | ||
| () => { | ||
| expandedCardKey = { nameID: IssueTypeInput.name, type: 'type' }; | ||
| selectedIcon = | ||
| iconList.find((icon) => icon.iconPath === IssueTypeInput.iconPath) ?? | ||
| null; | ||
| currentEditedName = IssueTypeInput.name; | ||
| currentEditedDescription = IssueTypeInput.description; | ||
| nameErrorMessage = ''; | ||
| } | ||
| " | ||
| @cancel="cancelCreateCard()" | ||
| @delete="deleteIssueTypeInputByName(IssueTypeInput.name)" | ||
| @confirm=" | ||
| ({ name, description }) => { | ||
| if (!name) { | ||
| nameErrorMessage = 'Name is required'; | ||
| return; | ||
| } | ||
| createIssueTypeInput( | ||
| IssueTypeInput.name, | ||
| name, | ||
| description, | ||
| selectedIcon?.iconPath ?? '' | ||
| ); | ||
| } | ||
| " |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Extract inline handlers to named functions.
The inline event handlers contain significant logic (10+ lines) that reduces template readability and makes testing difficult. This pattern repeats throughout the component.
Extract to named functions in the script section:
function handleAddIssueType() {
createIssueTypeInput('', '', '', '');
expandedCardKey.value = { nameID: '', type: 'type' };
}
function handleExpandIssueType(issueType: IssueTypeInput) {
expandedCardKey.value = { nameID: issueType.name, type: 'type' };
selectedIcon.value = iconList.value.find((icon) => icon.iconPath === issueType.iconPath) ?? null;
currentEditedName.value = issueType.name;
currentEditedDescription.value = issueType.description;
nameErrorMessage.value = '';
}
function handleConfirmIssueType(issueType: IssueTypeInput, { name, description }: { name: string; description: string }) {
if (!name) {
nameErrorMessage.value = 'Name is required';
return;
}
createIssueTypeInput(issueType.name, name, description, selectedIcon.value?.iconPath ?? '');
}Then use in template:
@click="handleAddIssueType"
@expand="handleExpandIssueType(IssueTypeInput)"
@confirm="(data) => handleConfirmIssueType(IssueTypeInput, data)"Apply the same pattern to priorities, states, assignments, and relations.
🤖 Prompt for AI Agents
In src/components/dialog/CreateIssueTemplateDialog.vue around lines 47 to 89,
the template contains large inline event handlers for add/expand/confirm that
reduce readability and testability; extract those handlers into named functions
in the script section (e.g., handleAddIssueType, handleExpandIssueType,
handleConfirmIssueType) that call the existing helpers (createIssueTypeInput,
deleteIssueTypeInputByName, etc.), update reactive refs (expandedCardKey,
selectedIcon, currentEditedName, currentEditedDescription, nameErrorMessage)
appropriately, and return early on validation errors; then replace the inline
handlers in the template with these named functions (and a small arrow wrapper
for passing arguments to confirm if needed). Apply the same extraction pattern
for priorities, states, assignments, and relations to keep templates concise and
consistent.
| function createRelationType(previousName: string, newName: string, inverseName: string, description: string) { | ||
| if (newName.trim().length === 0 && previousName.trim().length !== 0) { | ||
| nameErrorMessage.value = "Name is required"; | ||
| return; | ||
| } | ||
| if (previousName.trim().toLowerCase() !== newName.trim().toLowerCase()) { | ||
| if (issueStates.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { | ||
| nameErrorMessage.value = "Name already exists"; | ||
| return; | ||
| } else { | ||
| nameErrorMessage.value = ""; | ||
| } | ||
| } | ||
|
|
||
| if (inverseName.trim().length === 0 && previousName.trim().length !== 0) { | ||
| inverseNameErrorMessage.value = "Inverse Name is required"; | ||
| return; | ||
| } else { | ||
| inverseNameErrorMessage.value = ""; | ||
| } | ||
|
|
||
| deleteRelationTypeByName(previousName); | ||
| relationTypes.value.push({ name: newName, inverseName, description }); | ||
|
|
||
| relationTypes.value.sort((a, b) => a.name.localeCompare(b.name)); | ||
| expandedCardKey.value = null; | ||
| currentEditedName.value = ""; | ||
| currentEditedDescription.value = ""; | ||
| currentEditedInverseName.value = ""; | ||
| } |
There was a problem hiding this comment.
Critical: Duplicate check uses wrong collection.
Line 794 checks issueStates.value for duplicates but should check relationTypes.value. This allows duplicate relation type names to be created.
Apply this fix:
if (previousName.trim().toLowerCase() !== newName.trim().toLowerCase()) {
- if (issueStates.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) {
+ if (relationTypes.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) {
nameErrorMessage.value = "Name already exists";
return;
} else {🤖 Prompt for AI Agents
In src/components/dialog/CreateIssueTemplateDialog.vue around lines 788 to 817,
the duplicate-name check incorrectly inspects issueStates.value instead of
relationTypes.value; update the duplicate validation so it checks
relationTypes.value for any item whose name (trimmed, case-insensitive) matches
newName.trim().toLowerCase(), set nameErrorMessage.value to "Name already
exists" and return when found, otherwise clear the error; keep the rest of the
flow (deleteRelationTypeByName, push new relation, sort, reset UI state)
unchanged.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
src/components/dialog/CreateIssueTemplateDialog.vue (2)
790-819: Critical: Duplicate-name check uses wrong collection.Line 796 checks
issueStates.valuefor duplicate relation type names but should checkrelationTypes.value. This bug allows duplicate relation type names to slip through validation.Apply this fix:
if (previousName.trim().toLowerCase() !== newName.trim().toLowerCase()) { - if (issueStates.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { + if (relationTypes.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { nameErrorMessage.value = "Name already exists"; return; } else {
763-784: Critical: Duplicate-name check uses wrong collection.Line 769 checks
issueStates.valuefor duplicate assignment type names but should checkassignmentTypes.value. This bug allows duplicate assignment type names to slip through validation.Apply this fix:
if (previousName.trim().toLowerCase() !== newName.trim().toLowerCase()) { - if (issueStates.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { + if (assignmentTypes.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { nameErrorMessage.value = "Name already exists"; return; } else {
🧹 Nitpick comments (2)
src/components/dialog/CreateIssueTemplateDialog.vue (1)
47-89: Extract inline handlers to named functions.The inline event handler contains ~40 lines of logic spread across multiple callbacks. Extract these to named functions in the script section for better readability and testability. Based on learnings
Extract handlers like:
function handleAddIssueType() { createIssueTypeInput('', '', '', ''); expandedCardKey.value = { nameID: '', type: 'type' }; } function handleExpandIssueType(issueType: IssueTypeInput) { expandedCardKey.value = { nameID: issueType.name, type: 'type' }; selectedIcon.value = iconList.value.find((icon) => icon.iconPath === issueType.iconPath) ?? null; currentEditedName.value = issueType.name; currentEditedDescription.value = issueType.description; nameErrorMessage.value = ''; } function handleConfirmIssueType(issueType: IssueTypeInput, data: { name: string; description: string }) { if (!data.name) { nameErrorMessage.value = 'Name is required'; return; } createIssueTypeInput(issueType.name, data.name, data.description, selectedIcon.value?.iconPath ?? ''); }Then use in template:
@click="handleAddIssueType" @expand="handleExpandIssueType(IssueTypeInput)" @confirm="(data) => handleConfirmIssueType(IssueTypeInput, data)"Apply the same pattern to priorities (lines 198-243), states (lines 276-316), assignments (lines 342-382), and relations (lines 392-442).
src/views/home/Root.vue (1)
69-98: Consider extracting repeated template sidebar logic.Lines 69-98 contain five nearly identical case blocks that differ only in template type name. Consider extracting a helper to reduce duplication:
const templateTypes = { 'templates-issue': 'issue template', 'templates-artefact': 'artefact template', 'templates-component': 'component template', 'templates-interface-specification': 'interface specification template', 'templates-relation': 'relation template' } as const; // Then in the switch: if (route.name && route.name.toString().startsWith('templates-')) { const typeName = templateTypes[route.name as keyof typeof templateTypes]; if (typeName) { name = typeName; eventName = `create-${route.name.toString().substring(10)}-template` as keyof Events; disabled = !(store.user?.canCreateTemplates ?? false); break; } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/components/dialog/CreateIssueTemplateDialog.vue(1 hunks)src/router/index.ts(2 hunks)src/views/home/Root.vue(3 hunks)
🔇 Additional comments (2)
src/views/home/Root.vue (1)
144-179: LGTM: Template sidebar navigation.The left sidebar items for templates are well-structured with appropriate icons and route targets. The conditional rendering based on route name is clean and consistent.
src/router/index.ts (1)
385-395: Unable to fully verify due to sandbox environment limitations.The initial script output confirmed that:
- No template-specific components exist in the codebase
component/Home.vuerenders generic component details (displayingcomponent.name,component.description,component.pinnedIssues,component.issues)However, subsequent verification attempts failed to access the router file, preventing me from determining:
- Whether this route path is intentionally named "/templates" while displaying components
- Whether this is part of a broader routing pattern where "templates" is a feature alias for components
- The full context and purpose of this route within the application
The naming inconsistency between the route path ("/templates") and the component being loaded (component detail view) warrants manual verification to confirm whether this is intended behavior, a copy-paste error, or a placeholder.
| const allowedPathElements = ref<boolean>(false); | ||
| const originalAllowedPathElements = (path: string) => /^[MmLlHhVvCcSsQqTtAaZz0-9 ,."\-]+$/g.test(path); | ||
|
|
||
| watch( | ||
| () => newIcon.value.iconPath, | ||
| (newPath) => (allowedPathElements.value = originalAllowedPathElements(newPath)) | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
Verify inclusion of quotes in SVG path regex.
Line 606 includes " (double quotes) in the allowed SVG path characters. SVG path data should not contain literal quote characters. Verify whether this is intentional or if it should be removed.
🏁 Script executed:
#!/bin/bash
# Check if any SVG paths in the icon list contain quote characters
echo "Checking icon list for quotes in path data..."
cat src/components/icons.ts | grep -E 'iconPath.*".*"' | head -20Length of output: 45
I'll verify the SVG path specification and examine the actual icon data more thoroughly.
Let me examine the actual icon data and context more thoroughly:
Remove double quotes from SVG path validation regex.
SVG path data follows an EBNF grammar that specifies valid characters as command letters, numbers, and whitespace/comma separators. Coordinates can be separated by whitespace, commas, or both. Double quotes are not defined as valid path data characters in the SVG specification. The regex on line 606 includes " in the character class, which should be removed to accurately validate against the SVG path specification.
const originalAllowedPathElements = (path: string) => /^[MmLlHhVvCcSsQqTtAaZz0-9 ,.\-]+$/g.test(path);
🤖 Prompt for AI Agents
In src/components/dialog/CreateIssueTemplateDialog.vue around lines 605 to 611,
the SVG path validation regex incorrectly allows double quotes; update the
character class to remove the double-quote character so it only permits SVG path
command letters, digits, whitespace/comma, dot and hyphen, and also remove the
unnecessary global flag (/g) when using RegExp.test to avoid lastIndex
side-effects; replace the regex with a pattern that reflects those allowed chars
and use it in originalAllowedPathElements.
| function confirmAddIcon() { | ||
| if (newIcon.value.name && newIcon.value.iconPath && allowedPathElements) { | ||
| const newEntry = { | ||
| name: newIcon.value.name, | ||
| iconPath: newIcon.value.iconPath.trim().replace(/\"/g, "") | ||
| }; | ||
|
|
||
| iconList.value.unshift(newEntry); | ||
| selectedIcon.value = newEntry; | ||
| iconSearch.value = ""; | ||
| newIcon.value = { name: "", iconPath: "" }; | ||
| activeTab.value = "select"; | ||
| } | ||
| } |
There was a problem hiding this comment.
Fix ref access in conditional.
Line 614 references allowedPathElements without .value, but it's a ref. This will always evaluate to truthy (the ref object itself), not the boolean value inside.
Apply this fix:
function confirmAddIcon() {
- if (newIcon.value.name && newIcon.value.iconPath && allowedPathElements) {
+ if (newIcon.value.name && newIcon.value.iconPath && allowedPathElements.value) {
const newEntry = {
name: newIcon.value.name,
iconPath: newIcon.value.iconPath.trim().replace(/\"/g, "")🤖 Prompt for AI Agents
In src/components/dialog/CreateIssueTemplateDialog.vue around lines 613 to 626,
the conditional currently checks the ref allowedPathElements directly (which is
always truthy); change the condition to read allowedPathElements.value so the
inner boolean is evaluated. Update the if statement to use
allowedPathElements.value (and keep the existing newIcon.value checks) so the
branch only runs when the ref's boolean value is true.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (5)
src/components/dialog/CreateIssueTemplateDialog.vue (5)
172-176: Critical: Missing.valueon ref check.Line 175 checks
allowedPathElementsdirectly (the ref object) instead ofallowedPathElements.value, so the button will always be enabled even when the path is invalid.Apply this fix:
:disabled=" !newIcon.name || !newIcon.iconPath || - !allowedPathElements + !allowedPathElements.value "
605-611: Critical: Remove double quotes from SVG path validation regex.Line 606 includes
"in the allowed character class for SVG paths. Per the SVG specification, path data should only contain command letters (M, L, C, etc.), numbers, whitespace, commas, dots, and hyphens. Double quotes are not valid path data characters. Additionally, the/gflag is unnecessary with.test()and can cause unexpected behavior due tolastIndexside effects.Apply this fix:
-const originalAllowedPathElements = (path: string) => /^[MmLlHhVvCcSsQqTtAaZz0-9 ,."\-]+$/g.test(path); +const originalAllowedPathElements = (path: string) => /^[MmLlHhVvCcSsQqTtAaZz0-9 ,.\-]+$/.test(path);
613-626: Critical: Missing.valueon ref check (duplicate location).Line 614 has the same issue as line 175 - checking the ref object directly instead of its value.
Apply this fix:
- if (newIcon.value.name && newIcon.value.iconPath && allowedPathElements) { + if (newIcon.value.name && newIcon.value.iconPath && allowedPathElements.value) {
760-781: Critical: Duplicate check uses wrong collection.Line 766 checks
issueStates.valuefor duplicates when it should checkassignmentTypes.value. This allows duplicate assignment type names to be created, corrupting the data model.Apply this fix:
if (previousName.trim().toLowerCase() !== newName.trim().toLowerCase()) { - if (issueStates.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { + if (assignmentTypes.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { nameErrorMessage.value = "Name already exists"; return;
787-816: Critical: Duplicate check uses wrong collection.Line 793 checks
issueStates.valuefor duplicates when it should checkrelationTypes.value. This allows duplicate relation type names to be created, corrupting the data model.Apply this fix:
if (previousName.trim().toLowerCase() !== newName.trim().toLowerCase()) { - if (issueStates.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { + if (relationTypes.value.some((item) => item.name.trim().toLowerCase() === newName.trim().toLowerCase())) { nameErrorMessage.value = "Name already exists"; return;
🧹 Nitpick comments (1)
src/views/home/Templates/IssueTemplates.vue (1)
48-63: Consider using the orderBy and page parameters.The
fetchItemsmethod receivesorderByandpageparameters but doesn't use them. If sorting and pagination offset are needed, these should be passed to the GraphQL queries.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/components/dialog/CreateIssueTemplateDialog.vue(1 hunks)src/util/eventBus.ts(1 hunks)src/views/home/Templates/IssueTemplates.vue(1 hunks)
🔇 Additional comments (9)
src/util/eventBus.ts (1)
16-20: LGTM!The new template creation events are correctly typed and follow the established naming conventions. This extends the event bus contract appropriately for the new template management functionality.
src/views/home/Templates/IssueTemplates.vue (3)
1-26: LGTM!The template structure is well-organized with proper pagination, sorting, and event handling. The CreateIssueTemplateDialog integration correctly calls the
selectTemplatehandler to navigate upon creation.
38-46: LGTM!Good use of type inference from the GraphQL client return type, and the sort field configuration is appropriate for template listing.
65-88: LGTM!The navigation helpers and styling are correctly implemented. The route parameter uses "trackable" which aligns with the domain model where templates are trackable entities.
src/components/dialog/CreateIssueTemplateDialog.vue (5)
1-36: LGTM!The dialog structure and general information form are well-implemented with proper validation and user feedback for duplicate template names.
333-458: LGTM!The assignment and relation type sections follow a consistent pattern with proper validation for required fields like inverseName.
460-492: LGTM!Good UX implementation with dirty state tracking and confirmation dialog to prevent accidental data loss.
628-758: LGTM!The initial data setup and CRUD functions for issue types, priorities, and states are well-structured with consistent validation patterns and proper state management.
822-865: LGTM!The cleanup, event handling, and dialog lifecycle functions are correctly implemented with proper state resets and navigation logic.
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@src/components/input/IssueTemplatesAutocomplete.vue`:
- Around line 1-7: IssueTemplatesAutocomplete doesn't accept or emit v-model, so
wire it to the internal FetchingAutocomplete: add a prop named modelValue (e.g.,
Array) and declare emits ['update:modelValue'], then pass the model and events
through to FetchingAutocomplete (use :model-value="modelValue" and
`@update`:model-value="$emit('update:modelValue', $event)") while keeping the
existing :fetch="searchIssueTemplates" and item templates so the parent
CreateIssueTemplateDialog.vue v-model (selectedTemplates) syncs correctly.
In `@src/components/TemplatedFieldSpecificationsValueBox.vue`:
- Around line 149-153: The template binds :model-value to
modelValue[childEntry.name] which can throw if modelValue is undefined; change
the binding to use defensive access (e.g.
:model-value="modelValue?.[childEntry.name] ?? undefined" or a suitable default)
so TemplatedFieldSpecificationsValueBox receives a safe value, and ensure
updateChildValue(childEntry.name, newValue) continues to work with the possibly
undefined parent modelValue.
- Line 35: The template conditional for the list case uses a standalone v-if
("node?.type === 'list'") which breaks the chain with the other checks (enum,
map, container) that use v-else-if; change the list directive to v-else-if so it
becomes part of the same conditional chain in the
TemplatedFieldSpecificationsValueBox.vue template (locate the template block
checking node?.type === 'list' and replace v-if with v-else-if to match the
other branches).
In `@src/views/home/Templates/IssueTemplates.vue`:
- Around line 48-63: fetchItems in IssueTemplateItemManager ignores orderBy and
page, breaking sorting and pagination; update fetchItems to compute a skip
(e.g., skip = page * count or adjust if page is 1-based) and pass both orderBy
and skip (or the API's equivalent) into the GraphQL calls:
client.firstIssueTemplates(...) and client.searchIssueTemplates(...). Ensure
IssueTemplateOrder[] is converted/mapped to the GraphQL orderBy input expected
by the API (or passed through directly if types match), and verify the query
signatures accept orderBy and skip/offset arguments before applying.
- Around line 71-78: The templateRoute function is returning an unused params
object including trackable; update the templateRoute(template: IdObject):
RouteLocationRaw implementation to remove the params block so it only returns {
name: "templates-issue" } (leave the function signature intact if template is
still used elsewhere), and ensure no other code depends on a route param named
trackable for the "templates-issue" route; target the templateRoute function and
the RouteLocationRaw return value to remove the dead params/trackbar usage.
🧹 Nitpick comments (3)
src/components/TemplatedFieldSpecificationsValueBox.vue (2)
245-248: Unusedremoveevent declaration.The
removeevent is declared but never emitted in this component. Consider removing it if unused, or implement the emission if it's intended functionality.♻️ Proposed fix (if unused)
const emit = defineEmits<{ (e: "update:modelValue", value: any): void; - (e: "remove"): void; }>();
456-465: Duplicate sorting logic.The
typeWeightsmap and sorting logic are duplicated here and in thenodecomputed property (lines 406-413). Extract to a shared helper.♻️ Proposed refactor
+const typeWeights: Record<string, number> = { + primitive: 0, + enum: 1, + list: 2, + map: 3, + container: 4 +}; + +function sortChildrenByType(children: { name: string; node: Node }[]) { + return children.sort((a, b) => typeWeights[a.node.type] - typeWeights[b.node.type]); +} + const node = computed(() => { let processedNode: Node; if (props.rawNode?.type) { processedNode = JSON.parse(JSON.stringify(props.rawNode)); } else { processedNode = processNode(props.rawNode); } if (processedNode.type === "container") { - const typeWeights: Record<string, number> = { - primitive: 0, - enum: 1, - list: 2, - map: 3, - container: 4 - }; - processedNode.children.sort((a, b) => typeWeights[a.node.type] - typeWeights[b.node.type]); + sortChildrenByType(processedNode.children); } return processedNode; });And similarly update
processNodeto usesortChildrenByType.src/components/icons.ts (1)
127-310: LGTM! Consider adding explicit TypeScript typing.The icon catalog is well-organized. For better type safety, consider adding an explicit type:
♻️ Optional type enhancement
+interface IconEntry { + name: string; + iconPath: string; +} + -export const iconList = [ +export const iconList: IconEntry[] = [ { name: "Bug", ...
| <template> | ||
| <FetchingAutocomplete mode="model-multiple" :fetch="searchIssueTemplates" label="Template" item-title="name"> | ||
| <template #item="{ props, item }"> | ||
| <v-list-item :title="item.raw.name" :subtitle="item.raw.description" v-bind="props"> </v-list-item> | ||
| </template> | ||
| </FetchingAutocomplete> | ||
| </template> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "FetchingAutocomplete" --type=vue -A 3 -B 1Repository: ccims/gropius-frontend
Length of output: 93
🏁 Script executed:
fd FetchingAutocomplete -e vue -e ts -e jsRepository: ccims/gropius-frontend
Length of output: 111
🏁 Script executed:
cat -n src/components/input/FetchingAutocomplete.vueRepository: ccims/gropius-frontend
Length of output: 12141
🏁 Script executed:
rg -n "FetchingAutocomplete" -A 3 -B 1Repository: ccims/gropius-frontend
Length of output: 38215
🏁 Script executed:
rg "IssueTemplatesAutocomplete" --type-listRepository: ccims/gropius-frontend
Length of output: 455
🏁 Script executed:
rg "IssueTemplatesAutocomplete" -A 2 -B 1Repository: ccims/gropius-frontend
Length of output: 1007
🏁 Script executed:
cat -n src/components/input/IssueTemplatesAutocomplete.vueRepository: ccims/gropius-frontend
Length of output: 1540
IssueTemplatesAutocomplete is missing v-model binding for selected templates.
The parent component CreateIssueTemplateDialog.vue uses this component with v-model="selectedTemplates", but IssueTemplatesAutocomplete doesn't define a model or expose any props/emit for value synchronization. The internal FetchingAutocomplete supports v-model, but it's not wired up to communicate selections back to the parent.
🔧 Proposed fix
+const model = defineModel<DefaultIssueTemplateInfoFragment[]>();
+
<template>
- <FetchingAutocomplete mode="model-multiple" :fetch="searchIssueTemplates" label="Template" item-title="name">
+ <FetchingAutocomplete v-model="model" mode="model-multiple" :fetch="searchIssueTemplates" label="Template" item-title="name">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <template> | |
| <FetchingAutocomplete mode="model-multiple" :fetch="searchIssueTemplates" label="Template" item-title="name"> | |
| <template #item="{ props, item }"> | |
| <v-list-item :title="item.raw.name" :subtitle="item.raw.description" v-bind="props"> </v-list-item> | |
| </template> | |
| </FetchingAutocomplete> | |
| </template> | |
| <script setup> | |
| const model = defineModel<DefaultIssueTemplateInfoFragment[]>(); | |
| </script> | |
| <template> | |
| <FetchingAutocomplete v-model="model" mode="model-multiple" :fetch="searchIssueTemplates" label="Template" item-title="name"> | |
| <template `#item`="{ props, item }"> | |
| <v-list-item :title="item.raw.name" :subtitle="item.raw.description" v-bind="props"> </v-list-item> | |
| </template> | |
| </FetchingAutocomplete> | |
| </template> |
🤖 Prompt for AI Agents
In `@src/components/input/IssueTemplatesAutocomplete.vue` around lines 1 - 7,
IssueTemplatesAutocomplete doesn't accept or emit v-model, so wire it to the
internal FetchingAutocomplete: add a prop named modelValue (e.g., Array) and
declare emits ['update:modelValue'], then pass the model and events through to
FetchingAutocomplete (use :model-value="modelValue" and
`@update`:model-value="$emit('update:modelValue', $event)") while keeping the
existing :fetch="searchIssueTemplates" and item templates so the parent
CreateIssueTemplateDialog.vue v-model (selectedTemplates) syncs correctly.
| class IssueTemplateItemManager extends ItemManager<IssueTemplate, IssueTemplateOrderField> { | ||
| protected async fetchItems( | ||
| filter: string, | ||
| orderBy: IssueTemplateOrder[], | ||
| count: number, | ||
| page: number | ||
| ): Promise<[IssueTemplate[], number]> { | ||
| if (!filter) { | ||
| const res = await client.firstIssueTemplates({ count }); | ||
| return [res.issueTemplates.nodes, res.issueTemplates.nodes.length]; | ||
| } else { | ||
| const res = await client.searchIssueTemplates({ query: filter, count }); | ||
| return [res.searchIssueTemplates, res.searchIssueTemplates.length]; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
orderBy and page parameters are unused, breaking sorting and pagination.
The fetchItems method ignores both orderBy and page parameters:
- Sorting selection in the UI will have no effect since
orderByisn't passed to the GraphQL queries. - Pagination will only ever return the first page since
pageisn't used.
🔧 Proposed fix to wire orderBy and page parameters
class IssueTemplateItemManager extends ItemManager<IssueTemplate, IssueTemplateOrderField> {
protected async fetchItems(
filter: string,
orderBy: IssueTemplateOrder[],
count: number,
page: number
): Promise<[IssueTemplate[], number]> {
+ const skip = page * count;
if (!filter) {
- const res = await client.firstIssueTemplates({ count });
+ const res = await client.firstIssueTemplates({ count, skip, orderBy });
return [res.issueTemplates.nodes, res.issueTemplates.nodes.length];
} else {
- const res = await client.searchIssueTemplates({ query: filter, count });
+ const res = await client.searchIssueTemplates({ query: filter, count, skip });
return [res.searchIssueTemplates, res.searchIssueTemplates.length];
}
}
}Note: This assumes the GraphQL queries support skip and orderBy arguments. Please verify the query signatures and adjust accordingly.
🤖 Prompt for AI Agents
In `@src/views/home/Templates/IssueTemplates.vue` around lines 48 - 63, fetchItems
in IssueTemplateItemManager ignores orderBy and page, breaking sorting and
pagination; update fetchItems to compute a skip (e.g., skip = page * count or
adjust if page is 1-based) and pass both orderBy and skip (or the API's
equivalent) into the GraphQL calls: client.firstIssueTemplates(...) and
client.searchIssueTemplates(...). Ensure IssueTemplateOrder[] is
converted/mapped to the GraphQL orderBy input expected by the API (or passed
through directly if types match), and verify the query signatures accept orderBy
and skip/offset arguments before applying.
| function templateRoute(template: IdObject): RouteLocationRaw { | ||
| return { | ||
| name: "templates-issue", | ||
| params: { | ||
| trackable: template.id | ||
| } | ||
| }; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the route definition for "templates-issue"
rg -n "templates-issue" --type=ts --type=js --type=vue -A 5 -B 2Repository: ccims/gropius-frontend
Length of output: 93
🏁 Script executed:
#!/bin/bash
# Search for "templates-issue" route definition without type restriction
rg -n "templates-issue" -A 5 -B 2
# Also search for router configuration files
fd -e ts -e js -e vue -path "*router*" | head -20Repository: ccims/gropius-frontend
Length of output: 2347
🏁 Script executed:
#!/bin/bash
# Search for how the route parameter is used in IssueTemplates.vue
rg -n "route\." src/views/home/Templates/IssueTemplates.vue -A 2 -B 2
# Also check the full route definition context in router
rg -n "templates-issue\|templates-component\|templates-artefact" src/router/index.ts -B 3 -A 5Repository: ccims/gropius-frontend
Length of output: 48
🏁 Script executed:
#!/bin/bash
# Read IssueTemplates.vue to see how route parameters are used
cat src/views/home/Templates/IssueTemplates.vue | head -100
# Check more context from router/index.ts around the templates routes
sed -n '40,60p' src/router/index.tsRepository: ccims/gropius-frontend
Length of output: 3949
🏁 Script executed:
#!/bin/bash
# Check the full router context - parent and children
sed -n '25,70p' src/router/index.ts
# Search for other uses of "trackable" parameter in router
rg -n "trackable" src/router/index.ts -B 3 -A 3Repository: ccims/gropius-frontend
Length of output: 3763
🏁 Script executed:
#!/bin/bash
# Search for route.params or route.query usage in template files
rg -n "route\.params\|route\.query\|useRoute" src/views/home/Templates/ -A 2
# Also check if any template routes have dynamic segments
sed -n '38,90p' src/router/index.tsRepository: ccims/gropius-frontend
Length of output: 2396
🏁 Script executed:
#!/bin/bash
# Read the complete IssueTemplates.vue file
wc -l src/views/home/Templates/IssueTemplates.vue
# Search for any reference to params, query, or tracking in the file
rg -n "params\|query\|trackable" src/views/home/Templates/IssueTemplates.vueRepository: ccims/gropius-frontend
Length of output: 112
🏁 Script executed:
#!/bin/bash
# Read entire IssueTemplates.vue
cat -n src/views/home/Templates/IssueTemplates.vueRepository: ccims/gropius-frontend
Length of output: 3577
Remove unused trackable parameter from templateRoute.
The route "templates-issue" has no dynamic segment in its path definition (path: ""), so the trackable param passed in line 75 is never captured in the URL and is never used by the component. This is dead code that should be removed.
function templateRoute(template: IdObject): RouteLocationRaw {
return {
name: "templates-issue"
};
}
🤖 Prompt for AI Agents
In `@src/views/home/Templates/IssueTemplates.vue` around lines 71 - 78, The
templateRoute function is returning an unused params object including trackable;
update the templateRoute(template: IdObject): RouteLocationRaw implementation to
remove the params block so it only returns { name: "templates-issue" } (leave
the function signature intact if template is still used elsewhere), and ensure
no other code depends on a route param named trackable for the "templates-issue"
route; target the templateRoute function and the RouteLocationRaw return value
to remove the dead params/trackbar usage.
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Fix all issues with AI agents
In `@src/components/dialog/CreateIssueTemplateDialog.vue`:
- Around line 578-579: The v-form in the Template Field Specifications step is
incorrectly bound to formIssueStatesValid; rename the binding to a dedicated
property (e.g., formTemplateFieldsValid) and update the corresponding data
property and any validation/submit logic to use that new symbol instead of
formIssueStatesValid so the validation state correctly reflects the template
fields step; ensure you update every reference to formIssueStatesValid that
pertains to this step (including computed props, watchers, and methods) to the
new name to avoid breaking other steps.
- Around line 1201-1215: The createIssueTemplate function currently omits
templateFieldSpecifications from the GraphQL input and fires
client.createIssueTemplate() without awaiting or handling errors; update
createIssueTemplate to include templateFieldSpecifications (e.g.
templateFieldSpecifications.value or the component state holding the specs)
inside the input object passed to client.createIssueTemplate, and change the
call to await client.createIssueTemplate(...) with try/catch to surface errors
(prevent closing the dialog on failure and show appropriate feedback). Also
update the caller (next()) to await the createIssueTemplate invocation so the
mutation completes and errors are handled before proceeding.
- Around line 449-451: The v-form in the template slot item.3 is bound to a
misleading v-model name formIssuePrioritiesValid even though the step contains
assignment and relation types; rename the model to something descriptive (e.g.,
formAssignmentRelationValid or formIssueAssignmentsValid) and update every
reference in CreateIssueTemplateDialog.vue (data(), reactive refs, computed
properties, watchers, methods, and any validation logic that reads or sets
formIssuePrioritiesValid) as well as the template binding in <v-slot:item.3> so
the variable name consistently reflects "assignment/relation" validation instead
of "priorities".
- Around line 697-704: Remove the dead refs formAssignmentTypeValid,
formRelationTypeValid, formVersionTemplatesValid, and formTemplateFieldsValid
from CreateIssueTemplateDialog.vue by deleting their const declarations (they
are currently created with ref(...)); also search for and remove any stray
references/usages of these four symbols elsewhere in the component so there are
no unresolved identifiers left; leave formissueTypesValid,
formIssuePrioritiesValid, and formIssueStatesValid as-is since those are bound
to v-form v-models.
In `@src/components/TemplatedFieldSpecificationsValueBox.vue`:
- Around line 366-396: Refactor addChild and addPropertyChild to stop
duplicating the type→value mapping by calling the existing
createDefaultValueForType(type, primitiveKind) to produce newValue instead of
repeating the if/else chain; also avoid hard‑coding "properties" as the newName
when type === "property"—generate a unique key (e.g., use a "properties" base
with a counter or a "propertyN" prefix and check modelValue[newName] in the same
loop used for other types) so you don't silently overwrite an existing
properties key, then emit update:modelValue with the computed unique name and
the default value from createDefaultValueForType.
- Around line 547-553: The computed node currently uses the truthiness of
props.rawNode?.type to decide between deep-cloning and processing, which
incorrectly treats raw primitive specs like { type: "string" } as
already-processed; change the guard in the node computed to explicitly detect
processed Node types (e.g., check props.rawNode?.type is one of "primitive",
"enum", "object", "list", etc.) and only deep-clone when that explicit match
succeeds, otherwise call processNode(props.rawNode); update references to node,
props.rawNode, and processNode accordingly.
- Around line 223-227: The prop access modelValue.properties[childEntry.name]
can throw if modelValue or modelValue.properties is undefined; update the
binding to use defensive access (e.g., optional chaining or a fallback) so the
:model-value passed to TemplatedFieldSpecificationsValueBox becomes safe
(modelValue?.properties?.[childEntry.name] or equivalent), and ensure
updatePropertyChildValue(childEntry.name, newValue) continues to be used for
`@update`:model-value so undefined values are handled correctly by that function.
🧹 Nitpick comments (5)
src/components/TemplatedFieldSpecificationsValueBox.vue (1)
556-563:typeWeightsrecord is defined identically three times.Extract it to a module-level constant to reduce duplication.
♻️ Proposed refactor
+const typeWeights: Record<string, number> = { + primitive: 0, + enum: 1, + list: 2, + map: 3, + container: 4, + property: 5 +}; + const node = computed(() => { ... - const typeWeights: Record<string, number> = { ... }; processedNode.children.sort((a, b) => typeWeights[a.node.type] - typeWeights[b.node.type]); ... });Similarly remove the duplicate declarations inside
processNode.Also applies to: 608-616, 636-643
src/views/home/Templates/issueTemplate/Root.vue (2)
30-30:eventBusis injected but never used.
onEventon line 33 is called without referencingeventBus. This is dead code.-const eventBus = inject(eventBusKey);
42-42: Bare expressiontitleSegmentDependency.value;as a reactivity trigger is fragile.This relies on the expression not being tree-shaken or removed by linters/minifiers. Consider assigning it to a variable or using
void titleSegmentDependency.value;to make the intent explicit and prevent accidental removal.- titleSegmentDependency.value; + void titleSegmentDependency.value; // force re-evaluation on title-segment-changedsrc/components/dialog/CreateIssueTemplateDialog.vue (2)
723-731: API call on every keystroke without debounce.The
templateNamewatcher firessearchIssueTemplateson every character typed, which can generate excessive network traffic and race conditions where an older response arrives after a newer one.Suggested approach
Use a debounce utility (e.g.,
useDebounceFnfrom VueUse or a manualsetTimeout/clearTimeout):+import { useDebounceFn } from "@vueuse/core"; + +const checkTemplateName = useDebounceFn(async (newName: string) => { + const res = await client.searchIssueTemplates({ query: newName, count: 1 }); + templateAlreadyExists.value = res.searchIssueTemplates.some((t: { name: string }) => t.name === newName); +}, 300); + watch(templateName, async (newName) => { if (!newName) { templateAlreadyExists.value = false; return; } - - const res = await client.searchIssueTemplates({ query: newName, count: 1 }); - templateAlreadyExists.value = res.searchIssueTemplates.some((t: { name: string }) => t.name === newName); + checkTemplateName(newName); });
645-654: Cancel button click handler silently swallows click when dirty.
@click="!isDirty && cancelCreateIssueTemplate()"— whenisDirtyistrue, the click does nothing visible; theConfirmationDialognested inside presumably activates via Vuetify's activator pattern. This works but is non-obvious and fragile. Consider using theactivatorslot ofConfirmationDialogfor the cancel button explicitly, or add a brief comment explaining the pattern.
| <template v-slot:item.3> | ||
| <v-form v-model="formIssuePrioritiesValid"> | ||
| <v-row> |
There was a problem hiding this comment.
Misleading v-model name: formIssuePrioritiesValid used for the Assignment & Relation Types step.
Step 3's <v-form> is bound to formIssuePrioritiesValid, but this step contains assignment and relation types, not priorities. This naming inconsistency will confuse future maintainers.
Proposed fix
- <v-form v-model="formIssuePrioritiesValid">
+ <v-form v-model="formAssignmentTypeValid">🤖 Prompt for AI Agents
In `@src/components/dialog/CreateIssueTemplateDialog.vue` around lines 449 - 451,
The v-form in the template slot item.3 is bound to a misleading v-model name
formIssuePrioritiesValid even though the step contains assignment and relation
types; rename the model to something descriptive (e.g.,
formAssignmentRelationValid or formIssueAssignmentsValid) and update every
reference in CreateIssueTemplateDialog.vue (data(), reactive refs, computed
properties, watchers, methods, and any validation logic that reads or sets
formIssuePrioritiesValid) as well as the template binding in <v-slot:item.3> so
the variable name consistently reflects "assignment/relation" validation instead
of "priorities".
| <template v-slot:item.4> | ||
| <v-form v-model="formIssueStatesValid"> |
There was a problem hiding this comment.
Misleading v-model name: formIssueStatesValid used for Template Field Specifications step.
Step 4's <v-form> is bound to formIssueStatesValid, but this step contains template field specifications, not issue states.
Proposed fix
- <v-form v-model="formIssueStatesValid">
+ <v-form v-model="formTemplateFieldsValid">🤖 Prompt for AI Agents
In `@src/components/dialog/CreateIssueTemplateDialog.vue` around lines 578 - 579,
The v-form in the Template Field Specifications step is incorrectly bound to
formIssueStatesValid; rename the binding to a dedicated property (e.g.,
formTemplateFieldsValid) and update the corresponding data property and any
validation/submit logic to use that new symbol instead of formIssueStatesValid
so the validation state correctly reflects the template fields step; ensure you
update every reference to formIssueStatesValid that pertains to this step
(including computed props, watchers, and methods) to the new name to avoid
breaking other steps.
| const formGeneralValid = ref(false); | ||
| const formissueTypesValid = ref(true); | ||
| const formIssuePrioritiesValid = ref(true); | ||
| const formIssueStatesValid = ref(true); | ||
| const formAssignmentTypeValid = ref(true); | ||
| const formRelationTypeValid = ref(true); | ||
| const formVersionTemplatesValid = ref(true); | ||
| const formTemplateFieldsValid = ref(true); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, let's check for all usages of these refs in the file
rg -n 'formAssignmentTypeValid|formRelationTypeValid|formVersionTemplatesValid|formTemplateFieldsValid|formissueTypesValid|formIssueStatesValid|formIssuePrioritiesValid|formGeneralValid' src/components/dialog/CreateIssueTemplateDialog.vueRepository: ccims/gropius-frontend
Length of output: 701
🏁 Script executed:
#!/bin/bash
# Search for the next() function or method to see how validation is handled
rg -n -A 20 'next\(\s*\)' src/components/dialog/CreateIssueTemplateDialog.vue | head -100Repository: ccims/gropius-frontend
Length of output: 608
🏁 Script executed:
#!/bin/bash
# Search for meta declaration and how it's used
rg -n 'const meta|meta\.value' src/components/dialog/CreateIssueTemplateDialog.vue | head -30Repository: ccims/gropius-frontend
Length of output: 155
🏁 Script executed:
#!/bin/bash
# Search for where meta comes from - likely from useForm or similar
rg -n 'useForm|meta\s*=' src/components/dialog/CreateIssueTemplateDialog.vue | head -20Repository: ccims/gropius-frontend
Length of output: 188
🏁 Script executed:
#!/bin/bash
# Get context around the v-form declarations to understand what sections they cover
rg -B 3 '<v-form v-model="form' src/components/dialog/CreateIssueTemplateDialog.vue | head -80Repository: ccims/gropius-frontend
Length of output: 618
🏁 Script executed:
#!/bin/bash
# Get the full list of all v-form declarations with their associated models
rg -n '<v-form v-model="form' src/components/dialog/CreateIssueTemplateDialog.vueRepository: ccims/gropius-frontend
Length of output: 337
🏁 Script executed:
#!/bin/bash
# Double-check for any other usages of the four supposedly unused refs
rg -n 'formAssignmentTypeValid\|formRelationTypeValid\|formVersionTemplatesValid\|formTemplateFieldsValid' src/components/dialog/CreateIssueTemplateDialog.vueRepository: ccims/gropius-frontend
Length of output: 48
Remove unused form-validity refs.
formAssignmentTypeValid, formRelationTypeValid, formVersionTemplatesValid, and formTemplateFieldsValid are declared but never used anywhere in the component. Remove them to eliminate dead code.
Note: formissueTypesValid, formIssuePrioritiesValid, and formIssueStatesValid are bound to v-form v-model attributes and serve their intended purpose in tracking section validity. The validation logic via meta.value.valid from useForm() is the correct pattern for this architecture and requires no changes.
🤖 Prompt for AI Agents
In `@src/components/dialog/CreateIssueTemplateDialog.vue` around lines 697 - 704,
Remove the dead refs formAssignmentTypeValid, formRelationTypeValid,
formVersionTemplatesValid, and formTemplateFieldsValid from
CreateIssueTemplateDialog.vue by deleting their const declarations (they are
currently created with ref(...)); also search for and remove any stray
references/usages of these four symbols elsewhere in the component so there are
no unresolved identifiers left; leave formissueTypesValid,
formIssuePrioritiesValid, and formIssueStatesValid as-is since those are bound
to v-form v-models.
| function createIssueTemplate() { | ||
| client.createIssueTemplate({ | ||
| input: { | ||
| name: templateName.value, | ||
| description: templateDescription.value ?? "", | ||
| extends: selectedTemplates.value, | ||
| issueTypes: issueTypes.value, | ||
| issuePriorities: issuePriorities.value, | ||
| issueStates: issueStates.value, | ||
| assignmentTypes: assignmentTypes.value, | ||
| relationTypes: relationTypes.value | ||
| } | ||
| }); | ||
| console.log("Issue template created"); | ||
| } |
There was a problem hiding this comment.
templateFieldSpecifications is missing from the mutation input — user data will be silently lost.
The createIssueTemplate call omits templateFieldSpecifications from the payload, so any field specifications the user adds in step 4 are never sent to the backend.
Additionally, client.createIssueTemplate() returns a promise that is neither awaited nor error-handled. If the mutation fails, the dialog closes with no feedback.
Proposed fix
-function createIssueTemplate() {
- client.createIssueTemplate({
+async function createIssueTemplate() {
+ try {
+ await client.createIssueTemplate({
input: {
name: templateName.value,
description: templateDescription.value ?? "",
extends: selectedTemplates.value,
issueTypes: issueTypes.value,
issuePriorities: issuePriorities.value,
issueStates: issueStates.value,
assignmentTypes: assignmentTypes.value,
- relationTypes: relationTypes.value
+ relationTypes: relationTypes.value,
+ templateFieldSpecifications: templateFieldSpecifications.value
}
- });
- console.log("Issue template created");
+ });
+ } catch (e) {
+ console.error("Failed to create issue template", e);
+ // TODO: surface error to user
+ return;
+ }
}Also update the caller in next() to await:
- createIssueTemplate();
- createIssueTemplateDialog.value = false;
+ await createIssueTemplate();
+ createIssueTemplateDialog.value = false;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function createIssueTemplate() { | |
| client.createIssueTemplate({ | |
| input: { | |
| name: templateName.value, | |
| description: templateDescription.value ?? "", | |
| extends: selectedTemplates.value, | |
| issueTypes: issueTypes.value, | |
| issuePriorities: issuePriorities.value, | |
| issueStates: issueStates.value, | |
| assignmentTypes: assignmentTypes.value, | |
| relationTypes: relationTypes.value | |
| } | |
| }); | |
| console.log("Issue template created"); | |
| } | |
| async function createIssueTemplate() { | |
| try { | |
| await client.createIssueTemplate({ | |
| input: { | |
| name: templateName.value, | |
| description: templateDescription.value ?? "", | |
| extends: selectedTemplates.value, | |
| issueTypes: issueTypes.value, | |
| issuePriorities: issuePriorities.value, | |
| issueStates: issueStates.value, | |
| assignmentTypes: assignmentTypes.value, | |
| relationTypes: relationTypes.value, | |
| templateFieldSpecifications: templateFieldSpecifications.value | |
| } | |
| }); | |
| } catch (e) { | |
| console.error("Failed to create issue template", e); | |
| // TODO: surface error to user | |
| return; | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@src/components/dialog/CreateIssueTemplateDialog.vue` around lines 1201 -
1215, The createIssueTemplate function currently omits
templateFieldSpecifications from the GraphQL input and fires
client.createIssueTemplate() without awaiting or handling errors; update
createIssueTemplate to include templateFieldSpecifications (e.g.
templateFieldSpecifications.value or the component state holding the specs)
inside the input object passed to client.createIssueTemplate, and change the
call to await client.createIssueTemplate(...) with try/catch to surface errors
(prevent closing the dialog on failure and show appropriate feedback). Also
update the caller (next()) to await the createIssueTemplate invocation so the
mutation completes and errors are handled before proceeding.
| <TemplatedFieldSpecificationsValueBox | ||
| :rawNode="childEntry.node" | ||
| :model-value="modelValue.properties[childEntry.name]" | ||
| @update:model-value="(newValue) => updatePropertyChildValue(childEntry.name, newValue)" | ||
| /> |
There was a problem hiding this comment.
Same null-pointer risk on modelValue.properties.
modelValue.properties[childEntry.name] will throw if modelValue or modelValue.properties is undefined. Apply defensive access here as well.
- :model-value="modelValue.properties[childEntry.name]"
+ :model-value="modelValue?.properties?.[childEntry.name]"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <TemplatedFieldSpecificationsValueBox | |
| :rawNode="childEntry.node" | |
| :model-value="modelValue.properties[childEntry.name]" | |
| @update:model-value="(newValue) => updatePropertyChildValue(childEntry.name, newValue)" | |
| /> | |
| <TemplatedFieldSpecificationsValueBox | |
| :rawNode="childEntry.node" | |
| :model-value="modelValue?.properties?.[childEntry.name]" | |
| `@update`:model-value="(newValue) => updatePropertyChildValue(childEntry.name, newValue)" | |
| /> |
🤖 Prompt for AI Agents
In `@src/components/TemplatedFieldSpecificationsValueBox.vue` around lines 223 -
227, The prop access modelValue.properties[childEntry.name] can throw if
modelValue or modelValue.properties is undefined; update the binding to use
defensive access (e.g., optional chaining or a fallback) so the :model-value
passed to TemplatedFieldSpecificationsValueBox becomes safe
(modelValue?.properties?.[childEntry.name] or equivalent), and ensure
updatePropertyChildValue(childEntry.name, newValue) continues to be used for
`@update`:model-value so undefined values are handled correctly by that function.
| function addChild(type: "primitive" | "enum" | "container" | "list" | "map" | "property", primitiveKind: PrimitiveType = "string") { | ||
| const modelValue = props.modelValue ?? {}; | ||
|
|
||
| let newName; | ||
| if(type === "property"){ | ||
| newName = "properties"; | ||
| } else { | ||
| newName = "newName"; | ||
| let counter = 1; | ||
| while (modelValue[newName]) { | ||
| newName = `newItem${counter++}`; | ||
| } | ||
| } | ||
|
|
||
| let newValue: any = {}; | ||
| if (type === "primitive") { | ||
| newValue = { type: primitiveKind }; | ||
| } else if (type === "enum") { | ||
| newValue = { enum: [] }; | ||
| } else if (type === "container") { | ||
| newValue = {}; | ||
| } else if (type === "list") { | ||
| newValue = { elements: { type: "string" } }; | ||
| } else if (type === "map") { | ||
| newValue = { values: { type: "string" } }; | ||
| } else if (type === "property") { | ||
| newValue = { properties: {} }; | ||
| } | ||
|
|
||
| emit("update:modelValue", { ...modelValue, [newName]: newValue }); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
addChild and addPropertyChild duplicate the type→value mapping that already exists in createDefaultValueForType.
Both functions have identical if/else if chains for building newValue (lines 380–393 and 507–520) that duplicate createDefaultValueForType (line 439). Reuse it instead.
Also, hard-coding newName = "properties" when type === "property" (lines 370–371, 497–498) will silently overwrite an existing "properties" key if one already exists.
♻️ Suggested refactor for addChild
function addChild(type: "primitive" | "enum" | "container" | "list" | "map" | "property", primitiveKind: PrimitiveType = "string") {
const modelValue = props.modelValue ?? {};
- let newName;
- if(type === "property"){
- newName = "properties";
- } else {
- newName = "newName";
- let counter = 1;
- while (modelValue[newName]) {
- newName = `newItem${counter++}`;
- }
+ let newName = type === "property" ? "properties" : "newName";
+ let counter = 1;
+ while (modelValue[newName]) {
+ newName = type === "property" ? `properties${counter++}` : `newItem${counter++}`;
}
- let newValue: any = {};
- if (type === "primitive") {
- newValue = { type: primitiveKind };
- } else if (type === "enum") {
- newValue = { enum: [] };
- } else if (type === "container") {
- newValue = {};
- } else if (type === "list") {
- newValue = { elements: { type: "string" } };
- } else if (type === "map") {
- newValue = { values: { type: "string" } };
- } else if (type === "property") {
- newValue = { properties: {} };
- }
+ const newValue = createDefaultValueForType(type);
emit("update:modelValue", { ...modelValue, [newName]: newValue });
}Also applies to: 494-523
🤖 Prompt for AI Agents
In `@src/components/TemplatedFieldSpecificationsValueBox.vue` around lines 366 -
396, Refactor addChild and addPropertyChild to stop duplicating the type→value
mapping by calling the existing createDefaultValueForType(type, primitiveKind)
to produce newValue instead of repeating the if/else chain; also avoid
hard‑coding "properties" as the newName when type === "property"—generate a
unique key (e.g., use a "properties" base with a counter or a "propertyN" prefix
and check modelValue[newName] in the same loop used for other types) so you
don't silently overwrite an existing properties key, then emit update:modelValue
with the computed unique name and the default value from
createDefaultValueForType.
| const node = computed(() => { | ||
| let processedNode: Node; | ||
| if (props.rawNode?.type) { | ||
| processedNode = JSON.parse(JSON.stringify(props.rawNode)); | ||
| } else { | ||
| processedNode = processNode(props.rawNode); | ||
| } |
There was a problem hiding this comment.
node computed incorrectly skips processNode for raw primitive inputs.
The check props.rawNode?.type is truthy for raw primitives like {type: "string"} (where .type is "string"), so the deep-clone path is taken instead of processNode. The result is a node with type: "string", which matches none of the template branches ("primitive", "enum", etc.), rendering nothing.
This works for recursive calls (where rawNode is already a processed Node with type: "primitive"), but will silently break if the component ever receives a raw primitive spec from a parent.
Consider checking for a known Node type explicitly:
🔧 Proposed fix
const node = computed(() => {
let processedNode: Node;
- if (props.rawNode?.type) {
+ const knownNodeTypes = ['primitive', 'enum', 'container', 'property', 'list', 'map'];
+ if (props.rawNode?.type && knownNodeTypes.includes(props.rawNode.type)) {
processedNode = JSON.parse(JSON.stringify(props.rawNode));
} else {
processedNode = processNode(props.rawNode);
}🤖 Prompt for AI Agents
In `@src/components/TemplatedFieldSpecificationsValueBox.vue` around lines 547 -
553, The computed node currently uses the truthiness of props.rawNode?.type to
decide between deep-cloning and processing, which incorrectly treats raw
primitive specs like { type: "string" } as already-processed; change the guard
in the node computed to explicitly detect processed Node types (e.g., check
props.rawNode?.type is one of "primitive", "enum", "object", "list", etc.) and
only deep-clone when that explicit match succeeds, otherwise call
processNode(props.rawNode); update references to node, props.rawNode, and
processNode accordingly.
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Fix all issues with AI agents
In `@src/components/dialog/CreateIssueTemplateDialog.vue`:
- Around line 852-871: handleTemplateInheritance can leave partial inherited
state if it returns early; before looping over selectedTemplates.value, clear
all inherited state: reset every attribute's disabledCards arrays (e.g., clear
the arrays that pushDisabledCards populates) and remove any attributes
previously added by inheritance (the ones pushAttributes creates) so processing
always starts from a clean slate. Keep the existing early-return behavior that
sets templateInheritanceErrorMessage and isInheritanceConflict, but ensure the
cleanup happens first so no stale/incomplete disabledCards or duplicate
inherited attributes persist across invocations.
- Around line 1300-1316: The next() function should be made async and should
await the createIssueTemplate() call so the dialog only closes after the
mutation completes; update the function signature for next to async, change the
call to await createIssueTemplate(), and only set
createIssueTemplateDialog.value = false after the awaited call (also ensure any
callers that invoke next() handle the returned Promise if needed).
In `@src/components/ExpandableCard.vue`:
- Around line 59-62: The component uses props.name to compute isExpanded against
ExpandedKey.nameID, which is fragile; add an explicit unique id prop (e.g., prop
name "id" or "cardId") and update the ExpandedKey.type to use that id field
consistently: change comparisons in isExpanded (and anywhere else using
props.name for identity, e.g., the code around isExpanded and the
expand/collapse handlers) to compare ExpandedKey.nameID with props.id (or
props.cardId) instead of props.name, and ensure the prop is required/validated
so each card supplies a stable unique identifier.
- Around line 33-42: The confirm emit currently always includes description:
localDescription which defaults to "" even when the textarea is not rendered
(props.description === undefined); update the emit logic in the component (where
the confirm event is emitted) to check props.description and only include
description: localDescription when props.description !== undefined, otherwise
emit description: undefined so consumers can distinguish "not provided" from an
intentional empty string; apply the same conditional behavior to the other emit
site referenced (the second confirm/cancel emission) that also uses
localDescription.
In `@src/components/TemplatedFieldSpecificationsValueBox.vue`:
- Line 406: The code reads props.modelValue.enum directly (e.g., in
addEnumEntry) which will throw if modelValue is undefined; update addEnumEntry,
updateEnumEntry, and removeEnumEntry to defensively access enum via optional
chaining/defaults (use const values = [...(props.modelValue?.enum ?? [])]) and
before mutating ensure a safe model object to emit (create a new object with
enum: [] when props.modelValue is falsy), then emit the updated model via
update:modelValue with the new enum array; also add early-return guards in
updateEnumEntry/removeEnumEntry when the targeted index is out of bounds.
In `@src/views/home/Templates/issueTemplate/General.vue`:
- Around line 1-13: Add a loading/error state around the existing issueTemplate
rendering: create a ref (e.g., isLoading) and pass it as the evaluating option
to computedAsync when creating issueTemplate (use computedAsync(..., null, {
evaluating: isLoading })), then update the template to show a
v-progress-circular when isLoading is true, render the existing
DetailCompartment when issueTemplate is non-null/defined, and render a simple
empty/error message (e.g., "Template not found" or "Error loading template")
when not loading and issueTemplate is null/undefined; reference symbols:
issueTemplate, computedAsync, isLoading, v-progress-circular, DetailCompartment.
In `@src/views/home/Templates/issueTemplate/IssueAttributes.vue`:
- Around line 83-87: The code returns res.node as IssueTemplate without checking
for null; update the call that wraps client.getIssueTemplateFields (the
withErrorMessage invocation) to validate the response: after awaiting res, check
that res.node is non-null (e.g., if (!res.node) throw new Error(`Issue template
${issueTemplateId.value} not found or inaccessible`)) before returning it, so
downstream access to .issueTypes.nodes is safe; reference the symbols res.node,
issueTemplateId, withErrorMessage, and client.getIssueTemplateFields when making
the change.
🧹 Nitpick comments (7)
src/views/home/Templates/issueTemplate/General.vue (1)
35-39: Unsafe type assertion onres.node— a missing or mistyped ID will silently producenullfields.If the ID doesn't correspond to an
IssueTemplate(e.g., stale route, deleted template),res.nodemay benullor a different type. Theas IssueTemplatecast bypasses this. A guard would make the failure mode explicit:Proposed fix
const res = await withErrorMessage( () => client.getIssueTemplateFields({ id: issueTemplateId.value }), "Error loading issue template details" ); - return res.node as IssueTemplate; + const node = res.node as IssueTemplate | null; + if (!node) { + return null; + } + return node;src/views/home/Templates/issueTemplate/IssueAttributes.vue (1)
2-2: No loading indicator while data is being fetched.When
issueTemplateisnull(initial load or error), the entire view is hidden with no feedback to the user. Consider adding a loading spinner or skeleton to improve the UX.src/components/ExpandableCard.vue (1)
46-51: Distinguish the Confirm button visually from Cancel.Both buttons render identically, making it hard for users to identify the primary action at a glance. Consider adding
color="primary"(or similar) to the Confirm button.Suggested tweak
- <v-btn class="me-2" `@click`="emit('confirm', { name: localName, description: localDescription })"> + <v-btn class="me-2" color="primary" `@click`="emit('confirm', { name: localName, description: localDescription })">src/components/TemplatedFieldSpecificationsValueBox.vue (1)
562-569:typeWeightsis defined identically three times (lines 562, 615, 642).Extract it as a module-level constant to avoid repetition.
♻️ Suggested refactor
+const TYPE_WEIGHTS: Record<string, number> = { + primitive: 0, + enum: 1, + list: 2, + map: 3, + container: 4, + property: 5 +}; + const node = computed(() => { // ... - const typeWeights: Record<string, number> = { ... }; - processedNode.children.sort((a, b) => typeWeights[a.node.type] - typeWeights[b.node.type]); + processedNode.children.sort((a, b) => TYPE_WEIGHTS[a.node.type] - TYPE_WEIGHTS[b.node.type]);Apply the same replacement in
processNodeat both sorting locations.src/components/dialog/CreateIssueTemplateDialog.vue (3)
738-746: Template name existence check fires on every keystroke with no debounce.The watcher on
templateNameissues a GraphQL query per character typed. This can cause excessive network traffic and out-of-order responses (a slow response for "fo" arriving after the response for "foo" could show a stale result).Consider debouncing the watcher (e.g., 300ms) and/or aborting stale requests.
877-935:handleInheritanceConflictsis highly repetitive — extract a generic loop.The six near-identical
forblocks (lines 878–931) each check a different category but follow the exact same pattern. A helper function likecheckCategoryConflicts(categoryKey, templateNode)would eliminate ~80 lines of duplication.Sketch
async function checkCategoryConflict( categoryKey: string, templateNode: NodeReturnType<"getIssueTemplateFields", "IssueTemplate">, nodesAccessor: (t: typeof templateNode) => { name: string }[] ): Promise<boolean> { for (const entry of disabledCards.value[categoryKey]) { if ( templateNode.id !== entry.fromTemplate && nodesAccessor(templateNode).some((t) => t.name === entry.entry) ) { templateInheritanceErrorMessage.value = `${templateNode.name} is in conflict with ${await getTemplateName(entry.fromTemplate)}`; return true; } } return false; }
1037-1066: CRUD functions for each entity type are nearly identical — consider a generic factory.
createIssueTypeInput,createIssuePriority,createIssueState,createAssignmentType,createRelationType, andcreateTemplateFieldSpecificationshare the same structure: validate name → check duplicates → delete old → push new → sort → reset UI state. A single parameterized function or factory would reduce ~200 lines to ~40 and eliminate the bug surface (e.g., the wrong-collection bugs in assignment/relation).Also applies to: 1072-1112, 1118-1145
| async function handleTemplateInheritance() { | ||
| deleteObsoleteAttributes(); | ||
| for (const templateId of selectedTemplates.value) { | ||
| try { | ||
| const template = await loadTemplate(templateId); | ||
| if (template?.node) { | ||
| const templateNode = template.node as NodeReturnType<"getIssueTemplateFields", "IssueTemplate">; | ||
| await handleInheritanceConflicts(templateNode); | ||
| if (templateInheritanceErrorMessage.value) { | ||
| isInheritanceConflict.value = true; | ||
| return; | ||
| } | ||
| } | ||
| } catch (e) { | ||
| console.error(e); | ||
| } | ||
| } | ||
| deleteObsoleteAttributes(); | ||
| isInheritanceConflict.value = false; | ||
| } |
There was a problem hiding this comment.
Partial state accumulation when template inheritance detects a conflict mid-loop.
If handleInheritanceConflicts detects a conflict for template N (sets templateInheritanceErrorMessage and returns early), templates 0..N-1 have already had their attributes and disabled cards pushed via pushAttributes/pushDisabledCards. The function then returns at line 862 without reaching the second deleteObsoleteAttributes() at line 869. On the next invocation (e.g., when the user changes the selected templates), deleteObsoleteAttributes() is called first — but it only removes items whose every disabled-card record points to a deselected template. Items pushed from still-selected templates in prior partial runs will persist and can accumulate duplicates since pushAttributes filters by name but pushDisabledCards does not.
Consider clearing all inherited state at the top of handleTemplateInheritance (reset all disabledCards arrays and remove all inherited attributes) before re-processing from scratch, to ensure a clean slate regardless of prior partial runs.
🤖 Prompt for AI Agents
In `@src/components/dialog/CreateIssueTemplateDialog.vue` around lines 852 - 871,
handleTemplateInheritance can leave partial inherited state if it returns early;
before looping over selectedTemplates.value, clear all inherited state: reset
every attribute's disabledCards arrays (e.g., clear the arrays that
pushDisabledCards populates) and remove any attributes previously added by
inheritance (the ones pushAttributes creates) so processing always starts from a
clean slate. Keep the existing early-return behavior that sets
templateInheritanceErrorMessage and isInheritanceConflict, but ensure the
cleanup happens first so no stale/incomplete disabledCards or duplicate
inherited attributes persist across invocations.
| function next() { | ||
| if (step.value === 1) { | ||
| if (!meta.value.valid) { | ||
| validate(); | ||
| return; | ||
| } | ||
| if (isInheritanceConflict.value) { | ||
| return; | ||
| } | ||
| } | ||
| if (step.value < stepLabels.length) { | ||
| step.value++; | ||
| } else { | ||
| createIssueTemplate(); | ||
| createIssueTemplateDialog.value = false; | ||
| } | ||
| } |
There was a problem hiding this comment.
next() needs to be async once createIssueTemplate is awaited.
Currently next() is synchronous. When you fix the fire-and-forget createIssueTemplate call (making it async and awaiting it), next() must also become async — otherwise the dialog will close before the mutation completes.
-function next() {
+async function next() {
...
} else {
- createIssueTemplate();
+ await createIssueTemplate();
createIssueTemplateDialog.value = false;
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function next() { | |
| if (step.value === 1) { | |
| if (!meta.value.valid) { | |
| validate(); | |
| return; | |
| } | |
| if (isInheritanceConflict.value) { | |
| return; | |
| } | |
| } | |
| if (step.value < stepLabels.length) { | |
| step.value++; | |
| } else { | |
| createIssueTemplate(); | |
| createIssueTemplateDialog.value = false; | |
| } | |
| } | |
| async function next() { | |
| if (step.value === 1) { | |
| if (!meta.value.valid) { | |
| validate(); | |
| return; | |
| } | |
| if (isInheritanceConflict.value) { | |
| return; | |
| } | |
| } | |
| if (step.value < stepLabels.length) { | |
| step.value++; | |
| } else { | |
| await createIssueTemplate(); | |
| createIssueTemplateDialog.value = false; | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@src/components/dialog/CreateIssueTemplateDialog.vue` around lines 1300 -
1316, The next() function should be made async and should await the
createIssueTemplate() call so the dialog only closes after the mutation
completes; update the function signature for next to async, change the call to
await createIssueTemplate(), and only set createIssueTemplateDialog.value =
false after the awaited call (also ensure any callers that invoke next() handle
the returned Promise if needed).
| <v-textarea | ||
| v-if="props.description !== undefined" | ||
| class="mx-2" | ||
| label="Description" | ||
| v-model="localDescription" | ||
| auto-grow | ||
| rows="1" | ||
| max-rows="2" | ||
| density="compact" | ||
| /> |
There was a problem hiding this comment.
confirm always emits description: "" even when the description field is hidden.
When props.description is undefined, the textarea is not rendered (line 34), but the confirm event still includes description: localDescription which defaults to "". Consumers may misinterpret this as the user intentionally clearing the description. Consider emitting description: undefined when description wasn't provided.
Also applies to: 48-48
🤖 Prompt for AI Agents
In `@src/components/ExpandableCard.vue` around lines 33 - 42, The confirm emit
currently always includes description: localDescription which defaults to ""
even when the textarea is not rendered (props.description === undefined); update
the emit logic in the component (where the confirm event is emitted) to check
props.description and only include description: localDescription when
props.description !== undefined, otherwise emit description: undefined so
consumers can distinguish "not provided" from an intentional empty string; apply
the same conditional behavior to the other emit site referenced (the second
confirm/cancel emission) that also uses localDescription.
| type ExpandedKey = { | ||
| nameID: string; | ||
| type: string; | ||
| } | null; |
There was a problem hiding this comment.
Using props.name as the card identity is fragile and can cause multiple cards to expand simultaneously.
isExpanded matches expandedCardKey.nameID against props.name. If two cards share the same name and type, both will compute isExpanded = true. Consider introducing a dedicated unique id prop instead of relying on the display name for identity.
The ExpandedKey type already names the field nameID, which suggests it's intended to be a unique identifier — but comparing it to the mutable display name undermines that intent.
Proposed fix: add an explicit `id` prop
const props = withDefaults(
defineProps<{
+ id: string;
name: string;
description?: string;
expandedCardKey: ExpandedKey;
type: string;
nameErrorMessage?: string;
editable?: boolean;
}>(),
{
editable: true
}
);
const isExpanded = computed(
- () => props.editable && props.expandedCardKey?.type === props.type && props.expandedCardKey?.nameID === props.name
+ () => props.editable && props.expandedCardKey?.type === props.type && props.expandedCardKey?.nameID === props.id
);Also applies to: 85-87
🤖 Prompt for AI Agents
In `@src/components/ExpandableCard.vue` around lines 59 - 62, The component uses
props.name to compute isExpanded against ExpandedKey.nameID, which is fragile;
add an explicit unique id prop (e.g., prop name "id" or "cardId") and update the
ExpandedKey.type to use that id field consistently: change comparisons in
isExpanded (and anywhere else using props.name for identity, e.g., the code
around isExpanded and the expand/collapse handlers) to compare
ExpandedKey.nameID with props.id (or props.cardId) instead of props.name, and
ensure the prop is required/validated so each card supplies a stable unique
identifier.
| } | ||
|
|
||
| function addEnumEntry() { | ||
| const values = [...(props.modelValue.enum ?? [])]; |
There was a problem hiding this comment.
addEnumEntry accesses props.modelValue.enum without null guard.
If modelValue is undefined, props.modelValue.enum will throw. Same risk applies to updateEnumEntry and removeEnumEntry.
🔧 Proposed fix
function addEnumEntry() {
- const values = [...(props.modelValue.enum ?? [])];
+ const values = [...(props.modelValue?.enum ?? [])];
values.push("newValue");
- emit("update:modelValue", { ...props.modelValue, enum: values });
+ emit("update:modelValue", { ...(props.modelValue ?? {}), enum: values });
}Apply similar defensive access in updateEnumEntry and removeEnumEntry.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const values = [...(props.modelValue.enum ?? [])]; | |
| function addEnumEntry() { | |
| const values = [...(props.modelValue?.enum ?? [])]; | |
| values.push("newValue"); | |
| emit("update:modelValue", { ...(props.modelValue ?? {}), enum: values }); | |
| } |
🤖 Prompt for AI Agents
In `@src/components/TemplatedFieldSpecificationsValueBox.vue` at line 406, The
code reads props.modelValue.enum directly (e.g., in addEnumEntry) which will
throw if modelValue is undefined; update addEnumEntry, updateEnumEntry, and
removeEnumEntry to defensively access enum via optional chaining/defaults (use
const values = [...(props.modelValue?.enum ?? [])]) and before mutating ensure a
safe model object to emit (create a new object with enum: [] when
props.modelValue is falsy), then emit the updated model via update:modelValue
with the new enum array; also add early-return guards in
updateEnumEntry/removeEnumEntry when the targeted index is out of bounds.
| <template> | ||
| <div class="pa-4 h-100 overflow-y-auto" v-if="issueTemplate != undefined"> | ||
| <DetailCompartment name="General"> | ||
| <div class="field-group"> | ||
| <div class="field-label text-medium-emphasis">Name</div> | ||
| <div class="field-value">{{ issueTemplate.name }}</div> | ||
| </div> | ||
| <div class="field-group"> | ||
| <div class="field-label text-medium-emphasis">Description</div> | ||
| <div class="field-value">{{ issueTemplate.description || "No description provided" }}</div> | ||
| </div> | ||
| </DetailCompartment> | ||
| </div> |
There was a problem hiding this comment.
No loading or error state is rendered while the template is being fetched.
When issueTemplate is null (initial/loading state) or when the fetch fails, the user sees a blank page with no feedback. Consider showing a loading spinner (e.g., v-progress-circular) or a skeleton while computedAsync is resolving, and an error/empty state if the node is not found.
computedAsync provides an evaluating ref you can use:
const issueTemplate = computedAsync(
async () => { ... },
null,
{ shallow: false, evaluating: isLoading }
);Then in the template:
<v-progress-circular v-if="isLoading" indeterminate />
<div v-else-if="issueTemplate != undefined" class="pa-4 h-100 overflow-y-auto">
...
</div>🤖 Prompt for AI Agents
In `@src/views/home/Templates/issueTemplate/General.vue` around lines 1 - 13, Add
a loading/error state around the existing issueTemplate rendering: create a ref
(e.g., isLoading) and pass it as the evaluating option to computedAsync when
creating issueTemplate (use computedAsync(..., null, { evaluating: isLoading
})), then update the template to show a v-progress-circular when isLoading is
true, render the existing DetailCompartment when issueTemplate is
non-null/defined, and render a simple empty/error message (e.g., "Template not
found" or "Error loading template") when not loading and issueTemplate is
null/undefined; reference symbols: issueTemplate, computedAsync, isLoading,
v-progress-circular, DetailCompartment.
| const res = await withErrorMessage( | ||
| () => client.getIssueTemplateFields({ id: issueTemplateId.value }), | ||
| "Error loading issue template details" | ||
| ); | ||
| return res.node as IssueTemplate; |
There was a problem hiding this comment.
Missing null guard on res.node.
If the query returns a null node (e.g. deleted or inaccessible template), res.node as IssueTemplate silently passes null through the type assertion, and the template will fail when accessing .issueTypes.nodes. Add a guard or throw a descriptive error.
Proposed fix
const res = await withErrorMessage(
() => client.getIssueTemplateFields({ id: issueTemplateId.value }),
"Error loading issue template details"
);
- return res.node as IssueTemplate;
+ if (res.node == null) {
+ return null;
+ }
+ return res.node as IssueTemplate;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const res = await withErrorMessage( | |
| () => client.getIssueTemplateFields({ id: issueTemplateId.value }), | |
| "Error loading issue template details" | |
| ); | |
| return res.node as IssueTemplate; | |
| const res = await withErrorMessage( | |
| () => client.getIssueTemplateFields({ id: issueTemplateId.value }), | |
| "Error loading issue template details" | |
| ); | |
| if (res.node == null) { | |
| return null; | |
| } | |
| return res.node as IssueTemplate; |
🤖 Prompt for AI Agents
In `@src/views/home/Templates/issueTemplate/IssueAttributes.vue` around lines 83 -
87, The code returns res.node as IssueTemplate without checking for null; update
the call that wraps client.getIssueTemplateFields (the withErrorMessage
invocation) to validate the response: after awaiting res, check that res.node is
non-null (e.g., if (!res.node) throw new Error(`Issue template
${issueTemplateId.value} not found or inaccessible`)) before returning it, so
downstream access to .issueTypes.nodes is safe; reference the symbols res.node,
issueTemplateId, withErrorMessage, and client.getIssueTemplateFields when making
the change.
Summary by CodeRabbit
New Features
Chores
Style