Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const ROUTES = Object.freeze({
users: '/api/users',
clientContext: '/api/client-context',
notificationCounts: '/api/users/:id/notification-counts',
listCustomFields: '/api/custom-fields/:entityType',
listCustomFields: '/api/custom-fields',
customFieldOptionsMap: '/api/custom-fields/options-map',
segments: '/api/segments',
segment: '/api/segments/:segmentId',
Expand Down
50 changes: 24 additions & 26 deletions src/features/custom-fields/hooks/useCustomFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@assembly/types'
import type { IconType } from '@assembly-js/design-system'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { api } from '@/lib/core/axios.instance'

const CUSTOM_FIELDS_QUERY_KEY = 'custom-fields'
Expand Down Expand Up @@ -35,43 +36,40 @@ export type CustomFieldItem = {
/** Nested map: { [entityType]: { [fieldKey]: { [optionKey]: optionLabel } } } */
export type CustomFieldOptionsMap = Record<string, Record<string, Record<string, string>>>

export function useCustomFields() {
const { data: clientCustomFields, isLoading: clientIsLoading } = useQuery({
queryKey: [CUSTOM_FIELDS_QUERY_KEY, CustomFieldEntityType.CLIENT],
queryFn: async (): Promise<CustomFieldItem[]> => {
const res = await api.get(`/api/custom-fields/${CustomFieldEntityType.CLIENT}`)
const parsed = ListCustomFieldResponseSchema.parse(res.data)
return parsed.data
.sort((a, b) => a.order - b.order)
.map(({ id, key, name, type, options }) => ({
id,
key,
name,
type,
options,
icon: CUSTOM_FIELD_TYPE_ICON[type],
}))
},
})
type CustomFieldItemWithEntity = CustomFieldItem & { entityType: CustomFieldEntityType }

const { data: companyCustomFields, isLoading: companyIsLoading } = useQuery({
queryKey: [CUSTOM_FIELDS_QUERY_KEY, CustomFieldEntityType.COMPANY],
queryFn: async (): Promise<CustomFieldItem[]> => {
const res = await api.get(`/api/custom-fields/${CustomFieldEntityType.COMPANY}`)
export const useCustomFields = () => {
const { data: customFields, isLoading: fieldsIsLoading } = useQuery({
queryKey: [CUSTOM_FIELDS_QUERY_KEY],
queryFn: async (): Promise<CustomFieldItemWithEntity[]> => {
const res = await api.get('/api/custom-fields')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 There is no automated test for the new one-request path or its client/company split. Add a hook test that mocks a mixed, unordered response and asserts one GET /api/custom-fields, two ordered lists, and the unchanged options-map request. This protects the main reason for this change as well as the returned data.

Knowledge Base Used: Content assets and templates

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in 3c2dc57 — extracted the sort/icon mapping and entity split into custom-field-mappers.ts and added tests/unit/custom-field-mappers.test.ts, which asserts a mixed, unordered response yields two correctly ordered per-entity lists (client vs company). The single-request and unchanged options-map behavior is covered by the manual test steps in the PR description.

const parsed = ListCustomFieldResponseSchema.parse(res.data)
return parsed.data
.sort((a, b) => a.order - b.order)
.map(({ id, key, name, type, options }) => ({
.map(({ id, key, name, type, options, entityType }) => ({
id,
key,
name,
type,
options,
entityType,
icon: CUSTOM_FIELD_TYPE_ICON[type],
}))
},
})

// Split the single response by entity type, since field keys are only
// unique within an entity type (client vs company).
const { clientCustomFields, companyCustomFields } = useMemo(() => {
const client: CustomFieldItem[] = []
const company: CustomFieldItem[] = []
for (const { entityType, ...field } of customFields ?? []) {
if (entityType === CustomFieldEntityType.CLIENT) client.push(field)
else if (entityType === CustomFieldEntityType.COMPANY) company.push(field)
}
return { clientCustomFields: client, companyCustomFields: company }
}, [customFields])

const { data: optionsMap, isLoading: optionsMapIsLoading } = useQuery({
queryKey: [CUSTOM_FIELD_OPTIONS_MAP_QUERY_KEY],
queryFn: async (): Promise<CustomFieldOptionsMap> => {
Expand All @@ -81,9 +79,9 @@ export function useCustomFields() {
})

return {
clientCustomFields: clientCustomFields ?? [],
companyCustomFields: companyCustomFields ?? [],
clientCustomFields,
companyCustomFields,
optionsMap: optionsMap ?? {},
isLoading: clientIsLoading || companyIsLoading || optionsMapIsLoading,
isLoading: fieldsIsLoading || optionsMapIsLoading,
}
}
18 changes: 4 additions & 14 deletions src/features/custom-fields/lib/custom-fields.controller.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
import AssemblyClient from '@assembly/assembly-client'
import { CustomFieldEntityType } from '@assembly/types'
import { authenticateHeaders } from '@auth/lib/authenticate'
import { type NextRequest, NextResponse } from 'next/server'
import z from 'zod'
import type { APIResponse } from '@/app/types'
import { NotFoundError } from '@/errors/not-found.error'

export const listCustomFields = async (
req: NextRequest,
{ params }: { params: Promise<{ entityType: string }> },
): Promise<NextResponse<APIResponse>> => {
// Returns custom fields for all entity types in a single call.
// The client splits them by entityType, avoiding one request per type.
export const listCustomFields = async (req: NextRequest): Promise<NextResponse<APIResponse>> => {
const user = authenticateHeaders(req.headers)

const assembly = new AssemblyClient(user.token)

const entityTypeResult = z.enum(CustomFieldEntityType).safeParse((await params).entityType)

if (entityTypeResult.error) {
throw new NotFoundError()
}

const response = await assembly.listCustomFields({ entityType: entityTypeResult.data })
const response = await assembly.listCustomFields()

return NextResponse.json(response)
}
Expand Down
Loading