fix(miss-52): one shared Drawer for the chip-relation lists too
The B42 fix reached the municípios mobile cards, but the three chip-relation surfaces still mounted a Drawer root per OPENED cell on coarse pointers (lideranças 50 cells, dobradinhas 50, assessores 25): the exact hundreds-of-hidden-instances class that tripped React #130 in prod. Each now wraps its table in CampaignListSheetProvider — cell bodies portal into the single host Drawer, fine-pointer popover path untouched (behavior delta: after opening N cells sequentially on touch, N drawer roots -> 1).
This commit is contained in:
parent
bfbb0ec713
commit
6f42bc3bf3
3 changed files with 276 additions and 259 deletions
|
|
@ -10,6 +10,7 @@ import {
|
|||
CampaignListResults,
|
||||
CampaignTransitionAnchor,
|
||||
} from '@/components/campaign/shared/CampaignListPending'
|
||||
import { CampaignListSheetProvider } from '@/components/campaign/shared/CampaignListSheetHost'
|
||||
import {
|
||||
CampaignTable,
|
||||
CampaignTableHead,
|
||||
|
|
@ -249,14 +250,18 @@ export default async function StateDeputiesPage({ searchParams }: StateDeputiesP
|
|||
<p className="text-sm text-muted-foreground" aria-live="polite">
|
||||
{sortSummary}
|
||||
</p>
|
||||
<CampaignTable
|
||||
caption={`${sortSummary}. Deputados estaduais com quem a campanha dobra.`}
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
rows={rows}
|
||||
rowKey={(row) => row.id}
|
||||
empty={<StateDeputyListEmptyState state={state} />}
|
||||
/>
|
||||
{/* One shared Drawer for every chip-cell sheet on coarse pointers
|
||||
(miss #52 — never a Drawer root per opened cell). */}
|
||||
<CampaignListSheetProvider>
|
||||
<CampaignTable
|
||||
caption={`${sortSummary}. Deputados estaduais com quem a campanha dobra.`}
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
rows={rows}
|
||||
rowKey={(row) => row.id}
|
||||
empty={<StateDeputyListEmptyState state={state} />}
|
||||
/>
|
||||
</CampaignListSheetProvider>
|
||||
{rows.length ? (
|
||||
<CampaignListFooter
|
||||
totalDocs={totalDocs}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
CampaignListResults,
|
||||
CampaignTransitionAnchor,
|
||||
} from '@/components/campaign/shared/CampaignListPending'
|
||||
import { CampaignListSheetProvider } from '@/components/campaign/shared/CampaignListSheetHost'
|
||||
import {
|
||||
CampaignTable,
|
||||
CampaignTableHead,
|
||||
|
|
@ -402,14 +403,18 @@ export default async function LeadershipsPage({ searchParams }: LeadershipsPageP
|
|||
<p className="text-sm text-muted-foreground" aria-live="polite">
|
||||
{sortSummary}
|
||||
</p>
|
||||
<CampaignTable
|
||||
caption={`${sortSummary}. Uma ficha por pessoa — cada liderança pode atuar em vários municípios e organizações.`}
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
rows={rows}
|
||||
rowKey={(row) => row.id}
|
||||
empty={<LeadershipListEmptyState state={state} />}
|
||||
/>
|
||||
{/* One shared Drawer for every chip-cell sheet on coarse pointers
|
||||
(miss #52 — never a Drawer root per opened cell). */}
|
||||
<CampaignListSheetProvider>
|
||||
<CampaignTable
|
||||
caption={`${sortSummary}. Uma ficha por pessoa — cada liderança pode atuar em vários municípios e organizações.`}
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
rows={rows}
|
||||
rowKey={(row) => row.id}
|
||||
empty={<LeadershipListEmptyState state={state} />}
|
||||
/>
|
||||
</CampaignListSheetProvider>
|
||||
{rows.length ? (
|
||||
<CampaignListFooter
|
||||
totalDocs={totalDocs}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
campaignReadCellClassName,
|
||||
} from '@/components/campaign/shared/CampaignCopyableCell'
|
||||
import { CampaignListEmptyState } from '@/components/campaign/shared/CampaignListEmptyState'
|
||||
import { CampaignListSheetProvider } from '@/components/campaign/shared/CampaignListSheetHost'
|
||||
import { MunicipalityPortfolioCell } from '@/components/campaign/shared/MunicipalityPortfolioCell'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
|
@ -155,223 +156,120 @@ export const AdvisorsTable = ({
|
|||
const showEmpty = rows.length === 0 && !draft
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
{editing ? (
|
||||
<Button type="button" variant="outline" className="min-h-11" onClick={leaveEditing}>
|
||||
Concluir edição
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="min-h-11"
|
||||
onClick={() => setEditing(true)}
|
||||
>
|
||||
<PencilIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Editar nome e contato
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" className="min-h-11" onClick={startDraft} disabled={Boolean(draft)}>
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Novo assessor
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showEmpty ? (
|
||||
<CampaignListEmptyState
|
||||
icon={hasQuery ? SearchXIcon : UserCogIcon}
|
||||
title={hasQuery ? 'Nenhum assessor encontrado' : 'Nenhum assessor cadastrado'}
|
||||
description={
|
||||
hasQuery
|
||||
? 'Ajuste a busca ou limpe o filtro para ver todos.'
|
||||
: 'Ative a edição e crie a conta na tabela.'
|
||||
}
|
||||
>
|
||||
{!hasQuery ? (
|
||||
<Button type="button" className="min-h-11" onClick={startDraft}>
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Novo assessor
|
||||
// One shared Drawer for the portfolio chip cells on coarse pointers
|
||||
// (miss #52 — never a Drawer root per opened cell).
|
||||
<CampaignListSheetProvider>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
{editing ? (
|
||||
<Button type="button" variant="outline" className="min-h-11" onClick={leaveEditing}>
|
||||
Concluir edição
|
||||
</Button>
|
||||
) : null}
|
||||
</CampaignListEmptyState>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-xl border">
|
||||
<Table className="min-w-[56rem] table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20%]">Nome</TableHead>
|
||||
<TableHead className="w-[24%]">E-mail</TableHead>
|
||||
<TableHead className="w-[14%]">Celular</TableHead>
|
||||
<TableHead>Municípios</TableHead>
|
||||
<TableHead className="w-28 text-right">
|
||||
<span className="sr-only">Ações</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{draft ? (
|
||||
<TableRow className="bg-muted/20">
|
||||
<TableCell>
|
||||
<Input
|
||||
value={draft.name}
|
||||
onChange={(event) => {
|
||||
const name = event.currentTarget.value
|
||||
setDraft((current) => (current ? { ...current, name } : current))
|
||||
}}
|
||||
placeholder="Nome"
|
||||
aria-label="Nome do novo assessor"
|
||||
className={campaignInlineInputClassName}
|
||||
autoFocus
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input
|
||||
type="email"
|
||||
value={draft.email}
|
||||
onChange={(event) => {
|
||||
const email = event.currentTarget.value
|
||||
setDraft((current) => (current ? { ...current, email } : current))
|
||||
}}
|
||||
placeholder="E-mail"
|
||||
aria-label="E-mail do novo assessor"
|
||||
className={campaignInlineInputClassName}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input
|
||||
type="tel"
|
||||
inputMode="numeric"
|
||||
value={draft.phone}
|
||||
onChange={(event) => {
|
||||
const phone = formatBrazilianPhoneInput(
|
||||
sanitizeBrazilianPhoneInput(event.currentTarget.value),
|
||||
)
|
||||
setDraft((current) => (current ? { ...current, phone } : current))
|
||||
}}
|
||||
placeholder="Celular"
|
||||
aria-label="Celular do novo assessor"
|
||||
className={campaignInlineInputClassName}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MunicipalityPortfolioCell
|
||||
ownerId={null}
|
||||
ownerName={draft.name.trim() || 'um novo assessor'}
|
||||
municipalityIds={draft.municipalityIds}
|
||||
municipalityIndex={municipalityIndex}
|
||||
draft
|
||||
onDraftChange={(municipalityIds) =>
|
||||
setDraft((current) => (current ? { ...current, municipalityIds } : current))
|
||||
}
|
||||
commitAction={municipalitiesAction}
|
||||
drawerTitle="Carteira do assessor"
|
||||
updateErrorMessage="Não foi possível atualizar a carteira."
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-10"
|
||||
aria-label="Cancelar novo assessor"
|
||||
disabled={isCreating}
|
||||
onClick={cancelDraft}
|
||||
>
|
||||
<XIcon className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="size-10"
|
||||
aria-label="Salvar novo assessor"
|
||||
disabled={isCreating}
|
||||
onClick={saveDraft}
|
||||
>
|
||||
{isCreating ? (
|
||||
<Spinner className="size-4" aria-hidden="true" />
|
||||
) : (
|
||||
<SaveIcon className="size-4" aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="min-h-11"
|
||||
onClick={() => setEditing(true)}
|
||||
>
|
||||
<PencilIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Editar nome e contato
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" className="min-h-11" onClick={startDraft} disabled={Boolean(draft)}>
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Novo assessor
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showEmpty ? (
|
||||
<CampaignListEmptyState
|
||||
icon={hasQuery ? SearchXIcon : UserCogIcon}
|
||||
title={hasQuery ? 'Nenhum assessor encontrado' : 'Nenhum assessor cadastrado'}
|
||||
description={
|
||||
hasQuery
|
||||
? 'Ajuste a busca ou limpe o filtro para ver todos.'
|
||||
: 'Ative a edição e crie a conta na tabela.'
|
||||
}
|
||||
>
|
||||
{!hasQuery ? (
|
||||
<Button type="button" className="min-h-11" onClick={startDraft}>
|
||||
<PlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Novo assessor
|
||||
</Button>
|
||||
) : null}
|
||||
</CampaignListEmptyState>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-xl border">
|
||||
<Table className="min-w-[56rem] table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20%]">Nome</TableHead>
|
||||
<TableHead className="w-[24%]">E-mail</TableHead>
|
||||
<TableHead className="w-[14%]">Celular</TableHead>
|
||||
<TableHead>Municípios</TableHead>
|
||||
<TableHead className="w-28 text-right">
|
||||
<span className="sr-only">Ações</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
) : null}
|
||||
|
||||
{rows.map((row) => {
|
||||
const whatsAppHref = whatsAppHrefForPhone(row.phone)
|
||||
const emailShown = displayEmail(row.email)
|
||||
|
||||
return (
|
||||
<TableRow key={row.id}>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{draft ? (
|
||||
<TableRow className="bg-muted/20">
|
||||
<TableCell>
|
||||
{editing ? (
|
||||
<AdvisorDebouncedTextCell
|
||||
advisorId={row.id}
|
||||
field="name"
|
||||
defaultValue={row.name}
|
||||
ariaLabel={`Nome de ${row.name}`}
|
||||
formAction={updateProfileAction}
|
||||
/>
|
||||
) : (
|
||||
<Link
|
||||
href={`/campanha/assessores/${row.id}`}
|
||||
className={cn(
|
||||
campaignReadCellClassName,
|
||||
'font-medium text-primary underline-offset-4 hover:underline',
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{row.name}</span>
|
||||
</Link>
|
||||
)}
|
||||
<Input
|
||||
value={draft.name}
|
||||
onChange={(event) => {
|
||||
const name = event.currentTarget.value
|
||||
setDraft((current) => (current ? { ...current, name } : current))
|
||||
}}
|
||||
placeholder="Nome"
|
||||
aria-label="Nome do novo assessor"
|
||||
className={campaignInlineInputClassName}
|
||||
autoFocus
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{editing ? (
|
||||
<AdvisorDebouncedTextCell
|
||||
advisorId={row.id}
|
||||
field="email"
|
||||
type="email"
|
||||
defaultValue={row.email ?? ''}
|
||||
placeholder="E-mail"
|
||||
ariaLabel={`E-mail de ${row.name}`}
|
||||
placeholderEmailFallback={row.email}
|
||||
formAction={updateProfileAction}
|
||||
/>
|
||||
) : (
|
||||
<CampaignCopyableCell value={emailShown} label="E-mail" />
|
||||
)}
|
||||
<Input
|
||||
type="email"
|
||||
value={draft.email}
|
||||
onChange={(event) => {
|
||||
const email = event.currentTarget.value
|
||||
setDraft((current) => (current ? { ...current, email } : current))
|
||||
}}
|
||||
placeholder="E-mail"
|
||||
aria-label="E-mail do novo assessor"
|
||||
className={campaignInlineInputClassName}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{editing ? (
|
||||
<AdvisorDebouncedTextCell
|
||||
advisorId={row.id}
|
||||
field="phone"
|
||||
type="tel"
|
||||
defaultValue={row.phone ? formatBrazilianPhoneInput(row.phone) : ''}
|
||||
placeholder="Celular"
|
||||
ariaLabel={`Celular de ${row.name}`}
|
||||
formAction={updateProfileAction}
|
||||
/>
|
||||
) : (
|
||||
<CampaignCopyableCell
|
||||
value={row.phone}
|
||||
label="Celular"
|
||||
displayValue={
|
||||
row.phone ? formatBrazilianPhoneInput(row.phone) : undefined
|
||||
}
|
||||
className="tabular-nums"
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
type="tel"
|
||||
inputMode="numeric"
|
||||
value={draft.phone}
|
||||
onChange={(event) => {
|
||||
const phone = formatBrazilianPhoneInput(
|
||||
sanitizeBrazilianPhoneInput(event.currentTarget.value),
|
||||
)
|
||||
setDraft((current) => (current ? { ...current, phone } : current))
|
||||
}}
|
||||
placeholder="Celular"
|
||||
aria-label="Celular do novo assessor"
|
||||
className={campaignInlineInputClassName}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MunicipalityPortfolioCell
|
||||
ownerId={row.id}
|
||||
ownerName={row.name}
|
||||
municipalityIds={municipalityIdsByAdvisor.get(row.id) ?? []}
|
||||
ownerId={null}
|
||||
ownerName={draft.name.trim() || 'um novo assessor'}
|
||||
municipalityIds={draft.municipalityIds}
|
||||
municipalityIndex={municipalityIndex}
|
||||
draft
|
||||
onDraftChange={(municipalityIds) =>
|
||||
setDraft((current) =>
|
||||
current ? { ...current, municipalityIds } : current,
|
||||
)
|
||||
}
|
||||
commitAction={municipalitiesAction}
|
||||
drawerTitle="Carteira do assessor"
|
||||
updateErrorMessage="Não foi possível atualizar a carteira."
|
||||
|
|
@ -379,45 +277,154 @@ export const AdvisorsTable = ({
|
|||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
{whatsAppHref ? (
|
||||
<Button asChild variant="ghost" size="icon" className="size-10">
|
||||
<a
|
||||
href={whatsAppHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`Enviar WhatsApp para ${row.name}`}
|
||||
>
|
||||
<MessageCircleIcon className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-10"
|
||||
disabled
|
||||
aria-label={`WhatsApp indisponível — ${row.name} sem celular`}
|
||||
>
|
||||
<MessageCircleIcon className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
)}
|
||||
<AdvisorPasswordResetButton
|
||||
advisorId={row.id}
|
||||
disabled={!emailShown}
|
||||
layout="icon"
|
||||
accessibleName={`Enviar link de senha para ${row.name}`}
|
||||
formAction={passwordResetAction}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-10"
|
||||
aria-label="Cancelar novo assessor"
|
||||
disabled={isCreating}
|
||||
onClick={cancelDraft}
|
||||
>
|
||||
<XIcon className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="size-10"
|
||||
aria-label="Salvar novo assessor"
|
||||
disabled={isCreating}
|
||||
onClick={saveDraft}
|
||||
>
|
||||
{isCreating ? (
|
||||
<Spinner className="size-4" aria-hidden="true" />
|
||||
) : (
|
||||
<SaveIcon className="size-4" aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{rows.map((row) => {
|
||||
const whatsAppHref = whatsAppHrefForPhone(row.phone)
|
||||
const emailShown = displayEmail(row.email)
|
||||
|
||||
return (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell>
|
||||
{editing ? (
|
||||
<AdvisorDebouncedTextCell
|
||||
advisorId={row.id}
|
||||
field="name"
|
||||
defaultValue={row.name}
|
||||
ariaLabel={`Nome de ${row.name}`}
|
||||
formAction={updateProfileAction}
|
||||
/>
|
||||
) : (
|
||||
<Link
|
||||
href={`/campanha/assessores/${row.id}`}
|
||||
className={cn(
|
||||
campaignReadCellClassName,
|
||||
'font-medium text-primary underline-offset-4 hover:underline',
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{row.name}</span>
|
||||
</Link>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{editing ? (
|
||||
<AdvisorDebouncedTextCell
|
||||
advisorId={row.id}
|
||||
field="email"
|
||||
type="email"
|
||||
defaultValue={row.email ?? ''}
|
||||
placeholder="E-mail"
|
||||
ariaLabel={`E-mail de ${row.name}`}
|
||||
placeholderEmailFallback={row.email}
|
||||
formAction={updateProfileAction}
|
||||
/>
|
||||
) : (
|
||||
<CampaignCopyableCell value={emailShown} label="E-mail" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{editing ? (
|
||||
<AdvisorDebouncedTextCell
|
||||
advisorId={row.id}
|
||||
field="phone"
|
||||
type="tel"
|
||||
defaultValue={row.phone ? formatBrazilianPhoneInput(row.phone) : ''}
|
||||
placeholder="Celular"
|
||||
ariaLabel={`Celular de ${row.name}`}
|
||||
formAction={updateProfileAction}
|
||||
/>
|
||||
) : (
|
||||
<CampaignCopyableCell
|
||||
value={row.phone}
|
||||
label="Celular"
|
||||
displayValue={
|
||||
row.phone ? formatBrazilianPhoneInput(row.phone) : undefined
|
||||
}
|
||||
className="tabular-nums"
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MunicipalityPortfolioCell
|
||||
ownerId={row.id}
|
||||
ownerName={row.name}
|
||||
municipalityIds={municipalityIdsByAdvisor.get(row.id) ?? []}
|
||||
municipalityIndex={municipalityIndex}
|
||||
commitAction={municipalitiesAction}
|
||||
drawerTitle="Carteira do assessor"
|
||||
updateErrorMessage="Não foi possível atualizar a carteira."
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
{whatsAppHref ? (
|
||||
<Button asChild variant="ghost" size="icon" className="size-10">
|
||||
<a
|
||||
href={whatsAppHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`Enviar WhatsApp para ${row.name}`}
|
||||
>
|
||||
<MessageCircleIcon className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-10"
|
||||
disabled
|
||||
aria-label={`WhatsApp indisponível — ${row.name} sem celular`}
|
||||
>
|
||||
<MessageCircleIcon className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
)}
|
||||
<AdvisorPasswordResetButton
|
||||
advisorId={row.id}
|
||||
disabled={!emailShown}
|
||||
layout="icon"
|
||||
accessibleName={`Enviar link de senha para ${row.name}`}
|
||||
formAction={passwordResetAction}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CampaignListSheetProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue