Skip to content

feat: create flow item action#179

Merged
hmbanan666 merged 1 commit into
mainfrom
create-flow-item
Sep 23, 2025
Merged

feat: create flow item action#179
hmbanan666 merged 1 commit into
mainfrom
create-flow-item

Conversation

@hmbanan666
Copy link
Copy Markdown
Collaborator

@hmbanan666 hmbanan666 commented Sep 23, 2025

Summary by CodeRabbit

  • New Features
    • New “Создать пост” page with a form to add flow items, accessible from the “Поток” header button; shows a success toast and returns to the main page.
    • Copy agreements data to clipboard on the Agreement page.
  • Improvements
    • Unified titles across pages using a new SectionTitle component.
    • Flow items and details show user avatars when available.
    • Header and navigation layout refinements.
    • Checkout card avatar now references the correct entity.
  • Localization
    • Added Russian toast message: “Пост создан”.

@hmbanan666 hmbanan666 self-assigned this Sep 23, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 23, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Navigation updates
apps/atrium-telegram/app/components/Navigation.vue, apps/atrium-telegram/app/composables/useNavigation.ts, apps/atrium-telegram/app/pages/index.vue
Removed isMainPage usage in Navigation; added 'flow-new' to mainRoutes; added "Создать пост" button linking to /flow/new in the Flow section header.
SectionTitle component & adoption
apps/atrium-telegram/app/components/SectionTitle.vue, apps/atrium-telegram/app/pages/... (epic/[epicId]/index.vue, flow/[itemId]/index.vue, index.vue header section, navigation.vue, no-auth.vue, tasks/index.vue, ticket/[ticketId]/index.vue, ticket/index.vue)
New SectionTitle component; replaced various h1/static headers with SectionTitle across multiple pages.
Flow creation feature (UI, API, schema, DB, i18n)
apps/atrium-telegram/app/pages/flow/new.vue, apps/atrium-telegram/app/components/form/CreateFlowItem.vue, apps/atrium-telegram/server/api/flow/index.post.ts, apps/atrium-telegram/shared/services/flow.ts, packages/database/src/types.ts, packages/database/src/tables.ts, apps/atrium-telegram/i18n/locales/ru-RU.json
Added flow/new page and CreateFlowItem form with validation, submit, toasts, haptics; new POST /api/flow handler; shared arktype schema and CreateFlowItem type; DB: FlowItemType includes 'user_post', flowItems gains userId with FK to users and relations; i18n adds toast key "flow-item-created".
Flow avatar rendering
apps/atrium-telegram/app/components/flow/ItemCard.vue, apps/atrium-telegram/app/pages/flow/[itemId]/index.vue
Conditionally render UAvatar when item.userId is present; added computed userAvatarUrl from userStore; fallback to clipboard icon otherwise; also switched titles to SectionTitle where applicable.
Agreement: copy to clipboard
apps/web-app/app/pages/agreement/index.vue
Added header copy button; implemented JSON-to-CSV-like conversion and clipboard write handler.
Checkout avatar source tweak
apps/web-app/app/components/CheckoutCard.vue
Avatar URL now uses checkout?.id instead of checkout?.clientId.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A nibble of code, a hop through the flow,
New posts sprout up where the carrots grow.
Titles now shine with a tidy gleam,
Avatars wink in the river’s stream.
Copy, clip! Data on the go—
Thump-thump! says the rabbit, shipping the show. 🥕✨

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat: create flow item action" succinctly and accurately reflects the primary work in the changeset (adding flow item creation UI, API endpoint, schema, and related DB/type updates); it is concise, uses a conventional "feat" prefix, and is specific enough for team members scanning history to understand the main change.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch create-flow-item

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.

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (25)
packages/database/src/tables.ts (2)

1319-1326: Add reverse relation on users for discoverability.

Consider adding flowItems: many(flowItems) to userRelations so 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.id makes 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 as prop 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-active in 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-access to 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 SectionTitle

If 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 button

Icon-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 SectionTitle

Ensure 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 title

Consider using i18n for "Создание поста" to keep consistency with the rest of the app.


17-19: Minor: no need to return navigateTo

Returning 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 a11y

Provide 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 SectionTitle

Ensure SectionTitle renders the correct heading level for detail pages (likely h1).


71-71: Reuse existing store helper for avatar URL

For 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

📥 Commits

Reviewing files that changed from the base of the PR and between 59249c3 and fd42413.

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

Good replacement with SectionTitle for "Команда".


9-9: LGTM: unified heading component

Good 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 SectionTitle

Unifies 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-imported

createValidator 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.ts

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

Comment on lines +13 to +18
const item = await repository.flow.createItem({
type: data.type,
title: data.title,
description: data.description,
userId: data.userId,
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

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

@hmbanan666 hmbanan666 merged commit 20e0b5d into main Sep 23, 2025
8 checks passed
@hmbanan666 hmbanan666 deleted the create-flow-item branch September 23, 2025 12:06
@coderabbitai coderabbitai Bot mentioned this pull request Sep 25, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Oct 31, 2025
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.

1 participant