refactor!: migrate from Supabase client to internal API for various components#4480
refactor!: migrate from Supabase client to internal API for various components#4480
Conversation
…mponents - Updated delete-resource.tsx to use deleteWorkspaceStorageObject instead of Supabase client. - Refactored file-display.tsx to utilize createWorkspaceStorageSignedUrl from internal API. - Changed delete-link-button.tsx to use updateWorkspaceCourseModule for link deletion. - Replaced Supabase client calls in category-spending-chart.tsx with getCategoryBreakdown API. - Updated spending-trends-chart.tsx to fetch data using getSpendingTrends API. - Refactored recurring transaction form to use internal API for creating and updating transactions. - Modified recurring-transactions-page.tsx to list and delete recurring transactions via internal API. - Updated category-filter.tsx to fetch transaction categories using internal API. - Refactored category-breakdown-dialog.tsx to use getCategoryBreakdown API. - Changed category-donut-chart.tsx to fetch category breakdown data using internal API. - Updated period-breakdown-panel.tsx to use getTransactionStats API. - Refactored user-filter.tsx to list workspace members using internal API. - Updated wallet-role-access.tsx to fetch workspace roles using internal API. - Refactored google-calendar-settings.tsx to delete calendar connections using internal API. - Updated hour-settings.tsx to manage workspace calendar hours via internal API. - Refactored use-workspace-permission.ts to check permissions using internal API. - Updated use-workspace-user.ts to fetch user profile using internal API. - Added tests to ensure deprecated Supabase client imports are removed from various files.
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR systematically refactors the application to replace direct Supabase client calls with a centralized internal API abstraction layer. New REST endpoints are added for workspaces across AI models, finance, mail, posts, roles, permissions, and storage. Corresponding internal-api wrapper modules are created and exposed via package.json exports. Client-side components and hooks are updated to call these new wrapper functions instead of querying Supabase directly. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Browser Client
participant RestAPI as Next.js REST API<br/>/api/v1/*
participant InternalAPI as Internal API<br/>Module Wrapper
participant Supabase as Supabase<br/>Database
rect rgba(100, 150, 200, 0.5)
Note over Client,Supabase: Before: Direct Supabase Access
Client->>Client: Create Supabase client
Client->>Supabase: Query table directly<br/>e.g., ai_model_favorites
Supabase-->>Client: Return data
end
rect rgba(100, 200, 100, 0.5)
Note over Client,Supabase: After: Abstracted via Internal API
Client->>InternalAPI: Call wrapper function<br/>e.g., listWorkspaceAiModelFavorites()
InternalAPI->>RestAPI: HTTP GET/POST/PUT/DELETE<br/>/api/v1/workspaces/*/...
RestAPI->>RestAPI: Validate permissions<br/>Resolve auth user
RestAPI->>Supabase: Query/mutate as needed
Supabase-->>RestAPI: Return result
RestAPI-->>InternalAPI: Return JSON response
InternalAPI-->>Client: Return typed data
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔧 Biome Check ReportTotal Issues Found: 0🔧 Format Check Results ✅
🔍 Lint Check Results ✅
🎉 All Issues Resolved!Your code is now 100% clean! Great job! 🏆
🤖 Auto-generated by Biome Check workflow • Last updated: 3/20/2026, 5:10:21 PM
|
🌐 i18n Check Report✅ All i18n checks passed!
🤖 Auto-generated by i18n Check workflow • Last updated: 3/20/2026, 5:10:25 PM
|
There was a problem hiding this comment.
7 issues found across 65 files
Confidence score: 2/5
- There is meaningful merge risk because multiple high-confidence issues are user-impacting, including a permission gap in
apps/web/src/app/api/v1/workspaces/[wsId]/storage/object/route.tswhere users with any workspace permission may be able to delete storage objects withoutmanage_drive. apps/web/src/app/api/v1/workspaces/[wsId]/promotions/referral-settings/route.tscan incorrectly relink all referred users whenaffectedUserIdsis empty, which can misapply promotions and create broad data integrity/regression impact.- Several medium-severity regressions are also present (calendar disconnect success shown on failed DELETE, workspace scoping missing in finance filter-users, and banner behavior changing on API errors), so this is not just a single isolated defect.
- Pay close attention to
apps/web/src/app/api/v1/workspaces/[wsId]/storage/object/route.ts,apps/web/src/app/api/v1/workspaces/[wsId]/promotions/referral-settings/route.ts,apps/web/src/app/api/v1/workspaces/[wsId]/finance/filter-users/route.ts- authorization, cross-workspace data scope, and incorrect bulk-linking behavior need to be fixed before relying on this safely.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/web/src/app/api/v1/workspaces/[wsId]/promotions/referral-settings/route.ts">
<violation number="1" location="apps/web/src/app/api/v1/workspaces/[wsId]/promotions/referral-settings/route.ts:81">
P1: When no users have the old promotion linked (`affectedUserIds` is empty), this falls back to linking **all** referred users to the new promotion — even those who never had the old one. If no one had the old promo, the migration should be a no-op rather than assigning the new promo to everyone.</violation>
</file>
<file name="apps/web/src/app/api/v1/workspaces/[wsId]/mail/route.ts">
<violation number="1" location="apps/web/src/app/api/v1/workspaces/[wsId]/mail/route.ts:13">
P2: Validate and cap `page`/`pageSize` before using them in `.range(...)` to avoid NaN errors and unbounded queries when clients pass invalid or huge values.</violation>
</file>
<file name="packages/ui/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx">
<violation number="1" location="packages/ui/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx:358">
P2: Check the DELETE responses for calendar connections and surface failures; otherwise a failed deletion still reports a successful disconnect.</violation>
</file>
<file name="apps/web/src/app/api/v1/workspaces/[wsId]/finance/filter-users/route.ts">
<violation number="1" location="apps/web/src/app/api/v1/workspaces/[wsId]/finance/filter-users/route.ts:23">
P2: Filter creator views by `ws_id` so the endpoint only returns users for the requested workspace.</violation>
</file>
<file name="apps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/permission-setup-banner.tsx">
<violation number="1" location="apps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/permission-setup-banner.tsx:26">
P2: Handle API errors in `queryFn` to preserve previous behavior; without a fallback, fetch failures can incorrectly show the permission setup banner.</violation>
</file>
<file name="apps/web/src/app/api/v1/workspaces/[wsId]/storage/object/route.ts">
<violation number="1" location="apps/web/src/app/api/v1/workspaces/[wsId]/storage/object/route.ts:25">
P1: Add a `manage_drive` permission check before deleting storage objects; otherwise any user with any workspace permission can delete files.</violation>
</file>
<file name="apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/role-members.tsx">
<violation number="1" location="apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/role-members.tsx:343">
P2: listWorkspaceMembers doesn’t provide full_name, but the new cast to WorkspaceUser[] makes the UI keep using user.full_name. That field will always be undefined, so full-name search and display regress for users without display_name. Update the mapping or the UI to align with InternalApiWorkspaceMember.</violation>
</file>
Architecture diagram
sequenceDiagram
participant UI as Client Component
participant SDK as @tuturuuu/internal-api
participant API as Next.js API Route
participant Perms as Workspace Helper (Auth)
participant DB as Supabase (Server-side)
Note over UI,DB: Refactored Data/Control Flow (Migration from Client-side Supabase to API Proxy)
UI->>SDK: NEW: call internal API helper (e.g. getTransactionStats)
SDK->>API: NEW: fetch("/api/v1/workspaces/{wsId}/...")
rect rgb(240, 240, 240)
Note right of API: Server-side Boundary
API->>Perms: NEW: getPermissions({ wsId, request })
Perms->>DB: Get user session & workspace roles
DB-->>Perms: session / role data
Perms-->>API: permissions object
end
alt Has Required Permissions
API->>API: Validate request body (Zod)
alt Database Operation
API->>DB: CHANGED: supabase.from(...).select/upsert()
DB-->>API: data / error
else Storage Operation (e.g. delete resource)
API->>DB: NEW: supabase.storage.from('workspaces').remove()
Note right of API: Uses Admin client for privileged deletes
DB-->>API: storage result
end
API-->>SDK: 200 OK (JSON response)
SDK-->>UI: Return typed data
else Unauthorized / Forbidden
API-->>SDK: 401 / 403 Error
SDK-->>UI: Throw error / Toast notification
end
Note over UI,SDK: UI updates state via React Query / router.refresh()
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
apps/web/src/app/api/v1/workspaces/[wsId]/promotions/referral-settings/route.ts
Show resolved
Hide resolved
packages/ui/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx
Show resolved
Hide resolved
apps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/permission-setup-banner.tsx
Show resolved
Hide resolved
apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/role-members.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 36
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (17)
apps/web/src/app/api/v1/workspaces/[wsId]/roles/route.ts (2)
11-13:⚠️ Potential issue | 🟠 MajorPass
requesttocreateClientto support mobile Bearer token auth.The handler uses
createClient()without the request parameter, which prevents mobile clients using Authorization headers from authenticating. This also affects the POST handler at line 33.🔧 Proposed fix
-export async function GET(_: Request, { params }: Params) { - const supabase = await createClient(); +export async function GET(request: Request, { params }: Params) { + const supabase = await createClient(request);And for the POST handler:
-export async function POST(req: Request, { params }: Params) { - const supabase = await createClient(); +export async function POST(req: Request, { params }: Params) { + const supabase = await createClient(req);As per coding guidelines: "For API routes that must serve web sessions and mobile Bearer tokens, initialize Supabase with
createClient(request)so Authorization headers are honored."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/api/v1/workspaces/`[wsId]/roles/route.ts around lines 11 - 13, The GET and POST route handlers call createClient() without the incoming Request, so mobile Bearer Authorization headers aren’t honored; update both handlers to call createClient(request) (i.e., pass the handler's Request object into createClient) so Supabase will accept Authorization headers for mobile auth, and keep the rest of the logic in the GET and POST functions (e.g., the existing params/wsId handling) unchanged.
15-19:⚠️ Potential issue | 🟡 MinorAdd
normalizeWorkspaceIdcall to handle "personal" workspace identifier.The route uses
wsIddirectly without normalization. ImportnormalizeWorkspaceIdfrom@tuturuuu/utils/workspace-helperand normalize the ID before querying, consistent with other workspace-scoped routes in the codebase.Current code (lines 15-19)
const { data, error } = await supabase .from('workspace_roles') .select('*') .eq('ws_id', wsId) .order('name', { ascending: true });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/api/v1/workspaces/`[wsId]/roles/route.ts around lines 15 - 19, The route currently uses the raw wsId when querying workspace_roles; import normalizeWorkspaceId from '@tuturuuu/utils/workspace-helper' and call it on wsId before the Supabase query (replace usages of wsId in the .eq('ws_id', ...) call with the normalized value), ensuring the normalized ID is used when building the query in the handler in route.ts.apps/web/src/app/api/v1/workspaces/[wsId]/roles/[roleId]/members/route.ts (2)
4-8:⚠️ Potential issue | 🔴 CriticalExtract
wsIdfrom params and verify role belongs to workspace.The route path includes
[wsId]but the handler only extractsroleId. Without verifying that the role belongs to the specified workspace, this could allow unauthorized cross-workspace access.🔧 Proposed fix
interface Params { params: Promise<{ + wsId: string; roleId: string; }>; } -export async function GET(_: Request, { params }: Params) { - const supabase = await createClient(); - const { roleId } = await params; +export async function GET(request: Request, { params }: Params) { + const supabase = await createClient(request); + const { wsId, roleId } = await params; const { data, error, count } = await supabase .from('workspace_role_members') .select( '...users!inner(id, display_name, full_name, avatar_url, ...user_private_details(email))', { count: 'exact', } ) - .eq('role_id', roleId); + .eq('role_id', roleId) + .eq('ws_id', wsId);As per coding guidelines: "For API routes that must serve web sessions and mobile Bearer tokens, initialize Supabase with
createClient(request)so Authorization headers are honored."Also applies to: 10-12
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/api/v1/workspaces/`[wsId]/roles/[roleId]/members/route.ts around lines 4 - 8, The Params interface and route handler currently only extract roleId; update Params to include wsId and in the route handler extract both params.params.wsId and params.params.roleId, initialize Supabase with createClient(request) so Authorization headers (web sessions and mobile Bearer tokens) are honored, then query the roles table (e.g., SELECT * FROM roles WHERE id = roleId) and verify the returned role's workspace_id (or ws_id) equals the extracted wsId before returning members; if the check fails, return a 403/404. Ensure you reference the Params interface and the route handler in route.ts and apply the same change to the related handlers noted in the file.
35-53:⚠️ Potential issue | 🟠 MajorPOST handler also needs
createClient(request)and workspace verification.The same issues apply to the POST handler: it should use
createClient(req)and verify the role belongs to the workspace before inserting members.🔧 Proposed fix
-export async function POST(req: Request, { params }: Params) { - const supabase = await createClient(); - const { roleId } = await params; +export async function POST(req: Request, { params }: Params) { + const supabase = await createClient(req); + const { wsId, roleId } = await params; + + // Verify role belongs to workspace + const { data: role, error: verifyError } = await supabase + .from('workspace_roles') + .select('id') + .eq('id', roleId) + .eq('ws_id', wsId) + .maybeSingle(); + + if (verifyError || !role) { + return NextResponse.json({ message: 'Role not found' }, { status: 404 }); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/api/v1/workspaces/`[wsId]/roles/[roleId]/members/route.ts around lines 35 - 53, Update the POST handler to call createClient(req) (not createClient()) and properly read params (extract wsId and roleId from params, don't await params), then verify the role belongs to the workspace by querying workspace_roles for role_id = roleId and workspace_id = wsId before inserting; if the role is missing return an appropriate 404/403 NextResponse. After verification, perform the insert into workspace_role_members (as currently done), handle and return any insertion errors (roleError) in the response, and return success when complete.apps/web/src/app/[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/linked-modules/page.tsx (1)
161-164:⚠️ Potential issue | 🟠 MajorBug: Recursive retry calls wrong function.
The retry logic calls
getData(wsId, ...)but should callgetModules(wsId, ...). This appears to be a pre-existing bug but will cause incorrect behavior on retry.🐛 Proposed fix
const { data, error, count } = await queryBuilder; if (error) { if (!retry) throw error; - return getData(wsId, { q, pageSize, retry: false }); + return getModules(wsId, { q, pageSize, retry: false }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/`[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/linked-modules/page.tsx around lines 161 - 164, The retry branch currently calls getData(wsId, { q, pageSize, retry: false }) but should call getModules(wsId, { q, pageSize, retry: false }) so retries invoke the correct fetch function; update the error handling block to call getModules instead of getData (referencing the getData and getModules identifiers) and keep the same parameters and retry: false behavior.apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/layout.tsx (1)
184-191: 🧹 Nitpick | 🔵 Trivial
createDynamicClientusage is flagged as deprecated.The
getResourcesfunction usescreateDynamicClient()which is deprecated per coding guidelines. Consider migrating this to use the internal API pattern in a follow-up.Based on learnings: "When
tuturuuu/supabase/next/clientorcreateDynamicClientis flagged as deprecated, migrate to an authenticated Internal API route and consume it throughtuturuuu/internal-api".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/`[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/layout.tsx around lines 184 - 191, getResources currently calls the deprecated createDynamicClient(); replace this client usage by calling an authenticated internal API route that performs the Supabase storage.list operation server-side. Create an internal API endpoint (e.g. /api/internal/workspace-resources) that accepts the path, uses the non-deprecated server-side Supabase client to list storage from 'workspaces', and returns the data/error; then update getResources to call that internal endpoint via the tuturuuu/internal-api fetch pattern instead of createDynamicClient. Ensure the endpoint handles auth and propagates errors so getResources can return the same data shape.apps/web/src/app/[locale]/(dashboard)/[wsId]/mail/client.tsx (1)
48-61: 🧹 Nitpick | 🔵 TrivialConsider using
useInfiniteQueryfor pagination.The
loadMorecallback performs client-side data fetching outside of TanStack Query. While this pattern is common for infinite scroll, TanStack Query'suseInfiniteQuerywould provide better caching, error handling, and state management for paginated data.♻️ Suggested approach using useInfiniteQuery
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ['workspace-emails', wsId], queryFn: ({ pageParam = 1 }) => listWorkspaceEmails(wsId, { page: pageParam, pageSize: PAGE_SIZE }), getNextPageParam: (lastPage, pages) => lastPage.length === PAGE_SIZE ? pages.length + 1 : undefined, initialData: { pages: [data], pageParams: [1] }, });As per coding guidelines: "All client-side fetching/mutation must use
useQuery/useMutationfrom TanStack Query."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/`[locale]/(dashboard)/[wsId]/mail/client.tsx around lines 48 - 61, Replace the manual loadMore handler that calls getWorkspaceMails and mutates setEmails/page/hasMore with a TanStack Query useInfiniteQuery implementation: create an infinite query keyed by ['workspace-emails', wsId] whose queryFn calls getWorkspaceMails(wsId, { page: pageParam, pageSize: PAGE_SIZE }), provide getNextPageParam that returns next page when lastPage.length === PAGE_SIZE, and wire fetchNextPage/hasNextPage/isFetchingNextPage to trigger loading more instead of calling loadMore; remove the client-side page/hasMore state and use the paginated data structure returned by useInfiniteQuery to render emails. Ensure you reference fetchNextPage where loadMore was used and use the query result pages to derive the flattened email list for rendering.packages/ui/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx (1)
4-5:⚠️ Potential issue | 🟠 MajorMigrate deprecated Supabase client and consolidate calendar disconnect into an internal API route.
This shared UI component (
packages/ui/) still uses the deprecatedcreateClient()at line 4 to callauth.getUser()within a useEffect hook (lines 92–104). Per the migration pattern, replace this browser Supabase call with an authenticated internal API helper that returns whether the current user is a Tuturuuu employee.Additionally, the disconnect flow at lines 346–367 performs a raw GET and then a fan-out DELETE sequence on individual calendar connections without consolidated error handling. The Promise.all on individual DELETE requests at line 362–367 does not validate that each DELETE succeeded, risking orphaned connection records if one or more fail. Consolidate the entire disconnect and cleanup into a single authenticated API route and consume it through the internal API pattern, which also simplifies error handling and transaction safety.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ui/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx` around lines 4 - 5, The component still imports and uses the deprecated createClient() to call auth.getUser() inside the useEffect and also performs per-connection GET + Promise.all DELETEs for calendar disconnects; replace the client-side auth.getUser() call with an authenticated internal API helper (e.g., an internal endpoint and helper like isEmployee/getCurrentUser for the existing useEffect) and call that from the component instead of createClient(); remove the per-connection fan-out DELETE logic and instead implement a single authenticated internal API route (e.g., calendar.disconnectAll or calendar.cleanupConnections) that performs the DELETEs transactionally and returns success/failure, then call that single route from the component’s disconnect handler and surface its error state—update references to createClient(), the useEffect that reads auth.getUser(), and the disconnect handler that currently uses Promise.all to use the new internal API helpers.packages/ui/src/components/ui/custom/education/modules/resources/delete-resource.tsx (1)
9-10:⚠️ Potential issue | 🟡 MinorThread
wsIdexplicitly intoDeleteResourceButtoninstead of deriving it from the path.The component receives a workspace-prefixed path (
${wsId}/courses/...) and extracts the workspace ID viapath.split('/')[0]. While this works in the current call site, it creates an implicit contract that all paths must be workspace-prefixed. SincewsIdis available at the call site (from route params), pass it explicitly for clarity and to align with the codebase pattern of threading workspace IDs explicitly through helper functions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ui/src/components/ui/custom/education/modules/resources/delete-resource.tsx` around lines 9 - 10, The component DeleteResourceButton currently derives wsId from the path via path.split('/')[0]; instead, add an explicit wsId prop (e.g., DeleteResourceButton({ wsId, path })) and stop extracting workspace id inside the component; update the component to use the passed wsId wherever workspace identification is needed (remove path.split usage) and change all call sites to pass the route param wsId (from useRouter or parent props) so the workspace id is threaded explicitly consistent with the codebase.apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/migrations/hooks/use-migration-state.ts (1)
338-374:⚠️ Potential issue | 🟠 MajorReplace this
useEffectwithuseQueryto eliminate the stale-response race condition.This effect performs client-side data fetching in
useEffect, which violates the mandatory standard. Additionally, whentargetWorkspaceIdchanges, an oldergetWorkspace(wsId)call can resolve after the newer fetch and overwrite the displayed name with stale data. Migrate to TanStack Query'suseQuerywith proper debouncing, or add a stale-response guard (e.g., capture and check the currenttargetWorkspaceIdbefore setting state).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/`[locale]/(dashboard)/[wsId]/(workspace-settings)/migrations/hooks/use-migration-state.ts around lines 338 - 374, Replace the client-side useEffect/fetchTargetWorkspaceName flow with a TanStack Query useQuery keyed on targetWorkspaceId to handle loading and caching (or, if you prefer to keep manual fetching, add a stale-response guard): remove the setTimeout/useEffect block and instead call useQuery({ queryKey: ['workspaceName', targetWorkspaceId], queryFn: () => getWorkspace(targetWorkspaceId), enabled: Boolean(targetWorkspaceId), staleTime: 0, refetchOnWindowFocus: false }) and derive setTargetWorkspaceName and setLoadingTargetWorkspaceName from the query state; if you choose the guard approach keep fetchTargetWorkspaceName/getWorkspace but capture the current targetWorkspaceId in a local variable before awaiting and verify it still equals the latest targetWorkspaceId before calling setTargetWorkspaceName or setLoadingTargetWorkspaceName to prevent stale-response overwrites.apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/content/content-editor.tsx (1)
26-41: 🧹 Nitpick | 🔵 TrivialConsider debouncing the save operation.
The
saveContentToDBfunction is called on everyonChangeevent, which fires on each keystroke. This can cause:
- Excessive API calls overwhelming the server
- Race conditions where rapid changes cause out-of-order saves
- Poor UX if the network is slow
♻️ Suggested debounce implementation
+import { useDebouncedCallback } from 'use-debounce'; import { useState } from 'react'; export function ModuleContentEditor({ wsId, courseId, moduleId, content }: Props) { const [post, setPost] = useState<JSONContent | null>(content || null); const t = useTranslations(); + const debouncedSave = useDebouncedCallback( + async (content: JSONContent | null) => { + try { + await updateWorkspaceCourseModule(wsId, moduleId, { + course_id: courseId, + content, + }); + } catch (error) { + console.log(error); + toast.error(t('common.error_saving_content')); + } + }, + 1000 // 1 second debounce + ); const onChange = (content: JSONContent | null) => { setPost(content); - saveContentToDB(content); + debouncedSave(content); }; - const saveContentToDB = async (content: JSONContent | null) => { - try { - await updateWorkspaceCourseModule(wsId, moduleId, { - course_id: courseId, - content, - }); - } catch (error) { - console.log(error); - toast.error(t('common.error_saving_content')); - } - };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/`[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/content/content-editor.tsx around lines 26 - 41, onChange currently calls saveContentToDB on every keystroke causing excessive API calls and potential race conditions; wrap the save logic in a debounced function (e.g., create a debouncedSaveContent using lodash.debounce or a useDebouncedCallback hook) and call that from onChange while still calling setPost immediately; implement the debounced function with useCallback/useRef so it preserves identity, cancel/flush it in a cleanup effect (on unmount) and consider flushing on explicit save or blur to ensure latest content is persisted; keep updateWorkspaceCourseModule, wsId, moduleId and courseId usage unchanged inside the debounced call.packages/ui/src/components/ui/finance/recurring/recurring-transactions-page.tsx (1)
294-294: 🧹 Nitpick | 🔵 TrivialConsider typing the upcoming transaction instead of using
any.The
transaction: anyparameter weakens type safety. Consider defining or reusing an interface for the upcoming transaction shape returned bylistUpcomingRecurringTransactions.-{upcomingTransactions.map((transaction: any, index: number) => ( +{upcomingTransactions.map((transaction, index) => (If the return type of
listUpcomingRecurringTransactionsis properly typed, TypeScript can infer the type here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ui/src/components/ui/finance/recurring/recurring-transactions-page.tsx` at line 294, The map callback currently types its parameter as "transaction: any", losing type safety; replace any by a concrete type (either create/reuse an interface such as UpcomingRecurringTransaction or use the return type from listUpcomingRecurringTransactions) so TypeScript can infer/validate fields used inside upcomingTransactions.map in recurring-transactions-page.tsx; update the upstream function signature for listUpcomingRecurringTransactions if needed or import the existing type and use it in the map callback and related variables.apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/role-members.tsx (1)
82-117: 🛠️ Refactor suggestion | 🟠 MajorMutation handlers still use raw
fetch()instead ofuseMutationwith internal API helpers.The read operations have been migrated to use
@tuturuuu/internal-apihelpers, buthandleAddMemberandhandleRemoveMemberstill use rawfetch()calls. This is inconsistent with the migration pattern and violates the coding guideline requiring TanStack Query for all client-side mutations.Consider creating internal API helpers like
addRoleMember(wsId, roleId, memberIds)andremoveRoleMember(wsId, roleId, memberId)in@tuturuuu/internal-api/roles, then wrap them withuseMutation.♻️ Suggested refactor pattern
+import { addRoleMember, removeRoleMember } from '@tuturuuu/internal-api/roles'; + +const addMemberMutation = useMutation({ + mutationFn: (memberId: string) => addRoleMember(wsId, roleId!, [memberId]), + onSuccess: () => { + toast.success(t('ws-roles.member_added_successfully')); + roleMembersQuery.refetch(); + setAddMemberQuery(''); + }, + onError: () => { + toast.error(t('ws-roles.failed_to_add_member')); + }, +}); + +const removeMemberMutation = useMutation({ + mutationFn: (memberId: string) => removeRoleMember(wsId, roleId!, memberId), + onSuccess: () => { + toast.success(t('ws-roles.member_removed_successfully')); + roleMembersQuery.refetch(); + }, + onError: () => { + toast.error(t('ws-roles.failed_to_remove_member')); + }, +}); - -const handleAddMember = async (memberId: string) => { - const res = await fetch( - `/api/v1/workspaces/${wsId}/roles/${roleId}/members`, - ... - ); - ... -}; - -const handleRemoveMember = async (memberId: string) => { - const res = await fetch( - `/api/v1/workspaces/${wsId}/roles/${roleId}/members/${memberId}`, - ... - ); - ... -};As per coding guidelines: "All client-side fetching/mutation must use
useQuery/useMutationfrom TanStack Query" and "When client or shared UI code needs app API access, add or extend helpers inpackages/internal-api/src/*".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/`[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/role-members.tsx around lines 82 - 117, Replace the raw fetch calls in handleAddMember and handleRemoveMember with TanStack Query mutations: add two API helpers (e.g., addRoleMember(wsId, roleId, memberIds) and removeRoleMember(wsId, roleId, memberId) in `@tuturuuu/internal-api/roles`) and then use useMutation to call those helpers from this component; on success call roleMembersQuery.refetch() (and show the same toast messages), and on error show the existing error toasts—update handleAddMember and handleRemoveMember to trigger the corresponding mutations instead of using fetch().packages/ui/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx (1)
25-44:⚠️ Potential issue | 🟡 MinorSilent error handling provides no user feedback on failure.
The function catches errors and logs to console but provides no user-facing feedback. Consider adding a toast notification on failure:
♻️ Suggested improvement
+import { toast } from '@tuturuuu/ui/sonner'; + const updateYoutubeLinks = async ( moduleId: string, courseId: string, links: string[] ) => { setLoading(true); try { await updateWorkspaceCourseModule(wsId, moduleId, { course_id: courseId, youtube_links: links, }); router.refresh(); setLoading(false); - return null; } catch (error) { console.error('error', error); + toast.error('Failed to delete link'); setLoading(false); - return null; } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ui/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx` around lines 25 - 44, The updateYoutubeLinks function currently swallows errors by only logging and returning null; update it to show user-facing feedback by invoking the project's toast/notification utility on failure (and optionally on success). Specifically, inside the catch block of updateYoutubeLinks (which calls updateWorkspaceCourseModule and router.refresh), replace or augment console.error with a call to the app's toast (or notification) API with a clear error message and perhaps error.message, ensure setLoading(false) still runs, and consider a success toast after a successful update before or after router.refresh so users get visible confirmation.packages/ui/src/components/ui/legacy/calendar/settings/hour-settings.tsx (2)
66-90:⚠️ Potential issue | 🟡 MinorMissing state rollback on API failure for optimistic updates.
The handler updates local state optimistically before the API call, but doesn't restore the previous value if the update fails. The user sees the new value even after an error toast.
Proposed fix pattern
const handlePersonalHoursChange = async ( newHours?: WeekTimeRanges | null ) => { if (!newHours) { toast.error('No hours provided'); return; } + const previousHours = value.personalHours; setValue((prev) => ({ ...prev, personalHours: newHours, })); try { await updateWorkspaceCalendarHours(wsId, { type: 'PERSONAL', hours: newHours, }); toast.success('Personal hours updated'); } catch (error) { console.error('Error updating personal hours:', error); toast.error('Failed to update personal hours'); + setValue((prev) => ({ + ...prev, + personalHours: previousHours, + })); return; } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ui/src/components/ui/legacy/calendar/settings/hour-settings.tsx` around lines 66 - 90, The handler handlePersonalHoursChange performs an optimistic update via setValue before calling updateWorkspaceCalendarHours(wsId, ...), but on API failure it never restores the previous personalHours; capture the previous state (prev.personalHours) before calling setValue, then in the catch block call setValue to rollback to that captured value and still show the error toast/console.error; ensure the captured value is referenced inside the async flow so the state is restored only on failure.
36-60:⚠️ Potential issue | 🟠 MajorReplace
useEffectdata fetching with TanStack Query.The component uses
useEffectfor fetching calendar hours, which violates the project's data fetching guidelines. Migrate touseQueryfor better caching and state management.As per coding guidelines: "NEVER use useEffect for data fetching. TanStack Query (useQuery/useMutation) is the mandatory standard."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ui/src/components/ui/legacy/calendar/settings/hour-settings.tsx` around lines 36 - 60, The useEffect-based fetchHours should be replaced with a TanStack Query hook: remove the useEffect and fetchHours, and instead call useQuery (e.g., useQuery(['workspaceCalendarHours', wsId], () => getWorkspaceCalendarHours(wsId))) to load data; on success map the returned data into setValue (use the query's onSuccess) and on error show the toast.error and console.error via the query's onError or error state; ensure you reference getWorkspaceCalendarHours, setValue, and wsId and use the query key ['workspaceCalendarHours', wsId] so caching/invalidations work correctly.apps/web/src/app/[locale]/(dashboard)/[wsId]/posts/filters.tsx (1)
36-55:⚠️ Potential issue | 🟠 MajorReplace
useEffectdata fetching with TanStack Query.Using
useEffectfor data fetching violates project guidelines. Migrate touseQueryfor proper caching, error handling, and loading states.Proposed refactor using useQuery
-import { useEffect, useState } from 'react'; +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; // In the component: - const [userGroups, setUserGroups] = useState<UserGroup[]>([]); - const [excludedUserGroups, setExcludedUserGroups] = useState<UserGroup[]>([]); - const [users, setUsers] = useState<WorkspaceUser[]>([]); - - useEffect(() => { - const loadData = async () => { - try { - const [userGroupsData, excludedGroupsData, usersData] = - await Promise.all([ - getUserGroups(wsId), - getExcludedUserGroups(wsId, searchParams), - getUsers(wsId), - ]); - - setUserGroups(userGroupsData.data); - setExcludedUserGroups(excludedGroupsData.data); - setUsers(usersData.data); - } catch (error) { - console.error('Failed to load filter data:', error); - } - }; - - loadData(); - }, [wsId, searchParams]); + const includedGroupsParam = Array.isArray(searchParams.includedGroups) + ? searchParams.includedGroups + : searchParams.includedGroups + ? [searchParams.includedGroups] + : []; + + const { data } = useQuery({ + queryKey: ['posts-filter-options', wsId, includedGroupsParam], + queryFn: async () => { + const [base, excluded] = await Promise.all([ + getPostsFilterOptions(wsId), + getPostsFilterOptions(wsId, { includedGroups: includedGroupsParam }), + ]); + return { + userGroups: base.userGroups as UserGroup[], + excludedUserGroups: excluded.excludedUserGroups as UserGroup[], + users: base.users as WorkspaceUser[], + }; + }, + }); + + const userGroups = data?.userGroups ?? []; + const excludedUserGroups = data?.excludedUserGroups ?? []; + const users = data?.users ?? [];As per coding guidelines: "NEVER use useEffect for data fetching. TanStack Query (useQuery/useMutation) is the mandatory standard."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/`[locale]/(dashboard)/[wsId]/posts/filters.tsx around lines 36 - 55, Replace the useEffect/loadData pattern with TanStack Query hooks: remove the useEffect and the loadData async function and instead create useQuery (or useQueries) hooks that call getUserGroups(wsId), getExcludedUserGroups(wsId, searchParams), and getUsers(wsId); map their results into setUserGroups/setExcludedUserGroups/setUsers or preferably derive state directly from query.data, handle loading/error states from each query, and ensure each query key includes wsId and searchParams so caching/invalidation works correctly; keep function names getUserGroups, getExcludedUserGroups, getUsers and state setters (or replace them) and ensure queries refetch when wsId or searchParams change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1605837b-d647-4a82-8eb5-c218411be7ff
📒 Files selected for processing (65)
apps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/components/mira-gateway-models.tsapps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/components/mira-model-selector/use-mira-model-selector-data.tsapps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/permission-setup-banner.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/migrations/hooks/use-migration-state.tsapps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/index.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/role-members.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/content/content-editor.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/content/page.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/flashcards/client-flashcards.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/layout.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/quizzes/client-quizzes.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/youtube-links/page.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/linked-modules/linker.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/linked-modules/page.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/linked-modules/row-actions.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/inventory/promotions/settings-form.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/mail/client.tsxapps/web/src/app/[locale]/(dashboard)/[wsId]/posts/filters.tsxapps/web/src/app/api/v1/workspaces/[wsId]/ai/model-favorites/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/finance/filter-users/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/finance/recurring-transactions/[recurringTransactionId]/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/finance/recurring-transactions/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/finance/recurring-transactions/upcoming/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/mail/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/posts/filter-options/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/promotions/referral-settings/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/quiz-sets/[setId]/modules/[moduleId]/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/quiz-sets/[setId]/modules/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/roles/[roleId]/members/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/roles/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/settings/permissions/check/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/settings/permissions/setup-status/route.tsapps/web/src/app/api/v1/workspaces/[wsId]/storage/object/route.tsapps/web/src/app/api/workspaces/[wsId]/transactions/category-breakdown/route.tsapps/web/src/app/api/workspaces/[wsId]/transactions/spending-trends/route.tsapps/web/src/lib/calendar-preferences-provider.tsxpackages/internal-api/package.jsonpackages/internal-api/src/ai.tspackages/internal-api/src/education.tspackages/internal-api/src/finance.tspackages/internal-api/src/index.tspackages/internal-api/src/mail.tspackages/internal-api/src/promotions.tspackages/internal-api/src/roles.tspackages/internal-api/src/settings.tspackages/internal-api/src/users.tspackages/ui/src/components/ui/custom/education/modules/module-toggle.tsxpackages/ui/src/components/ui/custom/education/modules/resources/delete-resource.tsxpackages/ui/src/components/ui/custom/education/modules/resources/file-display.tsxpackages/ui/src/components/ui/custom/education/modules/youtube/delete-link-button.tsxpackages/ui/src/components/ui/finance/analytics/category-spending-chart.tsxpackages/ui/src/components/ui/finance/analytics/spending-trends-chart.tsxpackages/ui/src/components/ui/finance/recurring/form.tsxpackages/ui/src/components/ui/finance/recurring/recurring-transactions-page.tsxpackages/ui/src/components/ui/finance/transactions/category-filter.tsxpackages/ui/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsxpackages/ui/src/components/ui/finance/transactions/period-charts/category-donut-chart.tsxpackages/ui/src/components/ui/finance/transactions/period-charts/period-breakdown-panel.tsxpackages/ui/src/components/ui/finance/transactions/user-filter.tsxpackages/ui/src/components/ui/finance/wallets/walletId/wallet-role-access.tsxpackages/ui/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsxpackages/ui/src/components/ui/legacy/calendar/settings/hour-settings.tsxpackages/ui/src/hooks/use-workspace-permission.tspackages/ui/src/hooks/use-workspace-user.tsscripts/internal-api-migration.test.js
Description
What?
Why?
How?
Screenshots for proof (must have)
Summary by cubic
Migrates client-side Supabase calls to server-backed endpoints and
@tuturuuu/internal-apihelpers. Centralizes auth/permission checks and standardizes data access across AI, education, finance, roles, mail, posts, promotions, settings, storage, and users, with improved type safety in calendar hour settings.Refactors
@tuturuuu/internal-apimodules:ai,education,finance,mail,promotions,roles,settings,users,storage(new exports inpackages/internal-api)./ai/model-favoritesand@tuturuuu/internal-api/aito list/toggle favorites.getCategoryBreakdown/getSpendingTrends; recurring transactions use create/update/list/delete/upcoming APIs; filters use transaction categories and filter-users APIs.updateWorkspaceCourseModule, link/unlink APIs, and storage signed URLs/deletes; components now receivewsIdwhere needed.use-workspace-permission/use-workspace-userusesettings/usersAPIs; calendar preferences/hours usesettingsAPIs with improved type safety inHoursSettings; wallet role access usesrolesAPI.@tuturuuu/supabase/next/clientfrom client components; expanded tests to block deprecated imports.Migration
@tuturuuu/supabase/next/clientin client code; use@tuturuuu/internal-api/*helpers.wsIdto updated components:ModuleToggles,ModuleContentEditor,DeleteLinkButton,QuizsetModuleLinker.Written for commit 67f39c4. Summary will update on new commits. Review in cubic