Merge pull request #97 from fsolla/cursor/pool-i42-980ec2b6-2133

C11 — Escala planos (picker pós-C12 + paginação das demandas vinculadas)
This commit is contained in:
Francisco Solla 2026-08-01 04:16:27 -03:00 committed by GitHub
commit 269fb79f37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 353 additions and 158 deletions

View file

@ -2,6 +2,7 @@ import { PlusIcon } from 'lucide-react'
import Link from 'next/link'
import { ActivityResultForm } from '@/components/campaign/activity/ActivityResultForm'
import { CampaignListPagination } from '@/components/campaign/shared/CampaignListPagination'
import { Badge } from '@/components/ui/Badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
@ -17,6 +18,12 @@ const currencyFormatter = new Intl.NumberFormat('pt-BR', {
type ActivityDetailView = Awaited<ReturnType<typeof getActivityDetailPageData>>
const hrefForLinkedDemandsPage = (activitySlug: string, page: number) => {
const params = new URLSearchParams({ tab: 'overview' })
if (page > 1) params.set('demandsPage', String(page))
return `/campanha/atividades/${activitySlug}?${params.toString()}`
}
/** The "Visão geral" tab: details, team/orgs, linked demands and the result block. */
export const ActivityOverviewTab = ({
view,
@ -128,47 +135,67 @@ export const ActivityOverviewTab = ({
<CardTitle>Demandas vinculadas</CardTitle>
<p className="text-sm text-muted-foreground">
Custo estimado: {currencyFormatter.format(view.demandCostTotal)}
{view.totalDocs > 0 ? (
<>
{' '}
· {view.totalDocs} {view.totalDocs === 1 ? 'demanda' : 'demandas'}
</>
) : null}
</p>
</div>
{view.municipality ? (
<Button asChild variant="outline" className="min-h-11 shrink-0">
<Link
href={`/campanha/demandas/nova?activity=${view.id}&municipality=${view.municipality.id}`}
>
<PlusIcon data-icon="inline-start" aria-hidden="true" />
Adicionar demanda
</Link>
</Button>
) : null}
</CardHeader>
<CardContent>
{view.demands.length ? (
<ul role="list" className="divide-y">
{view.demands.map((demand) => (
<li
key={demand.id}
className="flex flex-col gap-2 py-3 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between"
<div className="flex shrink-0 flex-wrap gap-2">
{view.totalDocs > view.demands.length ? (
<Button asChild variant="ghost" className="min-h-11">
<Link href={`/campanha/demandas?activity=${view.id}`}>Ver todas</Link>
</Button>
) : null}
{view.municipality ? (
<Button asChild variant="outline" className="min-h-11 shrink-0">
<Link
href={`/campanha/demandas/nova?activity=${view.id}&municipality=${view.municipality.id}`}
>
<div className="min-w-0">
<Link
href={`/campanha/demandas/${demand.slug}`}
className="font-medium text-primary underline-offset-4 hover:underline"
>
{demand.title}
</Link>
<p className="text-sm text-muted-foreground">
{campaignDemandKindLabels[demand.kind]} ·{' '}
{campaignDemandStatusLabels[demand.status]}
</p>
</div>
<span className="text-sm text-muted-foreground">
{demand.cost == null
? 'Custo não registrado'
: currencyFormatter.format(demand.cost)}
</span>
</li>
))}
</ul>
<PlusIcon data-icon="inline-start" aria-hidden="true" />
Adicionar demanda
</Link>
</Button>
) : null}
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{view.demands.length ? (
<>
<ul role="list" className="divide-y">
{view.demands.map((demand) => (
<li
key={demand.id}
className="flex flex-col gap-2 py-3 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between"
>
<div className="min-w-0">
<Link
href={`/campanha/demandas/${demand.slug}`}
className="font-medium text-primary underline-offset-4 hover:underline"
>
{demand.title}
</Link>
<p className="text-sm text-muted-foreground">
{campaignDemandKindLabels[demand.kind]} ·{' '}
{campaignDemandStatusLabels[demand.status]}
</p>
</div>
<span className="text-sm text-muted-foreground">
{demand.cost == null
? 'Custo não registrado'
: currencyFormatter.format(demand.cost)}
</span>
</li>
))}
</ul>
<CampaignListPagination
page={view.page}
totalPages={view.totalPages}
hrefForPage={(page) => hrefForLinkedDemandsPage(view.slug, page)}
/>
</>
) : (
<p className="text-sm text-muted-foreground">
Nenhuma demanda vinculada a esta atividade.

View file

@ -29,6 +29,7 @@ import {
resolveAccessibleActivityContext,
} from '@/utilities/activityPageData'
import { isCampaignStaff } from '@/utilities/campaignAccess'
import { firstValue, strictDecimalInteger } from '@/utilities/campaignListUrl'
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
import { ActivityOverviewTab } from './ActivityOverviewTab'
@ -57,7 +58,13 @@ export default async function ActivityDetailPage({
throw error
},
)
const view = await getActivityDetailPageData(payload, user, context, activeTab)
const view = await getActivityDetailPageData(
payload,
user,
context,
activeTab,
strictDecimalInteger(firstValue(query.demandsPage)) ?? 1,
)
const canonicalTabRedirect = getActivityDetailTabRedirect(view.slug, query)
if (canonicalTabRedirect) redirect(canonicalTabRedirect)

View file

@ -0,0 +1,20 @@
'use server'
import config from '@payload-config'
import { getPayload } from 'payload'
import type { AsyncSearchOption } from '@/components/campaign/shared/AsyncSearchCombobox'
import { searchActivityRelationOptions } from '@/utilities/activityRelationOptions'
import { getCampaignUser } from '@/utilities/campaignAuth'
import { CAMPAIGN_AUTH_REQUIRED_MESSAGE } from '@/utilities/campaignFormActionError'
export const searchDemandActivityOptions = async (
query: string,
municipalityId: number | null,
): Promise<AsyncSearchOption[]> => {
const [payload, user] = await Promise.all([getPayload({ config }), getCampaignUser()])
if (!user) throw new Error(CAMPAIGN_AUTH_REQUIRED_MESSAGE)
const options = await searchActivityRelationOptions(payload, user, query, municipalityId)
return options.map((option) => ({ id: option.id, label: option.label }))
}

View file

@ -3,12 +3,14 @@ import { ArrowLeftIcon } from 'lucide-react'
import Link from 'next/link'
import { getPayload } from 'payload'
import { searchDemandActivityOptions } from '@/app/(campaign)/campanha/(app)/demandas/activitySearchActions'
import { DemandForm } from '@/components/campaign/demand/DemandForm'
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
import { Button } from '@/components/ui/button'
import { loadActivityRelationOptionById } from '@/utilities/activityRelationOptions'
import { firstValue, strictDecimalInteger } from '@/utilities/campaignListUrl'
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
import { loadActivityOptions, loadMunicipalityOptions } from '@/utilities/campaignRelationOptions'
import { loadMunicipalityOptions } from '@/utilities/campaignRelationOptions'
import { createDemandFormAction } from './formActions'
type NewDemandPageProps = {
@ -22,16 +24,16 @@ export default async function NewDemandPage({ searchParams }: NewDemandPageProps
searchParams,
])
const [municipalityOptions, activityOptions] = await Promise.all([
loadMunicipalityOptions(payload, user),
loadActivityOptions(payload, user),
])
const requestedActivity = activityOptions.find(
(activity) => activity.id === strictDecimalInteger(firstValue(query.activity)),
)
const requestedActivityId = strictDecimalInteger(firstValue(query.activity))
const requestedMunicipalityId = strictDecimalInteger(firstValue(query.municipality))
const [municipalityOptions, initialActivity] = await Promise.all([
loadMunicipalityOptions(payload, user),
requestedActivityId
? loadActivityRelationOptionById(payload, user, requestedActivityId)
: Promise.resolve(null),
])
const initialMunicipalityId =
requestedActivity?.municipalityId ??
initialActivity?.municipalityId ??
municipalityOptions.find((option) => option.id === requestedMunicipalityId)?.id
return (
@ -52,9 +54,9 @@ export default async function NewDemandPage({ searchParams }: NewDemandPageProps
<DemandForm
municipalityOptions={municipalityOptions}
activityOptions={activityOptions}
initialActivity={initialActivity}
initialMunicipalityId={initialMunicipalityId}
initialActivityId={requestedActivity?.id}
searchActivities={searchDemandActivityOptions}
formAction={createDemandFormAction}
/>
</CampaignPageShell>

View file

@ -1,7 +1,8 @@
'use client'
import { useActionState, useState } from 'react'
import { useActionState, useCallback, useState } from 'react'
import { AsyncSearchCombobox } from '@/components/campaign/shared/AsyncSearchCombobox'
import { CampaignFormActionMessage } from '@/components/campaign/shared/CampaignFormActionMessage'
import type { RelationOption } from '@/components/campaign/shared/RelationMultiSelect'
import { Button } from '@/components/ui/button'
@ -11,15 +12,18 @@ import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
import { Spinner } from '@/components/ui/Spinner'
import { Textarea } from '@/components/ui/textarea'
import { campaignDemandKindLabels, campaignDemandKinds } from '@/lib/schemas/campaignDemand'
import type { ActivityRelationOption } from '@/utilities/activityRelationOptions'
import type { CampaignFormActionState } from '@/utilities/campaignFormActionError'
import { fieldError } from '@/utilities/campaignFormFields'
import type { ActivityRelationOption } from '@/utilities/campaignRelationOptions'
type DemandFormProps = {
municipalityOptions: RelationOption[]
activityOptions: ActivityRelationOption[]
initialActivity?: ActivityRelationOption | null
initialMunicipalityId?: number
initialActivityId?: number
searchActivities: (
query: string,
municipalityId: number | null,
) => Promise<Array<{ id: number; label: string }>>
formAction: (
state: CampaignFormActionState,
formData: FormData,
@ -28,19 +32,32 @@ type DemandFormProps = {
export const DemandForm = ({
municipalityOptions,
activityOptions,
initialActivity = null,
initialMunicipalityId,
initialActivityId,
searchActivities,
formAction,
}: DemandFormProps) => {
const [state, submitAction, isPending] = useActionState(formAction, {})
const [municipalityId, setMunicipalityId] = useState(
initialMunicipalityId ? String(initialMunicipalityId) : '',
)
const [activityId, setActivityId] = useState(initialActivityId ? String(initialActivityId) : '')
const visibleActivities = municipalityId
? activityOptions.filter((activity) => String(activity.municipalityId) === municipalityId)
: activityOptions
const [activity, setActivity] = useState<{ id: number; label: string } | null>(
initialActivity ? { id: initialActivity.id, label: initialActivity.label } : null,
)
const parsedMunicipalityId = municipalityId ? Number(municipalityId) : null
const searchActivitiesForMunicipality = useCallback(
(query: string) => searchActivities(query, parsedMunicipalityId),
[parsedMunicipalityId, searchActivities],
)
const activityQueryReady = useCallback(
() => parsedMunicipalityId !== null,
[parsedMunicipalityId],
)
const activityFieldDescription = parsedMunicipalityId
? 'Opcional. Busque atividades do município selecionado.'
: 'Opcional. Escolha o município antes de vincular uma atividade.'
return (
<form action={submitAction} className="flex max-w-2xl flex-col gap-4">
@ -82,17 +99,8 @@ export const DemandForm = ({
required
value={municipalityId}
onChange={(event) => {
const nextMunicipalityId = event.target.value
setMunicipalityId(nextMunicipalityId)
const selectedActivity = activityOptions.find(
(activity) => String(activity.id) === activityId,
)
if (
selectedActivity &&
String(selectedActivity.municipalityId) !== nextMunicipalityId
) {
setActivityId('')
}
setMunicipalityId(event.target.value)
setActivity(null)
}}
className="min-h-11 w-full"
>
@ -112,31 +120,29 @@ export const DemandForm = ({
</div>
<Field>
<FieldLabel htmlFor="demand-activity">Atividade relacionada</FieldLabel>
<NativeSelect
id="demand-activity"
name="activityId"
value={activityId}
onChange={(event) => {
const nextActivityId = event.target.value
setActivityId(nextActivityId)
const selectedActivity = activityOptions.find(
(activity) => String(activity.id) === nextActivityId,
)
if (selectedActivity) setMunicipalityId(String(selectedActivity.municipalityId))
}}
className="min-h-11 w-full"
>
<NativeSelectOption value="">Nenhuma atividade</NativeSelectOption>
{visibleActivities.map((option) => (
<NativeSelectOption key={option.id} value={String(option.id)}>
{option.name}
</NativeSelectOption>
))}
</NativeSelect>
<FieldDescription>
Opcional. Ao escolher uma atividade, o município correspondente é preenchido
automaticamente.
</FieldDescription>
{parsedMunicipalityId ? (
<AsyncSearchCombobox
name="activityId"
label="Atividade relacionada"
value={activity}
emptyOptionLabel="Nenhuma atividade"
dialogDescription="Busque atividades do município selecionado por título."
isQueryReady={activityQueryReady}
queryTooShortMessage="Selecione o município para buscar atividades."
search={searchActivitiesForMunicipality}
onChange={setActivity}
/>
) : (
<Button
type="button"
variant="outline"
disabled
className="min-h-11 w-full justify-between font-normal opacity-70"
>
Selecione o município primeiro…
</Button>
)}
<FieldDescription>{activityFieldDescription}</FieldDescription>
{fieldError(state.fieldErrors, 'activity') ? (
<FieldError>{fieldError(state.fieldErrors, 'activity')}</FieldError>
) : null}

View file

@ -13,6 +13,8 @@ import {
type ActivityMunicipalitySummary,
} from '@/utilities/activityViewModels'
export const ACTIVITY_LINKED_DEMANDS_PAGE_SIZE = 10
type ActivityDemandSummary = {
id: number
title: string
@ -22,6 +24,22 @@ type ActivityDemandSummary = {
cost: number | null
}
export type ActivityLinkedDemandsPage = {
demands: ActivityDemandSummary[]
page: number
totalPages: number
totalDocs: number
demandCostTotal: number
}
const emptyLinkedDemandsPage = (): ActivityLinkedDemandsPage => ({
demands: [],
page: 1,
totalPages: 1,
totalDocs: 0,
demandCostTotal: 0,
})
const loadActivityUpdateAuthorNames = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
@ -75,17 +93,63 @@ const loadActivityMunicipalitySummaryById = async (
: null
}
const loadLinkedDemandsPage = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
activityId: number,
page: number,
): Promise<ActivityLinkedDemandsPage> => {
const where = { activity: { equals: activityId } }
const [pageResult, costRows] = await Promise.all([
payload.find({
collection: 'campaignDemand',
where,
depth: 0,
limit: ACTIVITY_LINKED_DEMANDS_PAGE_SIZE,
page,
sort: '-createdAt',
select: { title: true, slug: true, kind: true, status: true, cost: true },
user,
overrideAccess: false,
}),
payload.find({
collection: 'campaignDemand',
where,
depth: 0,
limit: 0,
pagination: false,
select: { cost: true },
user,
overrideAccess: false,
}),
])
const demands = pageResult.docs.map((demand) => ({
id: demand.id,
title: demand.title,
slug: demand.slug,
kind: demand.kind,
status: demand.status,
cost: demand.cost ?? null,
}))
return {
demands,
page: pageResult.page ?? page,
totalPages: pageResult.totalPages,
totalDocs: pageResult.totalDocs,
demandCostTotal: costRows.docs.reduce((total, demand) => total + (demand.cost ?? 0), 0),
}
}
export const getActivityDetailPageData = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
context: AccessibleActivityContext,
activeTab: ActivityDetailTab,
): Promise<
ActivityDetailViewModel & {
demands: ActivityDemandSummary[]
demandCostTotal: number
}
> => {
linkedDemandsPage = 1,
): Promise<ActivityDetailViewModel & ActivityLinkedDemandsPage> => {
const municipalityId = relationshipId(context.document.municipality)
const populatedMunicipalitySummary =
activityMunicipalitySummary(context.document.municipality) ?? null
@ -98,39 +162,18 @@ export const getActivityDetailPageData = async (
activeTab === 'updates' && context.document.updates?.length
? loadActivityUpdateAuthorNames(payload, user, context.document.updates)
: Promise.resolve(new Map<number, string>())
const demandSummariesPromise: Promise<ActivityDemandSummary[]> =
const linkedDemandsPromise =
activeTab === 'overview'
? payload
.find({
collection: 'campaignDemand',
where: { activity: { equals: context.id } },
depth: 0,
pagination: false,
sort: 'createdAt',
select: { title: true, slug: true, kind: true, status: true, cost: true },
user,
overrideAccess: false,
})
.then(({ docs }) =>
docs.map((demand) => ({
id: demand.id,
title: demand.title,
slug: demand.slug,
kind: demand.kind,
status: demand.status,
cost: demand.cost ?? null,
})),
)
: Promise.resolve([])
const [municipalitySummary, authorNames, demands] = await Promise.all([
? loadLinkedDemandsPage(payload, user, context.id, linkedDemandsPage)
: Promise.resolve(emptyLinkedDemandsPage())
const [municipalitySummary, authorNames, linkedDemands] = await Promise.all([
municipalitySummaryPromise,
authorNamesPromise,
demandSummariesPromise,
linkedDemandsPromise,
])
return {
...toActivityDetailViewModel(context.document, activeTab, authorNames, municipalitySummary),
demands,
demandCostTotal: demands.reduce((total, demand) => total + (demand.cost ?? 0), 0),
...linkedDemands,
}
}

View file

@ -9,7 +9,7 @@ const helpers = createDetailTabHelpers<ActivityDetailTab>({
tabs: activityDetailTabs,
defaultTab: 'overview',
tabQueryKeys: {
overview: [],
overview: ['demandsPage'],
tasks: [],
updates: ['newUpdate'],
},

View file

@ -0,0 +1,85 @@
import 'server-only'
import type { Payload } from 'payload'
import { relationshipId } from '@/lib/relationship'
import type { CampaignUser } from '@/payload-types'
export type ActivityRelationOption = {
id: number
label: string
municipalityId: number
}
export const ACTIVITY_RELATION_SEARCH_LIMIT = 20
const MIN_ACTIVITY_TITLE_QUERY_LENGTH = 2
const isActivityTitleQueryReady = (query: string) =>
query.trim().length === 0 || query.trim().length >= MIN_ACTIVITY_TITLE_QUERY_LENGTH
const toActivityRelationOption = (activity: {
id: number
title: string
municipality: unknown
}): ActivityRelationOption | null => {
const municipalityId = relationshipId(activity.municipality)
if (!municipalityId) return null
return { id: activity.id, label: activity.title, municipalityId }
}
export const loadActivityRelationOptionById = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
activityId: number,
): Promise<ActivityRelationOption | null> => {
const result = await payload.find({
collection: 'activity',
where: { id: { equals: activityId } },
depth: 0,
limit: 1,
pagination: false,
select: { title: true, municipality: true },
user,
overrideAccess: false,
})
const activity = result.docs[0]
return activity ? toActivityRelationOption(activity) : null
}
/**
* Server-backed activity picker for demand forms — scoped to one municipality when set.
* Empty query returns recent activities in that municipality; typed query filters by title.
*/
export const searchActivityRelationOptions = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
query: string,
municipalityId: number | null,
): Promise<ActivityRelationOption[]> => {
if (!municipalityId || !isActivityTitleQueryReady(query)) return []
const trimmed = query.trim().slice(0, 120)
const result = await payload.find({
collection: 'activity',
where: {
and: [
{ municipality: { equals: municipalityId } },
...(trimmed ? [{ title: { contains: trimmed } }] : []),
],
},
depth: 0,
limit: ACTIVITY_RELATION_SEARCH_LIMIT,
page: 1,
sort: 'title',
select: { title: true, municipality: true },
user,
overrideAccess: false,
})
return result.docs
.map((activity) => toActivityRelationOption(activity))
.filter((option): option is ActivityRelationOption => option !== null)
}

View file

@ -38,11 +38,13 @@ export type DemandListState = {
page: number
status?: CampaignDemandStatus
kind?: CampaignDemandKind
activityId?: number
}
export const parseDemandListParams = (searchParams: RawSearchParams): DemandListState => {
const rawStatus = firstValue(searchParams.status)
const rawKind = firstValue(searchParams.kind)
const activityId = strictDecimalInteger(firstValue(searchParams.activity))
return {
page: strictDecimalInteger(firstValue(searchParams.page)) ?? 1,
@ -52,6 +54,7 @@ export const parseDemandListParams = (searchParams: RawSearchParams): DemandList
...(campaignDemandKinds.includes(rawKind as CampaignDemandKind)
? { kind: rawKind as CampaignDemandKind }
: {}),
...(activityId ? { activityId } : {}),
}
}
@ -62,6 +65,7 @@ const buildDemandListSearchParams = (
const params = new URLSearchParams()
if (state.status) params.set('status', state.status)
if (state.kind) params.set('kind', state.kind)
if (state.activityId) params.set('activity', String(state.activityId))
if (page > 1) params.set('page', String(page))
return params
}
@ -141,6 +145,7 @@ export const loadDemandListPageData = async (
and: [
...(state.status ? [{ status: { equals: state.status } }] : []),
...(state.kind ? [{ kind: { equals: state.kind } }] : []),
...(state.activityId ? [{ activity: { equals: state.activityId } }] : []),
],
},
depth: 0,

View file

@ -3,13 +3,9 @@ import 'server-only'
import type { Payload } from 'payload'
import type { RelationOption } from '@/components/campaign/shared/RelationMultiSelect'
import { populatedContactName, relationshipId } from '@/lib/relationship'
import { populatedContactName } from '@/lib/relationship'
import type { CampaignUser } from '@/payload-types'
export type ActivityRelationOption = RelationOption & {
municipalityId: number
}
/** Municipalities the actor may operate on (coordinator: all 436; advisor: administered). */
export const loadMunicipalityOptions = async (
payload: Payload,
@ -47,28 +43,6 @@ export const loadOrganizationOptions = async (
return result.docs.map((organization) => ({ id: organization.id, name: organization.name }))
}
export const loadActivityOptions = async (
payload: Payload,
user: CampaignUser,
): Promise<ActivityRelationOption[]> => {
const result = await payload.find({
collection: 'activity',
depth: 0,
limit: 0,
pagination: false,
sort: 'title',
select: { title: true, municipality: true },
where: {},
user,
overrideAccess: false,
})
return result.docs.flatMap((activity) => {
const municipalityId = relationshipId(activity.municipality)
return municipalityId ? [{ id: activity.id, name: activity.title, municipalityId }] : []
})
}
/**
* `name` folds in the party for select/typeahead disambiguation (e.g.
* `RelationMultiSelect`); `plainName`/`party`/`slug` ride along so cell-level

View file

@ -42,7 +42,8 @@ test.describe('Atividades — registro-fundação', () => {
await expect(page.getByRole('link', { name: materialDemand })).toBeVisible()
await page.getByRole('link', { name: 'Adicionar demanda' }).click()
await expect(page.getByLabel('Atividade relacionada')).toHaveValue(/\d+/)
await expect(page.locator('input[name="activityId"]')).toHaveValue(/\d+/)
await expect(page.getByLabel('Atividade relacionada')).toContainText(activityTitle)
await expect(page.getByLabel('Município')).toHaveValue(String(municipality.id))
await page.goto(`${campaign.baseURL}/campanha/municipios/${municipality.slug}?tab=updates`)

View file

@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest'
import { ACTIVITY_LINKED_DEMANDS_PAGE_SIZE } from '@/utilities/activityDetailPageData'
import { ACTIVITY_RELATION_SEARCH_LIMIT } from '@/utilities/activityRelationOptions'
import { parseDemandListParams } from '@/utilities/campaignDemandData'
describe('activityRelationOptions', () => {
it('caps search results at the configured limit', () => {
expect(ACTIVITY_RELATION_SEARCH_LIMIT).toBe(20)
})
})
describe('activity linked demands scale (C11)', () => {
it('uses a bounded page size for overview cards', () => {
expect(ACTIVITY_LINKED_DEMANDS_PAGE_SIZE).toBe(10)
})
it('parses activity filter on the demand list', () => {
expect(parseDemandListParams({ activity: '42', page: '2' })).toEqual({
page: 2,
activityId: 42,
})
})
})

View file

@ -366,6 +366,7 @@ describe('src/utilities top-level is pinned', () => {
'activityFormData.ts',
'activityLeadershipOptions.ts',
'activityPageData.ts',
'activityRelationOptions.ts',
'activityUi.ts',
'activityViewModels.ts',
'advisorData.ts',