refactor: switch to using Bun (#718)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { FilterState } from "@components/generic/Filter/useFilterNode.ts";
|
||||
import {
|
||||
AccordionContent,
|
||||
AccordionHeader,
|
||||
@@ -5,13 +6,12 @@ import {
|
||||
AccordionTrigger,
|
||||
} from "@components/UI/Accordion.tsx";
|
||||
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
|
||||
import { Slider } from "@components/UI/Slider.tsx";
|
||||
import { ScrollArea } from "@components/UI/ScrollArea.tsx";
|
||||
import { Slider } from "@components/UI/Slider.tsx";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@components/UI/ToggleGroup.tsx";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import type { FilterState } from "@components/generic/Filter/useFilterNode.ts";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import type { ReactNode } from "react";
|
||||
import { useId } from "react";
|
||||
|
||||
interface FilterAccordionItemProps {
|
||||
label: string;
|
||||
@@ -66,9 +66,7 @@ export const FilterAccordionItem = ({
|
||||
</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
<AccordionContent
|
||||
className={cn(
|
||||
"px-1 pb-4 pt-2 space-y-3 dark:border-slate-700",
|
||||
)}
|
||||
className={cn("px-1 pb-4 pt-2 space-y-3 dark:border-slate-700")}
|
||||
>
|
||||
{children}
|
||||
</AccordionContent>
|
||||
@@ -85,6 +83,7 @@ export const FilterSlider = <K extends RangeKeys<FilterState>>({
|
||||
label,
|
||||
step,
|
||||
}: FilterSliderProps<K>) => {
|
||||
const sliderId = useId();
|
||||
const value: [number, number] = filterState[filterKey];
|
||||
const defaultValue: [number, number] = defaultFilterValues[filterKey];
|
||||
|
||||
@@ -98,7 +97,7 @@ export const FilterSlider = <K extends RangeKeys<FilterState>>({
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium">
|
||||
<label htmlFor={sliderId} className="block text-sm font-medium">
|
||||
{labelContent ?? defaultLabel}
|
||||
</label>
|
||||
<Slider
|
||||
@@ -112,6 +111,7 @@ export const FilterSlider = <K extends RangeKeys<FilterState>>({
|
||||
rangeClassName="bg-blue-500 dark:bg-blue-600"
|
||||
thumbClassName="w-3 h-3 bg-white dark:bg-slate-300 border border-slate-400 dark:border-slate-100"
|
||||
aria-label={label ?? String(filterKey)}
|
||||
id={sliderId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -121,7 +121,7 @@ function getNumberArray<T extends FilterState, K extends EnumArrayKeys<T>>(
|
||||
state: T,
|
||||
key: K,
|
||||
): number[] {
|
||||
return state[key] as unknown as number[];
|
||||
return state[key] as number[];
|
||||
}
|
||||
export const FilterMulti = <K extends EnumArrayKeys<FilterState>>({
|
||||
filterKey,
|
||||
@@ -132,8 +132,8 @@ export const FilterMulti = <K extends EnumArrayKeys<FilterState>>({
|
||||
}: FilterMultiProps<K>) => {
|
||||
const selected = getNumberArray(filterState, filterKey);
|
||||
|
||||
const allSelected = options.length > 0 &&
|
||||
options.every((opt) => selected.includes(opt));
|
||||
const allSelected =
|
||||
options.length > 0 && options.every((opt) => selected.includes(opt));
|
||||
|
||||
const toggleAll = () => {
|
||||
setFilterState((prev) => ({
|
||||
@@ -188,16 +188,19 @@ export const FilterToggle = <K extends keyof FilterState>({
|
||||
onChange,
|
||||
}: FilterToggleProps<K>) => (
|
||||
<div className="space-y-1 pb-1">
|
||||
<label className="block text-sm font-medium">
|
||||
<label htmlFor={label} className="block text-sm font-medium">
|
||||
{label}
|
||||
</label>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
aria-label={label}
|
||||
id={label}
|
||||
onValueChange={(value) => onChange(filterKey, value)}
|
||||
value={typeof filterState[filterKey] === "undefined"
|
||||
? ""
|
||||
: filterState[filterKey].toString()}
|
||||
value={
|
||||
typeof filterState[filterKey] === "undefined"
|
||||
? ""
|
||||
: String(filterState[filterKey])
|
||||
}
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="false"
|
||||
|
||||
@@ -1,37 +1,32 @@
|
||||
import {
|
||||
type ComponentProps,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { FunnelIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
import { debounce } from "@core/utils/debounce.ts";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@components/UI/Popover.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { Accordion } from "@components/UI/Accordion.tsx";
|
||||
|
||||
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
||||
import {
|
||||
FilterAccordionItem,
|
||||
FilterMulti,
|
||||
FilterSlider,
|
||||
FilterToggle,
|
||||
} from "@components/generic/Filter/FilterComponents.tsx";
|
||||
|
||||
import type { FilterState } from "@components/generic/Filter/useFilterNode.ts";
|
||||
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
||||
import { Accordion } from "@components/UI/Accordion.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@components/UI/Popover.tsx";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { debounce } from "@core/utils/debounce.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import type { TFunction } from "i18next";
|
||||
import { FunnelIcon } from "lucide-react";
|
||||
import {
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const DEBOUNCE_DELAY_MS = 250;
|
||||
const BATTERY_STATUS_PLUGGED_IN_VALUE = 101;
|
||||
@@ -76,24 +71,25 @@ interface LastHeardLabelProps {
|
||||
formatTS: (ts: number) => ReactNode;
|
||||
t: TFunction<"ui", undefined>;
|
||||
}
|
||||
function LastHeardLabelContent(
|
||||
{ lastHeardRange, defaultMaxLastHeard, formatTS, t }: LastHeardLabelProps,
|
||||
) {
|
||||
function LastHeardLabelContent({
|
||||
lastHeardRange,
|
||||
defaultMaxLastHeard,
|
||||
formatTS,
|
||||
t,
|
||||
}: LastHeardLabelProps) {
|
||||
const [start, end] = lastHeardRange;
|
||||
return (
|
||||
<>
|
||||
{t("lastHeard.labelText", { value: "" })}
|
||||
<br />
|
||||
{start === 0
|
||||
? (
|
||||
t("lastHeard.nowLabel")
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{start === defaultMaxLastHeard && ">"}
|
||||
{formatTS(start)}
|
||||
</>
|
||||
)}
|
||||
{start === 0 ? (
|
||||
t("lastHeard.nowLabel")
|
||||
) : (
|
||||
<>
|
||||
{start === defaultMaxLastHeard && ">"}
|
||||
{formatTS(start)}
|
||||
</>
|
||||
)}
|
||||
{start !== end && (
|
||||
<>
|
||||
{" — "}
|
||||
@@ -109,13 +105,16 @@ interface BatteryLevelLabelProps {
|
||||
batteryLevelRange: (number | undefined)[];
|
||||
t: TFunction<"ui", undefined>;
|
||||
}
|
||||
function BatteryLevelLabelContent(
|
||||
{ batteryLevelRange, t }: BatteryLevelLabelProps,
|
||||
) {
|
||||
function BatteryLevelLabelContent({
|
||||
batteryLevelRange,
|
||||
t,
|
||||
}: BatteryLevelLabelProps) {
|
||||
const [start, end] = batteryLevelRange;
|
||||
|
||||
const formatBatteryValue = (value: number | undefined) => {
|
||||
if (value === undefined) return "";
|
||||
if (value === undefined) {
|
||||
return "";
|
||||
}
|
||||
return value === BATTERY_STATUS_PLUGGED_IN_VALUE
|
||||
? t("batteryStatus.pluggedIn")
|
||||
: `${value}%`;
|
||||
@@ -158,12 +157,12 @@ export function FilterControl({
|
||||
|
||||
const handleTextChange = useCallback(
|
||||
<K extends keyof FilterState>(key: K) =>
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterState((prev) => ({
|
||||
...prev,
|
||||
[key]: e.target.value,
|
||||
}));
|
||||
},
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterState((prev) => ({
|
||||
...prev,
|
||||
[key]: e.target.value,
|
||||
}));
|
||||
},
|
||||
[setFilterState],
|
||||
);
|
||||
|
||||
@@ -175,27 +174,29 @@ export function FilterControl({
|
||||
[key]: value,
|
||||
}));
|
||||
}, DEBOUNCE_DELAY_MS),
|
||||
[setFilterState],
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRangeChange = useCallback(
|
||||
<K extends keyof FilterState>(key: K) => (value: number[]) => {
|
||||
setLocalFilterState((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
debouncedSetFilterState(key, value);
|
||||
},
|
||||
<K extends keyof FilterState>(key: K) =>
|
||||
(value: number[]) => {
|
||||
setLocalFilterState((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
debouncedSetFilterState(key, value);
|
||||
},
|
||||
[debouncedSetFilterState],
|
||||
);
|
||||
|
||||
const handleBoolChange = useCallback(
|
||||
<K extends keyof FilterState>(key: K, value: string | boolean) => {
|
||||
const typedValue = value === true || value === "true"
|
||||
? true
|
||||
: value === false || value === "false"
|
||||
? false
|
||||
: undefined;
|
||||
const typedValue =
|
||||
value === true || value === "true"
|
||||
? true
|
||||
: value === false || value === "false"
|
||||
? false
|
||||
: undefined;
|
||||
|
||||
setFilterState((prev) => ({
|
||||
...prev,
|
||||
@@ -298,20 +299,14 @@ export function FilterControl({
|
||||
<FilterToggle
|
||||
label={t("favorites.label")}
|
||||
filterKey="isFavorite"
|
||||
alternativeLabels={[
|
||||
t("hide.label"),
|
||||
t("showOnly.label"),
|
||||
]}
|
||||
alternativeLabels={[t("hide.label"), t("showOnly.label")]}
|
||||
filterState={filterState}
|
||||
onChange={handleBoolChange}
|
||||
/>
|
||||
<FilterToggle
|
||||
label={t("viaMqtt.label")}
|
||||
filterKey="viaMqtt"
|
||||
alternativeLabels={[
|
||||
t("hide.label"),
|
||||
t("showOnly.label"),
|
||||
]}
|
||||
alternativeLabels={[t("hide.label"), t("showOnly.label")]}
|
||||
filterState={filterState}
|
||||
onChange={handleBoolChange}
|
||||
/>
|
||||
@@ -370,9 +365,8 @@ export function FilterControl({
|
||||
Protobuf.Config.Config_DeviceConfig_Role,
|
||||
).filter((v): v is number => typeof v === "number")}
|
||||
getLabel={(val) =>
|
||||
formatEnumLabel(
|
||||
Protobuf.Config.Config_DeviceConfig_Role[val],
|
||||
)}
|
||||
formatEnumLabel(Protobuf.Config.Config_DeviceConfig_Role[val])
|
||||
}
|
||||
/>
|
||||
</FilterAccordionItem>
|
||||
|
||||
@@ -385,27 +379,22 @@ export function FilterControl({
|
||||
(v): v is number => typeof v === "number",
|
||||
)}
|
||||
getLabel={(val) =>
|
||||
formatEnumLabel(Protobuf.Mesh.HardwareModel[val])}
|
||||
formatEnumLabel(Protobuf.Mesh.HardwareModel[val])
|
||||
}
|
||||
/>
|
||||
</FilterAccordionItem>
|
||||
<FilterAccordionItem label={t("advanced.label")}>
|
||||
<FilterToggle
|
||||
label={t("hopsUnknown.label")}
|
||||
filterKey="hopsUnknown"
|
||||
alternativeLabels={[
|
||||
t("hide.label"),
|
||||
t("showOnly.label"),
|
||||
]}
|
||||
alternativeLabels={[t("hide.label"), t("showOnly.label")]}
|
||||
filterState={filterState}
|
||||
onChange={handleBoolChange}
|
||||
/>
|
||||
<FilterToggle
|
||||
label={t("showUnheard.label")}
|
||||
filterKey="showUnheard"
|
||||
alternativeLabels={[
|
||||
t("hide.label"),
|
||||
t("showOnly.label"),
|
||||
]}
|
||||
alternativeLabels={[t("hide.label"), t("showOnly.label")]}
|
||||
filterState={filterState}
|
||||
onChange={handleBoolChange}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { useFilterNode } from "@components/generic/Filter/useFilterNode.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
export function createMockNode(): Protobuf.Mesh.NodeInfo {
|
||||
function createMockNode(): Protobuf.Mesh.NodeInfo {
|
||||
return {
|
||||
$typeName: "meshtastic.NodeInfo",
|
||||
num: 1234567890,
|
||||
@@ -14,6 +14,7 @@ export function createMockNode(): Protobuf.Mesh.NodeInfo {
|
||||
isFavorite: true,
|
||||
isIgnored: false,
|
||||
hopsAway: 2,
|
||||
isKeyManuallyVerified: false,
|
||||
user: {
|
||||
$typeName: "meshtastic.User",
|
||||
id: "!12345678",
|
||||
@@ -29,7 +30,7 @@ export function createMockNode(): Protobuf.Mesh.NodeInfo {
|
||||
$typeName: "meshtastic.DeviceMetrics",
|
||||
batteryLevel: 101,
|
||||
voltage: 4.21,
|
||||
channelUtilization: 7.50,
|
||||
channelUtilization: 7.5,
|
||||
airUtilTx: 2.57,
|
||||
uptimeSeconds: 528092,
|
||||
},
|
||||
|
||||
@@ -67,7 +67,9 @@ export function useFilterNode() {
|
||||
...filterOverrides,
|
||||
};
|
||||
|
||||
if (!node.user) return false;
|
||||
if (!node.user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nodeName = filterState.nodeName.toLowerCase();
|
||||
if (
|
||||
@@ -89,7 +91,7 @@ export function useFilterNode() {
|
||||
|
||||
if (
|
||||
(filterState.hopsUnknown === true && node.hopsAway !== undefined) ||
|
||||
filterState.hopsUnknown === false && node.hopsAway === undefined
|
||||
(filterState.hopsUnknown === false && node.hopsAway === undefined)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -105,7 +107,7 @@ export function useFilterNode() {
|
||||
|
||||
if (
|
||||
(filterState.showUnheard === true && (node.lastHeard ?? 0) !== 0) ||
|
||||
filterState.showUnheard === false && (node.lastHeard ?? 0) === 0
|
||||
(filterState.showUnheard === false && (node.lastHeard ?? 0) === 0)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -125,7 +127,9 @@ export function useFilterNode() {
|
||||
}
|
||||
|
||||
const snr = node.snr ?? -20;
|
||||
if (snr < filterState.snr[0] || snr > filterState.snr[1]) return false;
|
||||
if (snr < filterState.snr[0] || snr > filterState.snr[1]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const channelUtilization = node.deviceMetrics?.channelUtilization ?? 0;
|
||||
if (
|
||||
@@ -159,13 +163,17 @@ export function useFilterNode() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const role: Protobuf.Config.Config_DeviceConfig_Role = node.user.role ??
|
||||
Protobuf.Config.Config_DeviceConfig_Role.CLIENT;
|
||||
if (!filterState.role.includes(role)) return false;
|
||||
const role: Protobuf.Config.Config_DeviceConfig_Role =
|
||||
node.user.role ?? Protobuf.Config.Config_DeviceConfig_Role.CLIENT;
|
||||
if (!filterState.role.includes(role)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hwModel: Protobuf.Mesh.HardwareModel = node.user.hwModel ??
|
||||
Protobuf.Mesh.HardwareModel.UNSET;
|
||||
if (!filterState.hwModel.includes(hwModel)) return false;
|
||||
const hwModel: Protobuf.Mesh.HardwareModel =
|
||||
node.user.hwModel ?? Protobuf.Mesh.HardwareModel.UNSET;
|
||||
if (!filterState.hwModel.includes(hwModel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
@@ -173,10 +181,7 @@ export function useFilterNode() {
|
||||
);
|
||||
|
||||
const isFilterDirty = useCallback(
|
||||
(
|
||||
current: FilterState,
|
||||
overrides?: Partial<FilterState>,
|
||||
): boolean => {
|
||||
(current: FilterState, overrides?: Partial<FilterState>): boolean => {
|
||||
const base: FilterState = overrides
|
||||
? { ...defaultFilterValues, ...overrides }
|
||||
: defaultFilterValues;
|
||||
|
||||
@@ -4,11 +4,7 @@ interface MonoProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
export const Mono = ({
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}: MonoProps) => {
|
||||
export const Mono = ({ children, className, ...rest }: MonoProps) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("font-mono text-sm text-text-secondary", className)}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { DataRow, Heading, Table } from "@components/generic/Table/index.tsx";
|
||||
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
||||
import { Mono } from "@components/generic/Mono.tsx";
|
||||
import {
|
||||
type DataRow,
|
||||
type Heading,
|
||||
Table,
|
||||
} from "@components/generic/Table/index.tsx";
|
||||
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
// @ts-types="react"
|
||||
|
||||
describe("Generic Table", () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import React from "react";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
export interface Heading {
|
||||
title: string;
|
||||
@@ -49,10 +48,14 @@ export const Table = ({ headings, rows }: TableProps) => {
|
||||
};
|
||||
|
||||
const sortedRows = useMemo(() => {
|
||||
if (!sortColumn) return rows;
|
||||
if (!sortColumn) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const columnIndex = headings.findIndex((h) => h.title === sortColumn);
|
||||
if (columnIndex === -1) return rows;
|
||||
if (columnIndex === -1) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
return [...rows].sort((a, b) => {
|
||||
if (a.isFavorite !== b.isFavorite) {
|
||||
@@ -73,8 +76,12 @@ export const Table = ({ headings, rows }: TableProps) => {
|
||||
bValue = bCell.sortValue;
|
||||
}
|
||||
|
||||
if (aValue < bValue) return sortOrder === "asc" ? -1 : 1;
|
||||
if (aValue > bValue) return sortOrder === "asc" ? 1 : -1;
|
||||
if (aValue < bValue) {
|
||||
return sortOrder === "asc" ? -1 : 1;
|
||||
}
|
||||
if (aValue > bValue) {
|
||||
return sortOrder === "asc" ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}, [rows, sortColumn, sortOrder, headings]);
|
||||
@@ -99,18 +106,23 @@ export const Table = ({ headings, rows }: TableProps) => {
|
||||
}
|
||||
}}
|
||||
tabIndex={heading.sortable ? 0 : -1}
|
||||
role="columnheader"
|
||||
aria-sort={sortColumn === heading.title
|
||||
? sortOrder === "asc" ? "ascending" : "descending"
|
||||
: "none"}
|
||||
aria-sort={
|
||||
sortColumn === heading.title
|
||||
? sortOrder === "asc"
|
||||
? "ascending"
|
||||
: "descending"
|
||||
: "none"
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{heading.title}
|
||||
{heading.sortable && sortColumn === heading.title && (
|
||||
sortOrder === "asc"
|
||||
? <ChevronUpIcon size={16} aria-hidden="true" />
|
||||
: <ChevronDownIcon size={16} aria-hidden="true" />
|
||||
)}
|
||||
{heading.sortable &&
|
||||
sortColumn === heading.title &&
|
||||
(sortOrder === "asc" ? (
|
||||
<ChevronUpIcon size={16} aria-hidden="true" />
|
||||
) : (
|
||||
<ChevronDownIcon size={16} aria-hidden="true" />
|
||||
))}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
@@ -126,26 +138,25 @@ export const Table = ({ headings, rows }: TableProps) => {
|
||||
: "bg-white dark:bg-slate-900 odd:bg-slate-200/40 dark:odd:bg-slate-800/40",
|
||||
)}
|
||||
>
|
||||
{row.cells.map((cell, cellIndex) =>
|
||||
cellIndex === 0
|
||||
? (
|
||||
<th
|
||||
key={`${row.id}_${cellIndex}`}
|
||||
className="whitespace-nowrap px-3 py-2 text-sm text-left text-text-secondary"
|
||||
scope="row"
|
||||
>
|
||||
{cell.content}
|
||||
</th>
|
||||
)
|
||||
: (
|
||||
<td
|
||||
key={`${row.id}_${cellIndex}`}
|
||||
className="whitespace-nowrap px-3 py-2 text-sm text-text-secondary"
|
||||
>
|
||||
{cell.content}
|
||||
</td>
|
||||
)
|
||||
)}
|
||||
{row.cells.map((cell, cellIndex) => {
|
||||
const key = `${row.id}_${cellIndex}`;
|
||||
const isFirstCell = cellIndex === 0;
|
||||
|
||||
const cellElement = isFirstCell ? (
|
||||
<th
|
||||
className="whitespace-nowrap px-3 py-2 text-sm text-left text-text-secondary"
|
||||
scope="row"
|
||||
>
|
||||
{cell.content}
|
||||
</th>
|
||||
) : (
|
||||
<td className="whitespace-nowrap px-3 py-2 text-sm text-text-secondary">
|
||||
{cell.content}
|
||||
</td>
|
||||
);
|
||||
|
||||
return React.cloneElement(cellElement, { key });
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user