refactor(webui): use central campaign components
This commit is contained in:
@@ -21,6 +21,7 @@ import { ToggleSwitch } from "@govoplan/core-webui";
|
||||
import { DismissibleAlert } from "@govoplan/core-webui";
|
||||
import { Dialog } from "@govoplan/core-webui";
|
||||
import { SegmentedControl } from "@govoplan/core-webui";
|
||||
import { TableActionGroup } from "@govoplan/core-webui";
|
||||
import DataGrid, { DataGridEmptyAction, DataGridRowActions, type DataGridColumn } from "../../components/table/DataGrid";
|
||||
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
|
||||
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
|
||||
@@ -582,24 +583,10 @@ function AddressHeaderControl({ columns, values, emptyText, disabled, onEdit, on
|
||||
<span>{translateText(emptyText)}</span>
|
||||
}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
className="admin-icon-button recipient-address-inline-button"
|
||||
disabled={disabled}
|
||||
aria-label="Edit addresses"
|
||||
title="Edit addresses"
|
||||
onClick={onEdit}>
|
||||
<Pencil aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="admin-icon-button recipient-address-inline-button"
|
||||
disabled={!copiedText}
|
||||
aria-label="Copy addresses"
|
||||
title="Copy addresses"
|
||||
onClick={onCopy}>
|
||||
<Copy aria-hidden="true" />
|
||||
</Button>
|
||||
<TableActionGroup actions={[
|
||||
{ id: "edit", label: "Edit addresses", icon: <Pencil aria-hidden="true" />, disabled, onClick: onEdit },
|
||||
{ id: "copy", label: "Copy addresses", icon: <Copy aria-hidden="true" />, applicable: Boolean(copiedText), onClick: onCopy }
|
||||
]} />
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
@@ -783,18 +770,17 @@ function RecipientAddressCategoryEditor({ column, addresses, merge, locked, onAd
|
||||
{column.allowMultiple && draftAddresses.length === 0 &&
|
||||
<div className="recipient-address-empty-row">
|
||||
<p className="muted recipient-address-empty">{column.emptyText}</p>
|
||||
<div className="data-grid-row-actions data-grid-empty-row-actions">
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="data-grid-row-action is-add"
|
||||
aria-label={translatedAddLabel}
|
||||
title={translatedAddLabel}
|
||||
disabled={!canAdd}
|
||||
onClick={() => addAddress()}>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
<TableActionGroup
|
||||
className="data-grid-empty-row-actions"
|
||||
actions={[{
|
||||
id: "add",
|
||||
label: translatedAddLabel,
|
||||
icon: <Plus size={16} aria-hidden="true" />,
|
||||
variant: "primary",
|
||||
disabled: !canAdd,
|
||||
onClick: () => addAddress()
|
||||
}]}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{draftAddresses.map((address, addressIndex) =>
|
||||
@@ -813,48 +799,15 @@ function RecipientAddressCategoryEditor({ column, addresses, merge, locked, onAd
|
||||
aria-label={`${column.label} email ${addressIndex + 1}`}
|
||||
onChange={(event) => patchAddress(addressIndex, { email: event.target.value })} />
|
||||
{column.allowMultiple &&
|
||||
<div className="data-grid-row-actions recipient-address-line-actions">
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="data-grid-row-action is-add"
|
||||
aria-label={translatedAddLabel}
|
||||
title={translatedAddLabel}
|
||||
disabled={!canAdd}
|
||||
onClick={() => addAddress(addressIndex)}>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="data-grid-row-action is-reorder"
|
||||
aria-label={translatedMoveUpLabel}
|
||||
title={translatedMoveUpLabel}
|
||||
disabled={locked || addressIndex === 0}
|
||||
onClick={() => moveAddress(addressIndex, addressIndex - 1)}>
|
||||
<ArrowUp size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="data-grid-row-action is-reorder"
|
||||
aria-label={translatedMoveDownLabel}
|
||||
title={translatedMoveDownLabel}
|
||||
disabled={locked || addressIndex >= draftAddresses.length - 1}
|
||||
onClick={() => moveAddress(addressIndex, addressIndex + 1)}>
|
||||
<ArrowDown size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
className="data-grid-row-action is-remove"
|
||||
aria-label={translatedRemoveLabel}
|
||||
title={translatedRemoveLabel}
|
||||
disabled={locked}
|
||||
onClick={() => removeAddress(addressIndex)}>
|
||||
<Trash2 size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
<TableActionGroup
|
||||
className="recipient-address-line-actions"
|
||||
actions={[
|
||||
{ id: "add", label: translatedAddLabel, icon: <Plus size={16} aria-hidden="true" />, variant: "primary", disabled: !canAdd, onClick: () => addAddress(addressIndex) },
|
||||
{ id: "move-up", label: translatedMoveUpLabel, icon: <ArrowUp size={16} aria-hidden="true" />, disabled: locked || addressIndex === 0, onClick: () => moveAddress(addressIndex, addressIndex - 1) },
|
||||
{ id: "move-down", label: translatedMoveDownLabel, icon: <ArrowDown size={16} aria-hidden="true" />, disabled: locked || addressIndex >= draftAddresses.length - 1, onClick: () => moveAddress(addressIndex, addressIndex + 1) },
|
||||
{ id: "remove", label: translatedRemoveLabel, icon: <Trash2 size={16} aria-hidden="true" />, variant: "danger", disabled: locked, onClick: () => removeAddress(addressIndex) }
|
||||
]}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
@@ -1080,34 +1033,25 @@ function AddressSourceImportDialog({ settings, campaignId, sources, initialSourc
|
||||
<div><dt>Recipients</dt><dd>{snapshot.recipients.length}</dd></div>
|
||||
<div><dt>Revision</dt><dd className="mono-small">{snapshot.source_revision}</dd></div>
|
||||
</dl>
|
||||
<div className="admin-table-surface recipient-import-preview-surface">
|
||||
<table className="recipient-import-preview-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Fields</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{snapshot.recipients.slice(0, 20).map((recipient, index) =>
|
||||
<tr key={`${recipient.contact_id}-${recipient.email}`}>
|
||||
<td>{index + 1}</td>
|
||||
<td>{recipient.display_name}</td>
|
||||
<td>{recipient.email}</td>
|
||||
<td>{Object.keys(recipient.fields ?? {}).filter((key) => fieldValueToString((recipient.fields ?? {})[key])).length}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<AddressSourceRecipientPreviewGrid recipients={snapshot.recipients.slice(0, 20)} />
|
||||
{snapshot.recipients.length > 20 && <p className="muted small-note">{snapshot.recipients.length - 20} more recipients will be imported.</p>}
|
||||
</>
|
||||
}
|
||||
</Dialog>);
|
||||
}
|
||||
|
||||
type AddressSourceSnapshotRecipient = CampaignRecipientAddressSourceSnapshot["recipients"][number];
|
||||
|
||||
function AddressSourceRecipientPreviewGrid({ recipients }: {recipients: AddressSourceSnapshotRecipient[];}) {
|
||||
const columns: DataGridColumn<AddressSourceSnapshotRecipient>[] = [
|
||||
{ id: "row", header: "#", width: 64, value: (_recipient, index) => index + 1, render: (_recipient, index) => index + 1 },
|
||||
{ id: "name", header: "Name", width: "minmax(180px, 1fr)", minWidth: 160, resizable: true, sortable: true, filterable: true, value: (recipient) => recipient.display_name },
|
||||
{ id: "email", header: "Email", width: "minmax(220px, 1.2fr)", minWidth: 190, resizable: true, sortable: true, filterable: true, value: (recipient) => recipient.email },
|
||||
{ id: "fields", header: "Fields", width: 100, sortable: true, value: (recipient) => Object.keys(recipient.fields ?? {}).filter((key) => fieldValueToString((recipient.fields ?? {})[key])).length }
|
||||
];
|
||||
return <DataGrid id="campaign-address-source-import-preview" rows={recipients} columns={columns} getRowKey={(recipient) => `${recipient.contact_id}-${recipient.email}`} />;
|
||||
}
|
||||
|
||||
type RecipientImportDialogProps = {
|
||||
settings: ApiSettings;
|
||||
campaignId: string;
|
||||
@@ -1451,6 +1395,30 @@ export function RecipientImportDialog({ settings, campaignId, existingEntries, e
|
||||
replaceColumnMapping(nextMapping);
|
||||
}
|
||||
|
||||
const mappingRows = (table?.headers ?? []).map((header, columnIndex) => ({
|
||||
id: `${columnIndex}-${header}`,
|
||||
header,
|
||||
columnIndex,
|
||||
mapping: mappings.find((item) => item.columnIndex === columnIndex) ?? { columnIndex, kind: "ignore" as const }
|
||||
}));
|
||||
type MappingRow = (typeof mappingRows)[number];
|
||||
const mappingColumns: DataGridColumn<MappingRow>[] = [
|
||||
{ id: "column", header: "i18n:govoplan-campaign.column.65ba00e9", width: "minmax(180px, .8fr)", minWidth: 160, resizable: true, sortable: true, filterable: true, value: (row) => row.header, render: (row) => <strong>{row.header}</strong> },
|
||||
{ id: "sample", header: "i18n:govoplan-campaign.sample.58fabfa7", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, filterable: true, value: (row) => table ? sampleColumnValue(table.dataRows, row.columnIndex) : "", render: (row) => <span className="mono-small">{table ? sampleColumnValue(table.dataRows, row.columnIndex) : ""}</span> },
|
||||
{ id: "content", header: "i18n:govoplan-campaign.content.4f9be057", width: 190, value: (row) => row.mapping.kind, render: (row) => <select value={row.mapping.kind} onChange={(event) => changeColumnKind(row.columnIndex, event.target.value as RecipientColumnKind)}>{recipientColumnKindOptions.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}</select> },
|
||||
{ id: "field", header: "i18n:govoplan-campaign.field.c326a466", width: "minmax(200px, .9fr)", minWidth: 180, resizable: true, value: (row) => row.mapping.fieldName ?? row.mapping.newFieldName ?? "", render: (row) => <>{row.mapping.kind === "field" && <select value={row.mapping.fieldName ?? ""} onChange={(event) => replaceColumnMapping({ ...row.mapping, fieldName: event.target.value, newFieldName: undefined })}><option value="">i18n:govoplan-campaign.select_field.bb7e63d5</option>{existingFields.map((field) => <option key={field.name} value={field.name}>{field.label || field.name}</option>)}</select>}{row.mapping.kind === "new_field" && <input value={row.mapping.newFieldName ?? ""} onChange={(event) => replaceColumnMapping({ ...row.mapping, newFieldName: event.target.value, fieldName: undefined })} />}</> }
|
||||
];
|
||||
|
||||
type PreviewRow = RecipientImportPreview["rows"][number];
|
||||
const previewColumns: DataGridColumn<PreviewRow>[] = [
|
||||
{ id: "row", header: "#", width: 68, sortable: true, value: (row) => row.rowNumber },
|
||||
{ id: "to", header: "i18n:govoplan-campaign.to.ae79ea1e", width: "minmax(220px, 1fr)", minWidth: 190, resizable: true, filterable: true, value: (row) => formatAddressList(row.addresses.to) },
|
||||
{ id: "name", header: "i18n:govoplan-campaign.name.709a2322", width: "minmax(180px, .8fr)", minWidth: 160, resizable: true, sortable: true, filterable: true, value: (row) => row.name },
|
||||
{ id: "fields", header: "i18n:govoplan-campaign.fields.e8b68527", width: 100, sortable: true, value: (row) => Object.keys(row.fields).length },
|
||||
{ id: "patterns", header: "i18n:govoplan-campaign.patterns.4d34f7a2", width: 110, sortable: true, value: (row) => row.patterns.length },
|
||||
{ id: "status", header: "i18n:govoplan-campaign.status.bae7d5be", width: "minmax(220px, 1fr)", minWidth: 190, resizable: true, filterable: true, value: (row) => row.issues.length ? row.issues.join(", ") : "i18n:govoplan-campaign.ready.20c7c552", render: (row) => row.issues.length ? row.issues.join(", ") : "i18n:govoplan-campaign.ready.20c7c552" }
|
||||
];
|
||||
|
||||
const stepContent = activeStep === "upload" ?
|
||||
<>
|
||||
<div className="campaign-header-grid recipient-import-upload-grid">
|
||||
@@ -1569,51 +1537,7 @@ export function RecipientImportDialog({ settings, campaignId, existingEntries, e
|
||||
</select>
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="admin-table-surface recipient-import-preview-surface">
|
||||
<table className="recipient-import-preview-table recipient-import-mapping-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>i18n:govoplan-campaign.column.65ba00e9</th>
|
||||
<th>i18n:govoplan-campaign.sample.58fabfa7</th>
|
||||
<th>i18n:govoplan-campaign.content.4f9be057</th>
|
||||
<th>i18n:govoplan-campaign.field.c326a466</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{table?.headers.map((header, columnIndex) => {
|
||||
const mapping = mappings.find((item) => item.columnIndex === columnIndex) ?? { columnIndex, kind: "ignore" as const };
|
||||
return (
|
||||
<tr key={`${columnIndex}-${header}`}>
|
||||
<td><strong>{header}</strong></td>
|
||||
<td className="mono-small">{sampleColumnValue(table.dataRows, columnIndex)}</td>
|
||||
<td>
|
||||
<select value={mapping.kind} onChange={(event) => changeColumnKind(columnIndex, event.target.value as RecipientColumnKind)}>
|
||||
{recipientColumnKindOptions.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
{mapping.kind === "field" &&
|
||||
<select
|
||||
value={mapping.fieldName ?? ""}
|
||||
onChange={(event) => replaceColumnMapping({ ...mapping, fieldName: event.target.value, newFieldName: undefined })}>
|
||||
|
||||
<option value="">i18n:govoplan-campaign.select_field.bb7e63d5</option>
|
||||
{existingFields.map((field) => <option key={field.name} value={field.name}>{field.label || field.name}</option>)}
|
||||
</select>
|
||||
}
|
||||
{mapping.kind === "new_field" &&
|
||||
<input
|
||||
value={mapping.newFieldName ?? ""}
|
||||
onChange={(event) => replaceColumnMapping({ ...mapping, newFieldName: event.target.value, fieldName: undefined })} />
|
||||
|
||||
}
|
||||
</td>
|
||||
</tr>);
|
||||
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataGrid id="campaign-recipient-import-mapping" rows={mappingRows} columns={mappingColumns} getRowKey={(row) => row.id} />
|
||||
</> :
|
||||
activeStep === "preview" ?
|
||||
<>
|
||||
@@ -1628,32 +1552,14 @@ export function RecipientImportDialog({ settings, campaignId, existingEntries, e
|
||||
{preview.fieldNamesToCreate.length > 0 &&
|
||||
<p className="muted small-note">i18n:govoplan-campaign.new_fields.c61e20c0 {preview.fieldNamesToCreate.join(", ")}</p>
|
||||
}
|
||||
<div className="admin-table-surface recipient-import-preview-surface">
|
||||
<table className="recipient-import-preview-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>i18n:govoplan-campaign.to.ae79ea1e</th>
|
||||
<th>i18n:govoplan-campaign.name.709a2322</th>
|
||||
<th>i18n:govoplan-campaign.fields.e8b68527</th>
|
||||
<th>i18n:govoplan-campaign.patterns.4d34f7a2</th>
|
||||
<th>i18n:govoplan-campaign.status.bae7d5be</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{preview.rows.slice(0, 20).map((row) =>
|
||||
<tr key={row.rowNumber} className={row.issues.length ? "is-invalid" : undefined}>
|
||||
<td>{row.rowNumber}</td>
|
||||
<td>{formatAddressList(row.addresses.to)}</td>
|
||||
<td>{row.name}</td>
|
||||
<td>{Object.keys(row.fields).length}</td>
|
||||
<td>{row.patterns.length}</td>
|
||||
<td>{row.issues.length ? row.issues.join(", ") : "i18n:govoplan-campaign.ready.20c7c552"}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataGrid
|
||||
id="campaign-recipient-import-preview"
|
||||
className="recipient-import-preview-grid"
|
||||
rows={preview.rows.slice(0, 20)}
|
||||
columns={previewColumns}
|
||||
getRowKey={(row) => String(row.rowNumber)}
|
||||
rowClassName={(row) => row.issues.length ? "is-invalid" : undefined}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</> :
|
||||
@@ -1743,6 +1649,14 @@ function RecipientImportFileLinkStep({ preview, basePath, resolution, resolving,
|
||||
return <DismissibleAlert tone="info" dismissible={false}>i18n:govoplan-campaign.no_attachment_patterns_were_imported_there_are_n.c01c8266</DismissibleAlert>;
|
||||
}
|
||||
|
||||
type ResolvedFile = ImportFileLinkResolution["files"][number];
|
||||
const fileColumns: DataGridColumn<ResolvedFile>[] = [
|
||||
{ id: "file", header: "i18n:govoplan-campaign.file.2c3cafa4", width: "minmax(200px, .8fr)", minWidth: 180, resizable: true, sortable: true, filterable: true, value: (file) => file.filename, render: (file) => <strong>{file.filename}</strong> },
|
||||
{ id: "path", header: "i18n:govoplan-campaign.path.519e3913", width: "minmax(260px, 1.3fr)", minWidth: 220, resizable: true, sortable: true, filterable: true, value: (file) => file.display_path },
|
||||
{ id: "size", header: "i18n:govoplan-campaign.size.b7152342", width: 120, sortable: true, value: (file) => file.size_bytes, render: (file) => formatImportBytes(file.size_bytes) },
|
||||
{ id: "status", header: "i18n:govoplan-campaign.status.bae7d5be", width: 160, sortable: true, filterable: true, value: (file) => linkableIds.has(file.id) ? "needs-linking" : "linked", render: (file) => linkableIds.has(file.id) ? "i18n:govoplan-campaign.needs_linking.a0fc8341" : "i18n:govoplan-campaign.linked.a089f600" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="recipient-import-file-step">
|
||||
<div className="recipient-import-file-actions">
|
||||
@@ -1781,30 +1695,13 @@ function RecipientImportFileLinkStep({ preview, basePath, resolution, resolving,
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="admin-table-surface recipient-import-preview-surface">
|
||||
<table className="recipient-import-preview-table recipient-import-file-link-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>i18n:govoplan-campaign.file.2c3cafa4</th>
|
||||
<th>i18n:govoplan-campaign.path.519e3913</th>
|
||||
<th>i18n:govoplan-campaign.size.b7152342</th>
|
||||
<th>i18n:govoplan-campaign.status.bae7d5be</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{resolution.files.length === 0 ?
|
||||
<tr><td colSpan={4}>i18n:govoplan-campaign.no_files_currently_match_the_imported_patterns.6b599e8b</td></tr> :
|
||||
resolution.files.map((file) =>
|
||||
<tr key={file.id}>
|
||||
<td><strong>{file.filename}</strong></td>
|
||||
<td>{file.display_path}</td>
|
||||
<td>{formatImportBytes(file.size_bytes)}</td>
|
||||
<td>{linkableIds.has(file.id) ? "i18n:govoplan-campaign.needs_linking.a0fc8341" : "i18n:govoplan-campaign.linked.a089f600"}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataGrid
|
||||
id="campaign-recipient-import-file-links"
|
||||
rows={resolution.files}
|
||||
columns={fileColumns}
|
||||
getRowKey={(file) => file.id}
|
||||
emptyText="i18n:govoplan-campaign.no_files_currently_match_the_imported_patterns.6b599e8b"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</div>);
|
||||
@@ -1820,27 +1717,31 @@ type RecipientImportRawTableProps = {
|
||||
function RecipientImportRawTable({ tableRows, headerRowCount, columnCount }: RecipientImportRawTableProps) {
|
||||
const visibleRows = tableRows.slice(0, 15);
|
||||
const safeColumnCount = Math.max(1, columnCount, ...visibleRows.map((row) => row.length));
|
||||
const rows = visibleRows.map((cells, rowIndex) => ({ rowIndex, cells }));
|
||||
type RawRow = (typeof rows)[number];
|
||||
const columns: DataGridColumn<RawRow>[] = [
|
||||
{ id: "row", header: "#", width: 64, sortable: true, value: (row) => row.rowIndex + 1 },
|
||||
...Array.from({ length: safeColumnCount }, (_value, columnIndex): DataGridColumn<RawRow> => ({
|
||||
id: `column-${columnIndex + 1}`,
|
||||
header: String(columnIndex + 1),
|
||||
width: "minmax(140px, 1fr)",
|
||||
minWidth: 120,
|
||||
resizable: true,
|
||||
filterable: true,
|
||||
value: (row) => row.cells[columnIndex] ?? ""
|
||||
}))
|
||||
];
|
||||
return (
|
||||
<div className="admin-table-surface recipient-import-preview-surface">
|
||||
<table className="recipient-import-preview-table recipient-import-raw-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
{Array.from({ length: safeColumnCount }, (_value, index) => <th key={index}>{index + 1}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visibleRows.length === 0 ?
|
||||
<tr><td colSpan={safeColumnCount + 1}>i18n:govoplan-campaign.no_rows_parsed.a7ccc3de</td></tr> :
|
||||
visibleRows.map((row, rowIndex) =>
|
||||
<tr key={rowIndex} className={rowIndex < headerRowCount ? "is-header-row" : undefined}>
|
||||
<td>{rowIndex + 1}</td>
|
||||
{Array.from({ length: safeColumnCount }, (_value, columnIndex) => <td key={columnIndex}>{row[columnIndex] ?? ""}</td>)}
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>);
|
||||
<DataGrid
|
||||
id="campaign-recipient-import-raw"
|
||||
className="recipient-import-raw-grid"
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
getRowKey={(row) => String(row.rowIndex)}
|
||||
rowClassName={(row) => row.rowIndex < headerRowCount ? "is-header-row" : undefined}
|
||||
emptyText="i18n:govoplan-campaign.no_rows_parsed.a7ccc3de"
|
||||
initialFit="content"
|
||||
/>);
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user