The "Municípios" column was dead text; it is now chips that navigate to the município, with a territory chip when a leadership covers a whole TI, and it is editable in place. What picks inline vs. Drawer is the `pointer:` media query, not viewport width: on a fine pointer the editor is always mounted (the "×" sits at the chip's corner, revealed on hover/focus) and on a coarse pointer a tap anywhere in the cell opens a Drawer with 44 px targets. `AdvisorMunicipalityCell` is promoted to `shared/MunicipalityPortfolioCell` and /campanha/assessores migrates onto it in the same change, so the second call site is born with the first. One batch action only; the single-id advisor twin is deleted with its int tests migrated. The anti-goal on always-mounted list inputs is amended for chip-based relations on fine pointers, not reopened. Also fixes a latent bug: `CampaignTableHead`'s `description` seam broke any RSC caller, because its two helpers lived in a 'use client' module.
215 lines
7.8 KiB
TypeScript
215 lines
7.8 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import type { Payload } from 'payload'
|
|
import { getPayload } from 'payload'
|
|
import { beforeAll, describe, expect, it } from 'vitest'
|
|
|
|
import { setLeadershipMunicipalitiesMembershipRecord } from '@/app/(campaign)/campanha/actions/leadership'
|
|
import config from '@/payload.config'
|
|
|
|
import { installCampaignFixtures } from '../helpers/campaignFixtures'
|
|
|
|
let payload: Payload
|
|
const campaignFixtures = installCampaignFixtures({
|
|
getPayload: () => payload,
|
|
setPayload: (nextPayload) => {
|
|
payload = nextPayload
|
|
},
|
|
})
|
|
|
|
const municipalityIds = (leadership: {
|
|
municipalities?: (number | { id: number })[] | null
|
|
}): number[] =>
|
|
(leadership.municipalities ?? []).map((entry) => (typeof entry === 'number' ? entry : entry.id))
|
|
|
|
describe('setLeadershipMunicipalitiesMembershipRecord (B34)', () => {
|
|
beforeAll(async () => {
|
|
payload = await getPayload({ config: await config })
|
|
})
|
|
|
|
it.each(['coordinator', 'candidate'] as const)(
|
|
'lets a %s add and remove municipalities by delta',
|
|
async (role) => {
|
|
const fixtures = campaignFixtures()
|
|
const actor = await fixtures.createCampaignUser(role)
|
|
const home = await fixtures.getMunicipality()
|
|
const added = await fixtures.getMunicipality()
|
|
const contact = await fixtures.createContact()
|
|
const leadership = await fixtures.createLeadership({
|
|
contact,
|
|
municipalities: [home],
|
|
})
|
|
|
|
const { leadership: assigned, municipalitySlugs } =
|
|
await setLeadershipMunicipalitiesMembershipRecord(payload, actor, {
|
|
leadershipId: leadership.id,
|
|
municipalityIds: [added.id],
|
|
assigned: true,
|
|
})
|
|
expect(municipalityIds(assigned)).toEqual([home.id, added.id])
|
|
expect(municipalitySlugs).toEqual([added.slug])
|
|
|
|
const { leadership: removed } = await setLeadershipMunicipalitiesMembershipRecord(
|
|
payload,
|
|
actor,
|
|
{
|
|
leadershipId: leadership.id,
|
|
municipalityIds: [added.id],
|
|
assigned: false,
|
|
},
|
|
)
|
|
expect(municipalityIds(removed)).toEqual([home.id])
|
|
},
|
|
)
|
|
|
|
it('adds a whole batch (território / zona) in one write', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const home = await fixtures.getMunicipality()
|
|
const first = await fixtures.getMunicipality()
|
|
const second = await fixtures.getMunicipality()
|
|
const contact = await fixtures.createContact()
|
|
const leadership = await fixtures.createLeadership({ contact, municipalities: [home] })
|
|
|
|
const { leadership: assigned, municipalitySlugs } =
|
|
await setLeadershipMunicipalitiesMembershipRecord(payload, coordinator, {
|
|
leadershipId: leadership.id,
|
|
municipalityIds: [first.id, second.id],
|
|
assigned: true,
|
|
})
|
|
|
|
expect(municipalityIds(assigned)).toEqual([home.id, first.id, second.id])
|
|
expect(municipalitySlugs).toEqual(expect.arrayContaining([first.slug, second.slug]))
|
|
})
|
|
|
|
it('refuses to remove the last municipality of a leadership', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const municipality = await fixtures.getMunicipality()
|
|
const contact = await fixtures.createContact()
|
|
const leadership = await fixtures.createLeadership({
|
|
contact,
|
|
municipalities: [municipality],
|
|
})
|
|
|
|
await expect(
|
|
setLeadershipMunicipalitiesMembershipRecord(payload, coordinator, {
|
|
leadershipId: leadership.id,
|
|
municipalityIds: [municipality.id],
|
|
assigned: false,
|
|
}),
|
|
).rejects.toThrow(/pelo menos um município/i)
|
|
|
|
const unchanged = await payload.findByID({
|
|
collection: 'leadership',
|
|
id: leadership.id,
|
|
depth: 0,
|
|
})
|
|
expect(municipalityIds(unchanged)).toEqual([municipality.id])
|
|
})
|
|
|
|
it('lets an advisor add a municipality they administer', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const advisor = await fixtures.createCampaignUser('advisor')
|
|
const home = await fixtures.getMunicipality()
|
|
const alsoAdministered = await fixtures.getMunicipality()
|
|
await fixtures.assignMunicipalityAdvisors(home, [advisor])
|
|
await fixtures.assignMunicipalityAdvisors(alsoAdministered, [advisor])
|
|
const contact = await fixtures.createContact()
|
|
const leadership = await fixtures.createLeadership({ contact, municipalities: [home] })
|
|
|
|
const { leadership: assigned } = await setLeadershipMunicipalitiesMembershipRecord(
|
|
payload,
|
|
advisor,
|
|
{
|
|
leadershipId: leadership.id,
|
|
municipalityIds: [alsoAdministered.id],
|
|
assigned: true,
|
|
},
|
|
)
|
|
expect(municipalityIds(assigned)).toEqual([home.id, alsoAdministered.id])
|
|
})
|
|
|
|
it('denies an advisor adding a municipality outside their portfolio, but lets them remove one', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const advisor = await fixtures.createCampaignUser('advisor')
|
|
const administered = await fixtures.getMunicipality()
|
|
const outside = await fixtures.getMunicipality()
|
|
await fixtures.assignMunicipalityAdvisors(administered, [advisor])
|
|
const contact = await fixtures.createContact()
|
|
// Cross-boundary leadership: the advisor administers only one of its municipalities.
|
|
const leadership = await fixtures.createLeadership({
|
|
contact,
|
|
municipalities: [administered, outside],
|
|
})
|
|
|
|
const stranger = await fixtures.getMunicipality()
|
|
await expect(
|
|
setLeadershipMunicipalitiesMembershipRecord(payload, advisor, {
|
|
leadershipId: leadership.id,
|
|
municipalityIds: [stranger.id],
|
|
assigned: true,
|
|
}),
|
|
).rejects.toThrow(/municípios que assessora/i)
|
|
|
|
// Removal needs no scope beyond the row access that resolved the leadership —
|
|
// this is what a whole-array replace would break for a cross-boundary row.
|
|
const { leadership: removed } = await setLeadershipMunicipalitiesMembershipRecord(
|
|
payload,
|
|
advisor,
|
|
{
|
|
leadershipId: leadership.id,
|
|
municipalityIds: [outside.id],
|
|
assigned: false,
|
|
},
|
|
)
|
|
expect(municipalityIds(removed)).toEqual([administered.id])
|
|
})
|
|
|
|
it('denies a leader actor', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const leaderActor = await fixtures.createCampaignUser('leader')
|
|
const municipality = await fixtures.getMunicipality()
|
|
const other = await fixtures.getMunicipality()
|
|
const contact = await fixtures.createContact()
|
|
const leadership = await fixtures.createLeadership({
|
|
contact,
|
|
municipalities: [municipality],
|
|
})
|
|
|
|
await expect(
|
|
setLeadershipMunicipalitiesMembershipRecord(payload, leaderActor, {
|
|
leadershipId: leadership.id,
|
|
municipalityIds: [other.id],
|
|
assigned: true,
|
|
}),
|
|
).rejects.toThrow(/coordenação e a assessoria/i)
|
|
})
|
|
|
|
it('is a no-op when the municipalities are already in the desired state', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const municipality = await fixtures.getMunicipality()
|
|
const absent = await fixtures.getMunicipality()
|
|
const contact = await fixtures.createContact()
|
|
const leadership = await fixtures.createLeadership({
|
|
contact,
|
|
municipalities: [municipality],
|
|
})
|
|
|
|
const { municipalitySlugs: afterAdd } = await setLeadershipMunicipalitiesMembershipRecord(
|
|
payload,
|
|
coordinator,
|
|
{ leadershipId: leadership.id, municipalityIds: [municipality.id], assigned: true },
|
|
)
|
|
// No write happened, so the caller has nothing to revalidate.
|
|
expect(afterAdd).toEqual([])
|
|
|
|
const { municipalitySlugs: afterRemove } = await setLeadershipMunicipalitiesMembershipRecord(
|
|
payload,
|
|
coordinator,
|
|
{ leadershipId: leadership.id, municipalityIds: [absent.id], assigned: false },
|
|
)
|
|
expect(afterRemove).toEqual([])
|
|
})
|
|
})
|