Skip to content

first commit for create issue Template UI#4

Open
LLemonLauncher wants to merge 13 commits intomainfrom
feature/issueTemplates
Open

first commit for create issue Template UI#4
LLemonLauncher wants to merge 13 commits intomainfrom
feature/issueTemplates

Conversation

@LLemonLauncher
Copy link

@LLemonLauncher LLemonLauncher commented Aug 30, 2025

Summary by CodeRabbit

  • New Features

    • Added Templates top-nav with Issue Templates listing, detail pages (General, Attributes, Linkage, Field Specs) and routing.
    • Multi-step Create Issue Template dialog with icon management, inheritance checks, and creation workflow.
    • New reusable components: expandable/editable cards, SVG wrapper, icon catalog, templated field editor, issue-templates autocomplete, paginated templates list.
  • Chores

    • Added mdi icon dependency.
  • Style

    • Centered sidebar items and icon buttons for improved alignment.

@LLemonLauncher LLemonLauncher self-assigned this Aug 30, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 30, 2025

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Create Template UI & Icon catalog
src/components/dialog/CreateIssueTemplateDialog.vue, src/components/icons.ts, src/components/SvgWrapper.vue
New multi-step Create Issue Template dialog with icon gallery/management and SVG path rendering; central iconList added.
Templates list & item manager
src/views/home/Templates/IssueTemplates.vue
New paginated IssueTemplates view and IssueTemplateItemManager subclass; wires CreateIssueTemplateDialog to navigate on creation.
Routing and home navigation
src/router/index.ts, src/views/home/Root.vue
Adds /templates routes with nested detail routes and inserts Templates tab and sidebar entries/actions for creating templates.
Template detail pages
src/views/home/Templates/issueTemplate/Root.vue, .../General.vue, .../IssueAttributes.vue, .../LinkageAttributes.vue, .../TemplateFieldSpecifications.vue
New issue-template detail layout and pages that fetch template fields and render General, Attributes, Linkage, and Field Specifications views.
Editable/Expandable cards
src/components/ExpandableCard.vue, src/components/EditableCard.vue
New reusable card components for collapsed/expanded editing and inline editing; emit expand/cancel/confirm/delete and provide preview/extra slots.
Templated field editor
src/components/TemplatedFieldSpecificationsValueBox.vue
New recursive editor for template field specifications (primitive, enum, list, map, container, property) with add/rename/delete and update:modelValue propagation.
Issue templates input
src/components/input/IssueTemplatesAutocomplete.vue
New fetching autocomplete for issue templates using search/first GraphQL endpoints with error handling.
SVG/icon dependency
package.json
Adds dependency @mdi/js ^7.4.47 for icon path data.
Event bus typing
src/util/eventBus.ts
Adds new Events keys: create-issue-template, create-artefact-template, create-component-template, create-interface-specification-template, create-relation-template.
GraphQL schema & generated client
src/graphql/issueTemplate.graphql, src/graphql/generated.ts
Adds IssueTemplateFields fragment, queries and mutations for issue templates and issues; expands generated TypeScript client functions for template/issue operations.
Sidebar style tweaks
src/components/SideBar.vue, src/components/SideBarButton.vue
Minor layout adjustments to center sidebar items and icons; no logic/API 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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • nk-coding
  • spethso

Poem

A rabbit hops where templates grow,
Cards unfold and icon paths glow,
Wizards stitch each field and name,
I nibble bytes and spark the flame,
Hooray — templates jump to life 🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'first commit for create issue Template UI' accurately describes the primary change: introducing the initial implementation of the Create Issue Template UI functionality with multiple new components and features.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issueTemplates

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between f843ab0 and 2f4c508.

📒 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.)

Copy link
Collaborator

@nk-coding nk-coding left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls also run the formatter (npm run format)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (6)
src/components/dialog/CreateIssueTemplateDialog.vue (4)

575-582: Validate SVG path before adding to iconList.

The confirmAddIcon function 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 newName against issueStates instead of assignmentTypes.

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 newName against issueStates instead of relationTypes.

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-template but the handler calls an undefined selectTemplate. Use the existing selectComponent or implement selectTemplate.

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 skip for 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2f4c508 and ca11368.

📒 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/js for 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 isExpanded prevents 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
src/components/dialog/CreateIssueTemplateDialog.vue (3)

538-541: Expanded-card type union misses valid values

Line 335 and below assign "assignment"/"relation" to expandedCardKey.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 collection

Line 760 still checks issueStates for name collisions, so duplicate assignment types slip through. Compare against assignmentTypes instead 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 states

Same problem here: Lines 787-791 consult issueStates, so duplicate relation type names aren’t caught. Guard against relationTypes instead.

-        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

📥 Commits

Reviewing files that changed from the base of the PR and between ca11368 and 3586749.

📒 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

Copy link
Collaborator

@nk-coding nk-coding left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/font can 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3586749 and 62c968d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/components/icons.ts (2)

1-125: Consider type annotation for better IntelliSense.

The exported iconList lacks an explicit type, which limits editor autocompletion and type safety. Add a type annotation or use as const to 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 const for 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, and Interface-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 during confirmAddIcon (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

📥 Commits

Reviewing files that changed from the base of the PR and between 62c968d and 69db1cf.

📒 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 SvgWrapper instead of v-html addresses 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.log placeholder 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?

Comment on lines +47 to +89
@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 ?? ''
);
}
"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +788 to +817
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 = "";
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.value for duplicate relation type names but should check relationTypes.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.value for duplicate assignment type names but should check assignmentTypes.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

📥 Commits

Reviewing files that changed from the base of the PR and between 69db1cf and 1203893.

📒 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:

  1. No template-specific components exist in the codebase
  2. component/Home.vue renders generic component details (displaying component.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.

Comment on lines +605 to +611
const allowedPathElements = ref<boolean>(false);
const originalAllowedPathElements = (path: string) => /^[MmLlHhVvCcSsQqTtAaZz0-9 ,."\-]+$/g.test(path);

watch(
() => newIcon.value.iconPath,
(newPath) => (allowedPathElements.value = originalAllowedPathElements(newPath))
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -20

Length 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.

Comment on lines +613 to +626
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";
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
src/components/dialog/CreateIssueTemplateDialog.vue (5)

172-176: Critical: Missing .value on ref check.

Line 175 checks allowedPathElements directly (the ref object) instead of allowedPathElements.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 /g flag is unnecessary with .test() and can cause unexpected behavior due to lastIndex side 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 .value on 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.value for duplicates when it should check assignmentTypes.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.value for duplicates when it should check relationTypes.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 fetchItems method receives orderBy and page parameters 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1203893 and 34c0520.

📒 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 selectTemplate handler 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: Unused remove event declaration.

The remove event 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 typeWeights map and sorting logic are duplicated here and in the node computed 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 processNode to use sortChildrenByType.

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",
         ...

Comment on lines +1 to +7
<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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "FetchingAutocomplete" --type=vue -A 3 -B 1

Repository: ccims/gropius-frontend

Length of output: 93


🏁 Script executed:

fd FetchingAutocomplete -e vue -e ts -e js

Repository: ccims/gropius-frontend

Length of output: 111


🏁 Script executed:

cat -n src/components/input/FetchingAutocomplete.vue

Repository: ccims/gropius-frontend

Length of output: 12141


🏁 Script executed:

rg -n "FetchingAutocomplete" -A 3 -B 1

Repository: ccims/gropius-frontend

Length of output: 38215


🏁 Script executed:

rg "IssueTemplatesAutocomplete" --type-list

Repository: ccims/gropius-frontend

Length of output: 455


🏁 Script executed:

rg "IssueTemplatesAutocomplete" -A 2 -B 1

Repository: ccims/gropius-frontend

Length of output: 1007


🏁 Script executed:

cat -n src/components/input/IssueTemplatesAutocomplete.vue

Repository: 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.

Suggested change
<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.

Comment on lines +48 to +63
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];
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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 orderBy isn't passed to the GraphQL queries.
  • Pagination will only ever return the first page since page isn'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.

Comment on lines +71 to +78
function templateRoute(template: IdObject): RouteLocationRaw {
return {
name: "templates-issue",
params: {
trackable: template.id
}
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 2

Repository: 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 -20

Repository: 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 5

Repository: 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.ts

Repository: 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 3

Repository: 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.ts

Repository: 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.vue

Repository: ccims/gropius-frontend

Length of output: 112


🏁 Script executed:

#!/bin/bash
# Read entire IssueTemplates.vue
cat -n src/views/home/Templates/IssueTemplates.vue

Repository: 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: typeWeights record 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: eventBus is injected but never used.

onEvent on line 33 is called without referencing eventBus. This is dead code.

-const eventBus = inject(eventBusKey);

42-42: Bare expression titleSegmentDependency.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-changed
src/components/dialog/CreateIssueTemplateDialog.vue (2)

723-731: API call on every keystroke without debounce.

The templateName watcher fires searchIssueTemplates on 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., useDebounceFn from VueUse or a manual setTimeout/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()" — when isDirty is true, the click does nothing visible; the ConfirmationDialog nested inside presumably activates via Vuetify's activator pattern. This works but is non-obvious and fragile. Consider using the activator slot of ConfirmationDialog for the cancel button explicitly, or add a brief comment explaining the pattern.

Comment on lines +449 to +451
<template v-slot:item.3>
<v-form v-model="formIssuePrioritiesValid">
<v-row>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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".

Comment on lines +578 to +579
<template v-slot:item.4>
<v-form v-model="formIssueStatesValid">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +697 to +704
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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.vue

Repository: 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 -100

Repository: 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 -30

Repository: 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 -20

Repository: 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 -80

Repository: 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.vue

Repository: 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.vue

Repository: 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.

Comment on lines +1201 to +1215
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");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

Comment on lines +223 to +227
<TemplatedFieldSpecificationsValueBox
:rawNode="childEntry.node"
:model-value="modelValue.properties[childEntry.name]"
@update:model-value="(newValue) => updatePropertyChildValue(childEntry.name, newValue)"
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
<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.

Comment on lines +366 to +396
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 });
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +547 to +553
const node = computed(() => {
let processedNode: Node;
if (props.rawNode?.type) {
processedNode = JSON.parse(JSON.stringify(props.rawNode));
} else {
processedNode = processNode(props.rawNode);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 on res.node — a missing or mistyped ID will silently produce null fields.

If the ID doesn't correspond to an IssueTemplate (e.g., stale route, deleted template), res.node may be null or a different type. The as IssueTemplate cast 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 issueTemplate is null (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: typeWeights is 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 processNode at 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 templateName issues 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: handleInheritanceConflicts is highly repetitive — extract a generic loop.

The six near-identical for blocks (lines 878–931) each check a different category but follow the exact same pattern. A helper function like checkCategoryConflicts(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, and createTemplateFieldSpecification share 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

Comment on lines +852 to +871
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +1300 to +1316
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;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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).

Comment on lines +33 to +42
<v-textarea
v-if="props.description !== undefined"
class="mx-2"
label="Description"
v-model="localDescription"
auto-grow
rows="1"
max-rows="2"
density="compact"
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +59 to +62
type ExpandedKey = {
nameID: string;
type: string;
} | null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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 ?? [])];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +1 to +13
<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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +83 to +87
const res = await withErrorMessage(
() => client.getIssueTemplateFields({ id: issueTemplateId.value }),
"Error loading issue template details"
);
return res.node as IssueTemplate;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants