feat: update epic action#81
Conversation
WalkthroughAdds epic update capability: new UpdateEpic form and modal components, integration on epic detail page to open modal, i18n keys for update UI, a PATCH API endpoint to update an epic by ID, and a shared updateEpicSchema/type. Also adjusts the epics list page title to use i18n. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Page as Epic Detail Page
participant Overlay as Overlay Controller
participant Modal as ModalUpdateEpic
participant Form as FormUpdateEpic
participant API as PATCH /api/epic/id/{epicId}
participant Repo as repository.epic.update
participant Store as Epic Store
participant Toast as Toast
Page->>Overlay: create(ModalUpdateEpic)
Page->>Overlay: open({ epicId })
Overlay->>Modal: mount with { epicId }
Modal->>Form: render with { epicId }
Form->>Form: validate(updateEpicSchema)
Form->>API: submit PATCH { title?, description? }
API->>Repo: update(epicId, data)
Repo-->>API: epic
API-->>Form: { ok: true, result }
Form->>Store: refresh()
Form->>Toast: show('toast.epic-updated')
Form-->>Modal: emit('submitted'), emit('success')
Modal->>Overlay: closeAll()
alt error
API-->>Form: error
Form->>Toast: show(error)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (12)
apps/web-app/app/pages/epic/index.vue (1)
95-96: Make document title reactive to locale changes
useHeadcalled with a static object won’t update when the user switches language. Use the functional form so the title recomputes on locale change.-useHead({ - title: t('app.menu.epics'), -}) +useHead(() => ({ + title: t('app.menu.epics'), +}))Minor related nit (outside this hunk): the import of the RU locale from date-fns is typically either a default import from the deep path or a named import from the root locale module. Current code uses a named import from a deep path, which can break depending on bundler.
- Option A (preferred):
import { ru } from 'date-fns/locale'- Option B:
import ru from 'date-fns/locale/ru'apps/web-app/app/components/modal/UpdateEpic.vue (1)
4-8: Close the modal on success only (avoid closing on “submitted”)Closing on
submittedcan dismiss the modal even if client/server validation fails. Prefer keeping it open until a confirmed success.<FormUpdateEpic :epic-id="epicId" - @submitted="overlay.closeAll" @success="overlay.closeAll" />apps/web-app/server/api/epic/id/[epicId]/index.patch.ts (3)
21-21: Guard against empty updatesBoth fields in
updateEpicSchemaare optional, so{}passes validation and results in a no-op update. Consider rejecting empty payloads with a 400.- const epic = await repository.epic.update(epicId, data) + if (Object.keys(data).length === 0) { + throw createError({ + statusCode: 400, + message: 'Nothing to update', + }) + } + + const epic = await repository.epic.update(epicId, data)
21-26: Return 404 when epic is not foundIf the repository returns null/undefined when the epic doesn’t exist, return a 404 instead of
{ ok: true, result: null }.- const epic = await repository.epic.update(epicId, data) - - return { - ok: true, - result: epic, - } + const epic = await repository.epic.update(epicId, data) + if (!epic) { + throw createError({ + statusCode: 404, + message: 'Epic not found', + }) + } + return { ok: true, result: epic }
16-19: Use schema.assert for validation in this endpointSwitching to
updateEpicSchema.assert(body)will perform validation and throw a typed error automatically, eliminating the manualinstanceof type.errorscheck and the extratypeimport.• File: apps/web-app/server/api/epic/id/[epicId]/index.patch.ts
• Lines: 15–19Suggested change:
-import { type } from 'arktype' const body = await readBody(event) - const data = updateEpicSchema(body) - if (data instanceof type.errors) { - throw data - } + // validate and throw on error + const data = updateEpicSchema.assert(body)Note: Ensure your ArkType version supports the
.assertAPI before applying. You may optionally apply this pattern across other endpoints to simplify schema validation.apps/web-app/shared/services/epic.ts (1)
9-14: Remove redundant “| undefined” in optional fieldsUsing
.optional()already covers absence. The| undefinedunion is redundant and slightly obscures intent.export const updateEpicSchema = type({ - title: type('2 <= string <= 150 | undefined').describe('error.length.invalid').optional(), - description: type('string <= 1500 | undefined').describe('error.length.invalid').optional(), + title: type('2 <= string <= 150').describe('error.length.invalid').optional(), + description: type('string <= 1500').describe('error.length.invalid').optional(), })apps/web-app/app/pages/epic/[epicId]/index.vue (3)
9-17: Overlay-driven Edit action is correctly gated; add i18n + aria-label for accessibilityGood use of v-if guarding and overlay modal integration. Consider moving the hardcoded Russian tooltip into i18n and adding an aria-label to the icon-only button.
- <UTooltip v-if="epic?.id" :text="`Редактировать «${epic?.title}»`"> + <UTooltip v-if="epic?.id" :text="t('app.update.epic.button')"> <UButton variant="outline" color="neutral" size="md" icon="i-lucide-pencil" class="size-10 justify-center" + :aria-label="t('app.update.epic.button')" @click="modalUpdateEpic.open({ epicId: epic.id })" /> </UTooltip>
30-31: Simplify condition for comments block
epic?.comments.length >= 0is always true for an array. If the intent is to show the block only when there are comments, check for length > 0. If the intent is simply “comments exist”, the length check is redundant.- <div v-if="epic?.comments && epic?.comments.length >= 0" class="text-muted"> + <div v-if="epic?.comments?.length" class="text-muted">
83-85: Make page title reactive to epic store updates
useHead({ ... })with a plain object captures the initial value only. Wrap it in a function to keep it reactive and provide a fallback title.-useHead({ - title: epic.value?.title, -}) +useHead(() => ({ + title: epic.value?.title ?? t('app.menu.epic'), +}))apps/web-app/app/components/form/UpdateEpic.vue (3)
16-24: Localize the description placeholderHardcoded Russian text should be moved to i18n for consistency.
- placeholder="Суть. Для чего создается, что будет обсуждаться, что будет сделано?" + :placeholder="$t('app.update.epic.description.placeholder')"Please ensure the key
app.update.epic.description.placeholderexists in locales (ru-RU.json and others as needed).
26-34: Prevent double submissions by binding loading state to the submit buttonAdd a pending state to disable the button and show a loader during submission.
<UButton type="submit" variant="solid" color="secondary" size="xl" block class="mt-3" + :loading="pending" + :disabled="pending" :label="$t('common.update')" />
60-78: Manage pending state and ensure toast lifecycle is balancedTrack pending around the async operation and reset it in finally. Also emit 'submitted' before network call as you do (good).
-async function onSubmit(event: FormSubmitEvent<UpdateEpic>) { - const toastId = actionToast.start() - emit('submitted') - - try { - await $fetch(`/api/epic/id/${epicId}`, { - method: 'PATCH', - body: event.data, - }) - - await epicStore.update() - - actionToast.success(toastId, t('toast.epic-updated')) - emit('success') - } catch (error) { - console.error(error) - actionToast.error(toastId) - } -} +const pending = ref(false) + +async function onSubmit(event: FormSubmitEvent<UpdateEpic>) { + const toastId = actionToast.start() + emit('submitted') + pending.value = true + + try { + await $fetch(`/api/epic/id/${epicId}`, { + method: 'PATCH', + body: event.data, + }) + await epicStore.update() + actionToast.success(toastId, t('toast.epic-updated')) + emit('success') + } catch (error) { + console.error(error) + actionToast.error(toastId) + } finally { + pending.value = false + } +}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
apps/web-app/app/components/form/UpdateEpic.vue(1 hunks)apps/web-app/app/components/modal/UpdateEpic.vue(1 hunks)apps/web-app/app/pages/epic/[epicId]/index.vue(2 hunks)apps/web-app/app/pages/epic/index.vue(1 hunks)apps/web-app/i18n/locales/ru-RU.json(1 hunks)apps/web-app/server/api/epic/id/[epicId]/index.patch.ts(1 hunks)apps/web-app/shared/services/epic.ts(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/web-app/server/api/epic/id/[epicId]/index.patch.ts (1)
apps/web-app/shared/services/epic.ts (1)
updateEpicSchema(9-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (6)
apps/web-app/i18n/locales/ru-RU.json (2)
288-292: LGTM — consistent with existing i18n patternsKeys and wording align with the rest of the update section. No issues.
288-292: No additional locales detected—no further action neededOnly apps/web-app/i18n/locales/ru-RU.json exists in the i18n/locales directory, so there are no other locale files requiring the new app.update.epic keys.
apps/web-app/app/components/modal/UpdateEpic.vue (1)
1-11: Solid modal wrapper and event wiringProps, slot usage, and overlay integration look clean. Using
$tdirectly in the template is consistent with other components.apps/web-app/server/api/epic/id/[epicId]/index.patch.ts (1)
5-13: Confirm auth/permissions are enforced upstreamThis endpoint updates epics. Verify it’s behind appropriate auth/permission middleware (e.g., user is authorized to edit this epic), either via route rules or a preceding handler.
apps/web-app/app/pages/epic/[epicId]/index.vue (2)
72-72: Importing ModalUpdateEpic from #components looks correctThe import path aligns with Nuxt auto-generated component exports.
80-82: Overlay controller creation is appropriateUsing
overlay.create(ModalUpdateEpic)and invokingopen({ epicId })is idiomatic for your overlay modal pattern.
| <UForm | ||
| :validate="createValidator(updateEpicSchema)" | ||
| :state="state" | ||
| class="flex flex-col gap-3" | ||
| @submit="onSubmit" | ||
| > |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Validate availability of createValidator
createValidator isn’t imported here. If it’s not globally auto-imported, this will throw at runtime. Please confirm it’s provided by your project’s auto-imports, or import it explicitly.
Run this script to locate its definition and ensure it’s exported/auto-imported:
🏁 Script executed:
#!/bin/bash
# Find where `createValidator` is defined/exported and how it's imported elsewhere
rg -n -C2 --type=ts --type=vue '\bcreateValidator\b'Length of output: 82
🏁 Script executed:
#!/bin/bash
# Search for any definition or import of `createValidator` across the repo
rg -n 'createValidator' .Length of output: 3791
🏁 Script executed:
#!/bin/bash
# Show the first 20 lines of UpdateEpic.vue and CreateProduct.vue for import context
head -n 20 apps/web-app/app/components/form/UpdateEpic.vue
echo "----"
head -n 20 apps/web-app/app/components/form/CreateProduct.vueLength of output: 1266
🏁 Script executed:
#!/bin/bash
echo "---- UpdateEpic.vue (first 200 lines) ----"
head -n200 apps/web-app/app/components/form/UpdateEpic.vue
echo ""
echo "---- CreateProduct.vue (first 200 lines) ----"
head -n200 apps/web-app/app/components/form/CreateProduct.vue
echo ""
echo "---- Search for AutoImport in Vite config ----"
rg -n "AutoImport" -g "vite.config.*"
rg -n "autoImport" -g "vite.config.*"
rg -n "unplugin-auto-import" -g "vite.config.*"Length of output: 4546
🏁 Script executed:
#!/bin/bash
# Check if unplugin-auto-import is listed in dependencies
rg -n 'unplugin-auto-import' package.json
# Search Nuxt config for auto-import plugin configuration
rg -n 'unplugin-auto-import' -g 'nuxt.config.*'
rg -n 'autoImport' -g 'nuxt.config.*'Length of output: 127
Import createValidator in UpdateEpic.vue
It’s defined in apps/web-app/app/utils/ui.ts but isn’t auto-imported, so you’ll get a runtime error. Add an explicit import (or configure auto-imports) in your <script setup>.
• File: apps/web-app/app/components/form/UpdateEpic.vue
• At the top of the <script setup lang="ts"> block
Suggested diff:
<script setup lang="ts">
+ import { createValidator } from '../../utils/ui'
import type { UpdateEpic } from '#shared/services/epic'
import type { FormSubmitEvent } from '@nuxt/ui'
import { updateEpicSchema } from '#shared/services/epic'
…📝 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.
| <UForm | |
| :validate="createValidator(updateEpicSchema)" | |
| :state="state" | |
| class="flex flex-col gap-3" | |
| @submit="onSubmit" | |
| > | |
| <script setup lang="ts"> | |
| import { createValidator } from '../../utils/ui' | |
| import type { UpdateEpic } from '#shared/services/epic' | |
| import type { FormSubmitEvent } from '@nuxt/ui' | |
| import { updateEpicSchema } from '#shared/services/epic' | |
| // … | |
| </script> |
🤖 Prompt for AI Agents
In apps/web-app/app/components/form/UpdateEpic.vue around lines 2 to 7, the
template uses createValidator but the symbol is not imported, causing a runtime
error; open the <script setup lang="ts"> block at the top of the file and add an
explicit import for createValidator from the module apps/web-app/app/utils/ui.ts
(or enable/configure your auto-imports to include it), then save and rebuild to
ensure the component resolves createValidator at runtime.
| const state = ref<Partial<UpdateEpic>>({ | ||
| title: epic.value?.title, | ||
| description: epic.value?.description ?? undefined, | ||
| }) |
There was a problem hiding this comment.
Form state won’t prefill if the epic arrives after mount
state is initialized from epic.value once. If the store loads asynchronously, inputs remain empty. Initialize reactively once when the epic becomes available.
-const state = ref<Partial<UpdateEpic>>({
- title: epic.value?.title,
- description: epic.value?.description ?? undefined,
-})
+const state = reactive<Partial<UpdateEpic>>({
+ title: epic.value?.title,
+ description: epic.value?.description ?? undefined,
+})
+
+// Initialize from store once when epic becomes available to avoid overriding user edits
+const initialized = ref(false)
+watch(epic, (val) => {
+ if (val && !initialized.value) {
+ state.title = val.title
+ state.description = val.description ?? undefined
+ initialized.value = true
+ }
+}, { immediate: true })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const state = ref<Partial<UpdateEpic>>({ | |
| title: epic.value?.title, | |
| description: epic.value?.description ?? undefined, | |
| }) | |
| -const state = ref<Partial<UpdateEpic>>({ | |
| - title: epic.value?.title, | |
| - description: epic.value?.description ?? undefined, | |
| const state = reactive<Partial<UpdateEpic>>({ | |
| title: epic.value?.title, | |
| description: epic.value?.description ?? undefined, | |
| }) | |
| // Initialize from store once when epic becomes available to avoid overriding user edits | |
| const initialized = ref(false) | |
| watch(epic, (val) => { | |
| if (val && !initialized.value) { | |
| state.title = val.title | |
| state.description = val.description ?? undefined | |
| initialized.value = true | |
| } | |
| }, { immediate: true }) |
🤖 Prompt for AI Agents
In apps/web-app/app/components/form/UpdateEpic.vue around lines 55–58, the
reactive form state is only initialized once from epic.value so inputs stay
empty if epic loads after mount; add a watcher on epic (watch(epic, ..., {
immediate: true })) that sets state.title and state.description from epic.value
when epic becomes available, but only assign each field if it is currently
undefined/null to avoid clobbering user edits.
| result: epic, | ||
| } | ||
| } catch (error) { | ||
| throw errorResolver(error) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Ensure errorResolver is in scope (or import it)
errorResolver isn’t imported here. If it’s not globally available in the server runtime, this will throw at runtime.
🏁 Script executed:
#!/bin/bash
# Find definition/imports of errorResolver to confirm availability
rg -nP '(^\s*(export\s+)?(const|function)\s+errorResolver\b)|(\berrorResolver\s*=\s*)|from\s+.*errorResolver' -C2Length of output: 1528
🏁 Script executed:
#!/bin/bash
# List import statements to verify if errorResolver is imported
rg -n '^import ' 'apps/web-app/server/api/epic/id/[epicId]/index.patch.ts'Length of output: 222
Add missing import for errorResolver
The call to
throw errorResolver(error)in apps/web-app/server/api/epic/id/[epicId]/index.patch.ts will fail at runtime because errorResolver isn’t imported. Please add, for example:
// at the top of apps/web-app/server/api/epic/id/[epicId]/index.patch.ts
import { errorResolver } from '../../../utils/error'—or, if you’re using path aliases:
import { errorResolver } from '~~/server/utils/error'🤖 Prompt for AI Agents
In apps/web-app/server/api/epic/id/[epicId]/index.patch.ts around line 28, the
call to throw errorResolver(error) will fail because errorResolver is not
imported; add the appropriate import at the top of the file (either the relative
path import from ../../../utils/error or your project alias import such as
~~/server/utils/error) so errorResolver is defined before use.



Summary by CodeRabbit