feat: create flow item action#179
Conversation
WalkthroughAdds a user-post flow creation feature end-to-end (UI form, route, API, schema, DB), introduces a reusable SectionTitle component and replaces headings across pages, updates navigation to include flow-new, renders user avatars in flow items, adds agreement data copy-to-clipboard, and adjusts minor assets and i18n. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant P as /flow/new Page
participant F as CreateFlowItem.vue
participant A as API POST /api/flow
participant DB as Database
participant S as Stores (flowStore,userStore)
participant T as Toast/Haptics
U->>P: Open /flow/new
P->>F: Render form (title, description, type)
U->>F: Submit form
F->>F: Validate with createFlowItemSchema
alt valid
F->>A: POST { title, description, type, userId? } (Auth header)
A->>DB: repository.flow.createItem(...)
alt created
DB-->>A: Flow item
A-->>F: { ok: true, result }
F->>S: Update flowStore / userStore
F->>T: Success toast ("flow-item-created"), haptic vibrate
F-->>P: emit('success')
P->>P: Navigate to '/'
else failed
A-->>F: Error via errorResolver
F->>T: Error toast, error haptic
end
else invalid
F->>T: Validation error feedback
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ 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. Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (25)
packages/database/src/tables.ts (2)
1319-1326: Add reverse relation on users for discoverability.Consider adding
flowItems: many(flowItems)touserRelationsso user → items is easy to traverse.Outside this diff, in
userRelations:export const userRelations = relations(users, ({ many, one }) => ({ // ... flowItems: many(flowItems), }))
809-812: Optional: require userId for 'user_post' and add index on user_id
- createFlowItemSchema currently makes userId optional and the POST handler forwards it unchanged — enforce conditional validation (type === 'user_post' ⇒ userId required) in apps/atrium-telegram/shared/services/flow.ts or apps/atrium-telegram/server/api/flow/index.post.ts (or add a DB CHECK: type != 'user_post' OR user_id IS NOT NULL in packages/database/src/tables.ts).
- Add an index on flow_items.user_id (and consider a composite index on (type, created_at)) to speed author/feed lookups.
apps/web-app/app/components/CheckoutCard.vue (1)
7-7: Fallback to clientId when available to keep avatar consistent per client.Using
checkout.idmakes avatars vary per order. Prefer client’s id when present, with checkout id as fallback.Apply this diff:
- :src="`https://avatar.nextorders.ru/${checkout?.id}?emotion=8`" + :src="`https://avatar.nextorders.ru/${checkout?.clientId ?? checkout?.id}?emotion=8`"apps/atrium-telegram/app/components/SectionTitle.vue (1)
1-11: Allow heading level to preserve semantic hierarchy (a11y).Some pages need an h1. Add an
asprop to render h1–h6 as needed.Apply this diff:
-<template> - <h2 class="text-2xl/6 font-bold tracking-tight"> - {{ title }} - </h2> -</template> +<template> + <component :is="as" class="text-2xl/6 font-bold tracking-tight"> + {{ title }} + </component> +</template> <script setup lang="ts"> -defineProps<{ - title: string -}>() +const props = withDefaults(defineProps<{ + title: string + as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' +}>(), { as: 'h2' }) </script>apps/atrium-telegram/app/pages/ticket/index.vue (1)
4-4: Use i18n for the title string.Avoid hardcoded Russian. Prefer a translation key.
Example:
- <SectionTitle title="Активные тикеты" /> + <SectionTitle :title="$t('app.tickets-active')" />And add
app.tickets-activein locales.apps/atrium-telegram/app/pages/no-auth.vue (1)
3-3: Localize the title.Hardcoded string; use i18n.
Example:
- <SectionTitle title="Нет доступа!" /> + <SectionTitle :title="$t('error.no-access')" />Add
error.no-accessto locales.apps/atrium-telegram/app/pages/tasks/index.vue (1)
11-11: Consider i18n with placeholder for the greeting.Keeps localization consistent.
Example:
<SectionTitle :title="$t('app.greeting', { name: userStore.name })" />And in locales:
"greeting": "{name}, привет!"apps/atrium-telegram/app/pages/ticket/[ticketId]/index.vue (1)
16-16: Confirm heading semantics in SectionTitleIf SectionTitle doesn’t render an h1 on detail pages, we may regress a11y/SEO. Consider a prop (e.g., tag/level) to keep h1 here.
apps/atrium-telegram/app/pages/index.vue (1)
19-29: Add i18n for the button label and verify route exists
- Consider localizing "Создать пост".
- Confirm the /flow/new route is registered (new page looks present in this PR).
apps/web-app/app/pages/agreement/index.vue (2)
41-47: Add accessible label/title to the copy buttonIcon-only buttons need an accessible name.
Apply this diff:
<UButton variant="outline" color="neutral" size="md" icon="i-lucide-copy" + aria-label="Скопировать" + title="Скопировать" @click="handleCopyDataClick()" />
351-371: Make CSV export deterministic and robust (ordering, escaping, empty arrays, errors)
- Enforce column order and add a header row.
- Escape values containing ;, ", or newlines.
- Default files to [] to avoid runtime errors.
- Handle clipboard errors.
-function convertJsonToCSV() { - const csvRows = partnerStore.agreements.map((row) => { - // Remove unnecessary data - const { kitchens, files, createdAt, updatedAt, legalEntity, legalEntityId, id, ...rest } = row as any - - // Take only name from legalEntity - rest.legalEntityName = legalEntity?.name - - rest.files = files.map((file: any) => file.name).join(',') - - return Object.values(rest).join(';') - }) - - return csvRows.join('\n') -} - -function handleCopyDataClick() { - const csvData = convertJsonToCSV() - navigator.clipboard.writeText(csvData) -} +function convertJsonToCSV() { + const agreements = partnerStore.agreements + if (!agreements.length) return '' + + // Derive ordered columns from the first row (excluding dropped fields) and append computed fields. + const { kitchens: _k1, files: _f1 = [], createdAt: _c1, updatedAt: _u1, legalEntity: _l1, legalEntityId: _le1, id: _id1, ...restSample } = agreements[0] as any + const columns = [...Object.keys(restSample), 'legalEntityName', 'files'] + + const escapeCsv = (value: string) => (/[;\n"]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value) + + const rows = agreements.map((row) => { + const { kitchens: _k, files = [], createdAt: _c, updatedAt: _u, legalEntity, legalEntityId: _le, id: _id, ...rest } = row as any + const out: Record<string, any> = { + ...rest, + legalEntityName: legalEntity?.name ?? '', + files: files.map((f: any) => f.name).join(','), + } + return columns.map((key) => escapeCsv(String(out[key] ?? ''))).join(';') + }) + + return [columns.join(';'), ...rows].join('\n') +} + +async function handleCopyDataClick() { + const csvData = convertJsonToCSV() + try { + await navigator.clipboard.writeText(csvData) + } catch (e) { + console.error('Failed to copy data', e) + } +}apps/atrium-telegram/app/pages/epic/[epicId]/index.vue (1)
16-16: Confirm heading level in SectionTitleEnsure SectionTitle renders an appropriate heading level (likely h1 here) to keep document outline intact.
apps/atrium-telegram/app/pages/flow/new.vue (2)
4-7: Localize static titleConsider using i18n for "Создание поста" to keep consistency with the rest of the app.
17-19: Minor: no need to return navigateToReturning the promise isn’t required here; optionally await for clarity.
-function handleSuccess() { - return navigateTo('/') -} +async function handleSuccess() { + await navigateTo('/') +}apps/atrium-telegram/app/pages/flow/[itemId]/index.vue (3)
5-14: Add alt text to avatar for a11yProvide alt (e.g., author name) when rendering UAvatar.
- <UAvatar + <UAvatar v-if="item?.userId" :src="userAvatarUrl" class="size-10" + alt="Автор поста" />
17-17: Confirm heading level in SectionTitleEnsure SectionTitle renders the correct heading level for detail pages (likely h1).
71-71: Reuse existing store helper for avatar URLFor consistency with views block, use userStore.getAvatarUrl.
-const userAvatarUrl = computed(() => userStore.users.find((user) => user.id === item.value?.userId)?.avatarUrl ?? undefined) +const userAvatarUrl = computed(() => (item.value?.userId ? userStore.getAvatarUrl(item.value.userId) : undefined))apps/atrium-telegram/server/api/flow/index.post.ts (1)
8-11: Return proper HTTP status for validation errors.Map schema errors to 422/400 instead of throwing raw errors.
Apply this diff:
- if (data instanceof type.errors) { - throw data - } + if (data instanceof type.errors) { + throw createError({ statusCode: 422, statusMessage: 'Validation failed', data }) + }apps/atrium-telegram/app/components/flow/ItemCard.vue (1)
61-61: Optional: provide avatar alt/fallback.Improve accessibility/fallback when avatar URL is missing.
Apply this diff:
-const userAvatarUrl = computed(() => userStore.users.find((user) => user.id === item.userId)?.avatarUrl ?? undefined) +const userAvatarUrl = computed(() => userStore.users.find((user) => user.id === item.userId)?.avatarUrl ?? undefined)And in template:
- <UAvatar + <UAvatar v-if="item.userId" - :src="userAvatarUrl" + :src="userAvatarUrl" + :alt="item.title" class="size-8" />apps/atrium-telegram/app/components/Navigation.vue (1)
3-16: Remove commented code block.Dead commented markup adds noise; delete if not planned for imminent reuse.
Apply this diff:
- <!-- <div class="w-full h-14 px-4 py-0 flex flex-row flex-nowrap gap-0 items-start justify-center transition-all duration-200 ease-in-out"> - <UButton - v-if="isMainPage" - variant="solid" - color="secondary" - size="xl" - class="z-60 absolute top-8 transition-all duration-200 ease-in-out motion-preset-slide-up motion-duration-1000" - icon="i-lucide-plus" - :ui="{ - base: 'size-12 font-bold rounded-full', - leadingIcon: 'size-6 mx-auto', - }" - /> - </div> -->apps/atrium-telegram/app/components/form/CreateFlowItem.vue (3)
56-61: Don’t include userId in client payload. Server should set it from auth.Avoid sending userId to prevent spoofing and reduce mismatch with server-side source of truth.
Apply this diff:
const state = ref<Partial<CreateFlowItem>>({ title: undefined, description: undefined, type: 'user_post', - userId: userStore.id, })
63-74: Only send whitelisted fields to the server.Build a payload without userId; the server will attach it from context.
Apply this diff:
async function onSubmit(event: FormSubmitEvent<CreateFlowItem>) { const toastId = actionToast.start() emit('submitted') try { - await $fetch('/api/flow', { + const payload = { + title: event.data.title, + description: event.data.description, + type: event.data.type, + } + await $fetch('/api/flow', { method: 'POST', headers: { Authorization: `tma ${userStore.initDataRaw}`, }, - body: event.data, + body: payload, })
81-83: Optional: optimistic update using API response.Use the returned item to update the store immediately to reduce perceived latency, then background-refresh.
apps/atrium-telegram/shared/services/flow.ts (2)
7-10: Tighten optionals; avoid redundant| undefined.Simplify schemas; keep optionals via
.optional()and let the server derive userId.Apply this diff:
- description: type('string <= 1500 | undefined').describe('error.length.invalid').optional(), + description: type('string <= 1500').describe('error.length.invalid').optional(), type: flowTypeSchema.describe('error.length.invalid'), - userId: type('string | undefined').describe('error.length.invalid').optional(), + userId: type('string').describe('error.length.invalid').optional(),
5-10: Optional: define a server-only schema without userId.A separate server schema (no userId) reduces risk if client types drift.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (21)
apps/atrium-telegram/app/components/Navigation.vue(3 hunks)apps/atrium-telegram/app/components/SectionTitle.vue(1 hunks)apps/atrium-telegram/app/components/flow/ItemCard.vue(2 hunks)apps/atrium-telegram/app/components/form/CreateFlowItem.vue(1 hunks)apps/atrium-telegram/app/composables/useNavigation.ts(1 hunks)apps/atrium-telegram/app/pages/epic/[epicId]/index.vue(1 hunks)apps/atrium-telegram/app/pages/flow/[itemId]/index.vue(2 hunks)apps/atrium-telegram/app/pages/flow/new.vue(1 hunks)apps/atrium-telegram/app/pages/index.vue(2 hunks)apps/atrium-telegram/app/pages/navigation.vue(1 hunks)apps/atrium-telegram/app/pages/no-auth.vue(1 hunks)apps/atrium-telegram/app/pages/tasks/index.vue(1 hunks)apps/atrium-telegram/app/pages/ticket/[ticketId]/index.vue(1 hunks)apps/atrium-telegram/app/pages/ticket/index.vue(1 hunks)apps/atrium-telegram/i18n/locales/ru-RU.json(1 hunks)apps/atrium-telegram/server/api/flow/index.post.ts(1 hunks)apps/atrium-telegram/shared/services/flow.ts(1 hunks)apps/web-app/app/components/CheckoutCard.vue(1 hunks)apps/web-app/app/pages/agreement/index.vue(2 hunks)packages/database/src/tables.ts(2 hunks)packages/database/src/types.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/atrium-telegram/server/api/flow/index.post.ts (1)
apps/atrium-telegram/shared/services/flow.ts (1)
createFlowItemSchema(5-10)
⏰ 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 (10)
apps/atrium-telegram/i18n/locales/ru-RU.json (1)
135-137: Approve — flow creation uses toast.flow-item-created; no overlap.CreateFlowItem.vue calls t('toast.flow-item-created') (apps/atrium-telegram/app/components/form/CreateFlowItem.vue:81); ru-RU.json contains both "post-created" and "flow-item-created", but only "flow-item-created" is used for flow items.
apps/atrium-telegram/app/composables/useNavigation.ts (1)
13-13: Route name 'flow-new' present — no action required.
apps/atrium-telegram/app/pages/flow/new.vue declares name: 'flow-new' (line 13).apps/atrium-telegram/app/pages/index.vue (2)
4-4: LGTM: unified heading componentGood replacement with SectionTitle for "Команда".
9-9: LGTM: unified heading componentGood replacement with SectionTitle for "Данные на сегодня".
packages/database/src/types.ts (1)
39-39: LGTM: added FlowItemType 'user_post'Aligns with user-post creation flow.
Please verify DB enum/schema and any validators (e.g., Zod) include 'user_post' to avoid insert/parse failures.
apps/atrium-telegram/app/pages/navigation.vue (1)
3-3: LGTM: consistent headings via SectionTitleUnifies typography across pages.
apps/atrium-telegram/app/components/flow/ItemCard.vue (1)
4-13: LGTM: conditional avatar rendering is clear and concise.apps/atrium-telegram/app/components/Navigation.vue (1)
39-39: LGTM: matches updated useNavigation signature.apps/atrium-telegram/app/components/form/CreateFlowItem.vue (1)
2-7: Import createValidator or ensure it's auto-importedcreateValidator is exported in apps/atrium-telegram/app/utils/ui.ts (also in apps/web-app) but CreateFlowItem.vue uses it without an import and no auto-import configuration was found — add an explicit import in the <script setup> or enable/confirm auto-import for this utility.
apps/atrium-telegram/server/api/flow/index.post.ts (1)
45-47: Resolved — errorResolver is exported in apps/atrium-telegram/server/utils/error.tsMultiple API routes (including apps/atrium-telegram/server/api/flow/index.post.ts) call errorResolver without an import; the function is exported at apps/atrium-telegram/server/utils/error.ts so this is intentional (auto-import/global). No change required unless you prefer adding an explicit import.
| const item = await repository.flow.createItem({ | ||
| type: data.type, | ||
| title: data.title, | ||
| description: data.description, | ||
| userId: data.userId, | ||
| }) |
There was a problem hiding this comment.
Do not trust client-supplied userId; derive from authenticated context.
Accepting userId from the request body enables impersonation. Use event.context.user.id and ignore body.userId.
Apply this diff:
- const item = await repository.flow.createItem({
- type: data.type,
- title: data.title,
- description: data.description,
- userId: data.userId,
- })
+ const userId = event.context.user?.id
+ if (!userId) {
+ throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
+ }
+ const item = await repository.flow.createItem({
+ type: data.type,
+ title: data.title,
+ description: data.description,
+ userId,
+ })📝 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 item = await repository.flow.createItem({ | |
| type: data.type, | |
| title: data.title, | |
| description: data.description, | |
| userId: data.userId, | |
| }) | |
| const userId = event.context.user?.id | |
| if (!userId) { | |
| throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }) | |
| } | |
| const item = await repository.flow.createItem({ | |
| type: data.type, | |
| title: data.title, | |
| description: data.description, | |
| userId, | |
| }) |
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/flow/index.post.ts around lines 13 to 18, the
code is using client-supplied data.userId when creating a flow item which allows
impersonation; instead derive the user ID from the authenticated context
(event.context.user.id) and ignore any userId in the request body. Update the
createItem call to pass the authenticated user id, validate that
event.context.user and event.context.user.id exist (return 401/400 if missing),
and remove or ignore usage of data.userId in this handler so only the
server-derived user ID is stored.



Summary by CodeRabbit