mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 08:53:11 +00:00
Replaces the checkbox-list + "Salvar assessores" button in the Assessors popover (/campanha/municipios) with removable chips + a Command combobox, delta-based auto-save (one advisor add/remove per request, never the whole array), and optimistic UI with request sequencing to resolve out-of-order responses correctly. - New POST /campanha/municipios/advisors JSON route (mirrors expected-votes/), backed by setMunicipalityAdvisorMembership(Record) in actions/municipality.ts (text advisory lock + transaction). - nextAdvisorIdsAfterMembership moved to src/lib/municipalityAdvisorMembership.ts with unit tests. - Shared isSameOriginRequest (src/utilities/sameOriginRequest.ts) and uniqueRelationshipIds (src/utilities/relationship.ts) helpers, de-duplicating the same-origin CSRF check and the Set+map(relationshipId)+filter pattern across advisor call sites. - MunicipalityListAdvisorsControl.tsx rewritten: chips, Command combobox, per-item pending affordance, rollback on error, and a request-sequence guard so only the most recently sent delta's response is adopted when concurrent optimistic updates settle. - e2e coverage for the combobox auto-save flow; setup prewarms the new POST-only route to avoid Playwright dev-server HMR aborting the first request. - Plan and roadmap marked as delivered (B27).
26 lines
1.1 KiB
TypeScript
26 lines
1.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { nextAdvisorIdsAfterMembership } from '@/lib/municipalityAdvisorMembership'
|
|
import { MAX_ADVISORS_PER_MUNICIPALITY } from '@/lib/schemas/municipality'
|
|
|
|
describe('nextAdvisorIdsAfterMembership', () => {
|
|
it('adds an advisor not yet present', () => {
|
|
expect(nextAdvisorIdsAfterMembership([1, 2], 3, true)).toEqual([1, 2, 3])
|
|
})
|
|
|
|
it('removes an advisor present in the list', () => {
|
|
expect(nextAdvisorIdsAfterMembership([1, 2, 3], 2, false)).toEqual([1, 3])
|
|
})
|
|
|
|
it('is a no-op (returns null) when already in the desired state', () => {
|
|
expect(nextAdvisorIdsAfterMembership([1, 2], 2, true)).toBeNull()
|
|
expect(nextAdvisorIdsAfterMembership([1, 2], 3, false)).toBeNull()
|
|
})
|
|
|
|
it('throws once the municipality is at the advisor cap', () => {
|
|
const atCap = Array.from({ length: MAX_ADVISORS_PER_MUNICIPALITY }, (_, index) => index + 1)
|
|
expect(() => nextAdvisorIdsAfterMembership(atCap, 999, true)).toThrow(
|
|
`Cada município aceita no máximo ${MAX_ADVISORS_PER_MUNICIPALITY} assessores.`,
|
|
)
|
|
})
|
|
})
|