kokotable 0.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +320 -0
- package/contract/table-request.schema.json +424 -0
- package/dist/core/src/adapters/sources.d.ts +59 -0
- package/dist/core/src/adapters/sources.js +235 -0
- package/dist/core/src/client/aggregate.d.ts +94 -0
- package/dist/core/src/client/aggregate.js +222 -0
- package/dist/core/src/client/groupRows.d.ts +40 -0
- package/dist/core/src/client/groupRows.js +145 -0
- package/dist/core/src/client/runClientQuery.d.ts +67 -0
- package/dist/core/src/client/runClientQuery.js +157 -0
- package/dist/core/src/columns/ColumnBuilder.d.ts +121 -0
- package/dist/core/src/columns/ColumnBuilder.js +273 -0
- package/dist/core/src/columns/cascade.d.ts +24 -0
- package/dist/core/src/columns/cascade.js +93 -0
- package/dist/core/src/columns/deriveOperators.d.ts +38 -0
- package/dist/core/src/columns/deriveOperators.js +94 -0
- package/dist/core/src/columns/expandSorting.d.ts +19 -0
- package/dist/core/src/columns/expandSorting.js +52 -0
- package/dist/core/src/columns/helper.d.ts +196 -0
- package/dist/core/src/columns/helper.js +250 -0
- package/dist/core/src/columns/labels.d.ts +12 -0
- package/dist/core/src/columns/labels.js +21 -0
- package/dist/core/src/contract.d.ts +12 -0
- package/dist/core/src/contract.js +12 -0
- package/dist/core/src/contracts/aiColumnMeta.d.ts +102 -0
- package/dist/core/src/contracts/aiColumnMeta.js +197 -0
- package/dist/core/src/contracts/describeColumns.d.ts +48 -0
- package/dist/core/src/contracts/describeColumns.js +40 -0
- package/dist/core/src/contracts/exportData.d.ts +150 -0
- package/dist/core/src/contracts/exportData.js +228 -0
- package/dist/core/src/contracts/index.d.ts +6 -0
- package/dist/core/src/contracts/index.js +12 -0
- package/dist/core/src/contracts/legacyUrl.d.ts +47 -0
- package/dist/core/src/contracts/legacyUrl.js +156 -0
- package/dist/core/src/contracts/preset.d.ts +104 -0
- package/dist/core/src/contracts/preset.js +141 -0
- package/dist/core/src/contracts/presetStore.d.ts +81 -0
- package/dist/core/src/contracts/presetStore.js +146 -0
- package/dist/core/src/contracts/usePresets.d.ts +51 -0
- package/dist/core/src/contracts/usePresets.js +170 -0
- package/dist/core/src/defaults.d.ts +7 -0
- package/dist/core/src/defaults.js +9 -0
- package/dist/core/src/filters/codec.d.ts +20 -0
- package/dist/core/src/filters/codec.js +61 -0
- package/dist/core/src/filters/date.d.ts +30 -0
- package/dist/core/src/filters/date.js +179 -0
- package/dist/core/src/filters/describe.d.ts +45 -0
- package/dist/core/src/filters/describe.js +189 -0
- package/dist/core/src/filters/kind.d.ts +52 -0
- package/dist/core/src/filters/kind.js +110 -0
- package/dist/core/src/filters/kinds/_list.d.ts +13 -0
- package/dist/core/src/filters/kinds/_list.js +42 -0
- package/dist/core/src/filters/kinds/boolean.d.ts +3 -0
- package/dist/core/src/filters/kinds/boolean.js +21 -0
- package/dist/core/src/filters/kinds/cascade.d.ts +6 -0
- package/dist/core/src/filters/kinds/cascade.js +16 -0
- package/dist/core/src/filters/kinds/commonCode.d.ts +3 -0
- package/dist/core/src/filters/kinds/commonCode.js +13 -0
- package/dist/core/src/filters/kinds/date.d.ts +9 -0
- package/dist/core/src/filters/kinds/date.js +32 -0
- package/dist/core/src/filters/kinds/enum.d.ts +4 -0
- package/dist/core/src/filters/kinds/enum.js +13 -0
- package/dist/core/src/filters/kinds/index.d.ts +49 -0
- package/dist/core/src/filters/kinds/index.js +39 -0
- package/dist/core/src/filters/kinds/linear.d.ts +5 -0
- package/dist/core/src/filters/kinds/linear.js +16 -0
- package/dist/core/src/filters/kinds/number.d.ts +5 -0
- package/dist/core/src/filters/kinds/number.js +19 -0
- package/dist/core/src/filters/kinds/period.d.ts +4 -0
- package/dist/core/src/filters/kinds/period.js +17 -0
- package/dist/core/src/filters/kinds/relation.d.ts +5 -0
- package/dist/core/src/filters/kinds/relation.js +17 -0
- package/dist/core/src/filters/kinds/schedule.d.ts +5 -0
- package/dist/core/src/filters/kinds/schedule.js +16 -0
- package/dist/core/src/filters/kinds/text.d.ts +4 -0
- package/dist/core/src/filters/kinds/text.js +18 -0
- package/dist/core/src/filters/linear.d.ts +33 -0
- package/dist/core/src/filters/linear.js +65 -0
- package/dist/core/src/filters/list.d.ts +32 -0
- package/dist/core/src/filters/list.js +87 -0
- package/dist/core/src/filters/number.d.ts +17 -0
- package/dist/core/src/filters/number.js +82 -0
- package/dist/core/src/filters/registry.d.ts +23 -0
- package/dist/core/src/filters/registry.js +121 -0
- package/dist/core/src/filters/relation.d.ts +26 -0
- package/dist/core/src/filters/relation.js +75 -0
- package/dist/core/src/filters/schedule.d.ts +21 -0
- package/dist/core/src/filters/schedule.js +60 -0
- package/dist/core/src/filters/text.d.ts +44 -0
- package/dist/core/src/filters/text.js +127 -0
- package/dist/core/src/hooks/index.d.ts +6 -0
- package/dist/core/src/hooks/index.js +6 -0
- package/dist/core/src/hooks/useDataTable.d.ts +13 -0
- package/dist/core/src/hooks/useDataTable.js +109 -0
- package/dist/core/src/hooks/useGrouping.d.ts +33 -0
- package/dist/core/src/hooks/useGrouping.js +57 -0
- package/dist/core/src/hooks/useSearchHistory.d.ts +20 -0
- package/dist/core/src/hooks/useSearchHistory.js +52 -0
- package/dist/core/src/hooks/useTableData.d.ts +25 -0
- package/dist/core/src/hooks/useTableData.js +49 -0
- package/dist/core/src/hooks/useTableFromDefinition.d.ts +47 -0
- package/dist/core/src/hooks/useTableFromDefinition.js +174 -0
- package/dist/core/src/hooks/useTableSearch.d.ts +10 -0
- package/dist/core/src/hooks/useTableSearch.js +434 -0
- package/dist/core/src/hooks/useUrlSync.d.ts +64 -0
- package/dist/core/src/hooks/useUrlSync.js +145 -0
- package/dist/core/src/index.d.ts +38 -0
- package/dist/core/src/index.js +43 -0
- package/dist/core/src/pivot/runClientPivot.d.ts +101 -0
- package/dist/core/src/pivot/runClientPivot.js +230 -0
- package/dist/core/src/pivot/transforms.d.ts +19 -0
- package/dist/core/src/pivot/transforms.js +105 -0
- package/dist/core/src/registry.d.ts +46 -0
- package/dist/core/src/registry.js +67 -0
- package/dist/core/src/types/column.d.ts +213 -0
- package/dist/core/src/types/column.js +1 -0
- package/dist/core/src/types/data.d.ts +63 -0
- package/dist/core/src/types/data.js +1 -0
- package/dist/core/src/types/definition.d.ts +239 -0
- package/dist/core/src/types/definition.js +59 -0
- package/dist/core/src/types/filter.d.ts +94 -0
- package/dist/core/src/types/filter.js +1 -0
- package/dist/core/src/types/filterKind.d.ts +204 -0
- package/dist/core/src/types/filterKind.js +1 -0
- package/dist/core/src/types/grouping.d.ts +80 -0
- package/dist/core/src/types/grouping.js +4 -0
- package/dist/core/src/types/index.d.ts +8 -0
- package/dist/core/src/types/index.js +8 -0
- package/dist/core/src/types/operators.d.ts +64 -0
- package/dist/core/src/types/operators.js +108 -0
- package/dist/core/src/types/request.d.ts +114 -0
- package/dist/core/src/types/request.js +1 -0
- package/dist/core/src/types/search.d.ts +112 -0
- package/dist/core/src/types/search.js +1 -0
- package/dist/core/src/types/sources.d.ts +84 -0
- package/dist/core/src/types/sources.js +1 -0
- package/dist/core/src/types/table.d.ts +40 -0
- package/dist/core/src/types/table.js +1 -0
- package/dist/core/src/utils/compare.d.ts +13 -0
- package/dist/core/src/utils/compare.js +53 -0
- package/dist/core/src/utils/dateText.d.ts +12 -0
- package/dist/core/src/utils/dateText.js +30 -0
- package/dist/core/src/utils/getValue.d.ts +9 -0
- package/dist/core/src/utils/getValue.js +32 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/mui/src/TableLayout.d.ts +75 -0
- package/dist/mui/src/TableLayout.js +162 -0
- package/dist/mui/src/TableScreen.d.ts +70 -0
- package/dist/mui/src/TableScreen.js +174 -0
- package/dist/mui/src/addons/FilterModal.d.ts +45 -0
- package/dist/mui/src/addons/FilterModal.js +39 -0
- package/dist/mui/src/addons/FilterStatusModal.d.ts +40 -0
- package/dist/mui/src/addons/FilterStatusModal.js +68 -0
- package/dist/mui/src/addons/PresetBar.d.ts +56 -0
- package/dist/mui/src/addons/PresetBar.js +145 -0
- package/dist/mui/src/addons/ShareDropdown.d.ts +35 -0
- package/dist/mui/src/addons/ShareDropdown.js +41 -0
- package/dist/mui/src/addons/searchStore.d.ts +44 -0
- package/dist/mui/src/addons/searchStore.js +112 -0
- package/dist/mui/src/addons/searchSummary.d.ts +106 -0
- package/dist/mui/src/addons/searchSummary.js +109 -0
- package/dist/mui/src/columns/displayColumns.d.ts +100 -0
- package/dist/mui/src/columns/displayColumns.js +174 -0
- package/dist/mui/src/content/CardContent.d.ts +24 -0
- package/dist/mui/src/content/CardContent.js +25 -0
- package/dist/mui/src/content/ColumnMenu.d.ts +43 -0
- package/dist/mui/src/content/ColumnMenu.js +40 -0
- package/dist/mui/src/content/ColumnVisibilityMenu.d.ts +14 -0
- package/dist/mui/src/content/ColumnVisibilityMenu.js +37 -0
- package/dist/mui/src/content/EditCell.d.ts +19 -0
- package/dist/mui/src/content/EditCell.js +61 -0
- package/dist/mui/src/content/HeaderCell.d.ts +59 -0
- package/dist/mui/src/content/HeaderCell.js +88 -0
- package/dist/mui/src/content/SelectionActionBar.d.ts +17 -0
- package/dist/mui/src/content/SelectionActionBar.js +19 -0
- package/dist/mui/src/content/SortControl.d.ts +17 -0
- package/dist/mui/src/content/SortControl.js +45 -0
- package/dist/mui/src/content/TableContent.d.ts +191 -0
- package/dist/mui/src/content/TableContent.js +525 -0
- package/dist/mui/src/content/TableErrorFallback.d.ts +24 -0
- package/dist/mui/src/content/TableErrorFallback.js +29 -0
- package/dist/mui/src/content/cellError.d.ts +13 -0
- package/dist/mui/src/content/cellError.js +20 -0
- package/dist/mui/src/content/cellRenderers.d.ts +71 -0
- package/dist/mui/src/content/cellRenderers.js +127 -0
- package/dist/mui/src/content/cellValue.d.ts +27 -0
- package/dist/mui/src/content/cellValue.js +82 -0
- package/dist/mui/src/content/columnOrder.d.ts +7 -0
- package/dist/mui/src/content/columnOrder.js +48 -0
- package/dist/mui/src/content/density.d.ts +13 -0
- package/dist/mui/src/content/density.js +12 -0
- package/dist/mui/src/content/menuItems.d.ts +118 -0
- package/dist/mui/src/content/menuItems.js +128 -0
- package/dist/mui/src/content/pinning.d.ts +17 -0
- package/dist/mui/src/content/pinning.js +65 -0
- package/dist/mui/src/exporters/index.d.ts +1 -0
- package/dist/mui/src/exporters/index.js +1 -0
- package/dist/mui/src/exporters/useExportFormatters.d.ts +13 -0
- package/dist/mui/src/exporters/useExportFormatters.js +17 -0
- package/dist/mui/src/filter/FilterArea.d.ts +40 -0
- package/dist/mui/src/filter/FilterArea.js +37 -0
- package/dist/mui/src/filter/FilterInput.d.ts +14 -0
- package/dist/mui/src/filter/FilterInput.js +18 -0
- package/dist/mui/src/filter/inputs/DateFilterInput.d.ts +14 -0
- package/dist/mui/src/filter/inputs/DateFilterInput.js +112 -0
- package/dist/mui/src/filter/inputs/LinearFilterInput.d.ts +10 -0
- package/dist/mui/src/filter/inputs/LinearFilterInput.js +35 -0
- package/dist/mui/src/filter/inputs/NumberFilterInput.d.ts +12 -0
- package/dist/mui/src/filter/inputs/NumberFilterInput.js +111 -0
- package/dist/mui/src/filter/inputs/OperatorMenu.d.ts +14 -0
- package/dist/mui/src/filter/inputs/OperatorMenu.js +25 -0
- package/dist/mui/src/filter/inputs/RelationFilterInput.d.ts +11 -0
- package/dist/mui/src/filter/inputs/RelationFilterInput.js +67 -0
- package/dist/mui/src/filter/inputs/ScheduleFilterInput.d.ts +8 -0
- package/dist/mui/src/filter/inputs/ScheduleFilterInput.js +17 -0
- package/dist/mui/src/filter/inputs/SelectFilterInput.d.ts +16 -0
- package/dist/mui/src/filter/inputs/SelectFilterInput.js +57 -0
- package/dist/mui/src/filter/inputs/TextFilterInput.d.ts +13 -0
- package/dist/mui/src/filter/inputs/TextFilterInput.js +55 -0
- package/dist/mui/src/filter/kinds.d.ts +58 -0
- package/dist/mui/src/filter/kinds.js +103 -0
- package/dist/mui/src/grouping/AggregateChip.d.ts +55 -0
- package/dist/mui/src/grouping/AggregateChip.js +128 -0
- package/dist/mui/src/grouping/AggregationChips.d.ts +49 -0
- package/dist/mui/src/grouping/AggregationChips.js +80 -0
- package/dist/mui/src/grouping/AggregationSettingsButton.d.ts +14 -0
- package/dist/mui/src/grouping/AggregationSettingsButton.js +19 -0
- package/dist/mui/src/grouping/GroupingBar.d.ts +59 -0
- package/dist/mui/src/grouping/GroupingBar.js +267 -0
- package/dist/mui/src/grouping/StepExpandButton.d.ts +12 -0
- package/dist/mui/src/grouping/StepExpandButton.js +18 -0
- package/dist/mui/src/grouping/groupRow.d.ts +68 -0
- package/dist/mui/src/grouping/groupRow.js +129 -0
- package/dist/mui/src/headers/defaultHeader.d.ts +28 -0
- package/dist/mui/src/headers/defaultHeader.js +90 -0
- package/dist/mui/src/headers/defineHeader.d.ts +21 -0
- package/dist/mui/src/headers/defineHeader.js +68 -0
- package/dist/mui/src/headers/index.d.ts +5 -0
- package/dist/mui/src/headers/index.js +3 -0
- package/dist/mui/src/headers/meta.d.ts +18 -0
- package/dist/mui/src/headers/meta.js +11 -0
- package/dist/mui/src/headers/types.d.ts +84 -0
- package/dist/mui/src/headers/types.js +1 -0
- package/dist/mui/src/index.d.ts +110 -0
- package/dist/mui/src/index.js +79 -0
- package/dist/mui/src/layout/ResizeHandle.d.ts +30 -0
- package/dist/mui/src/layout/ResizeHandle.js +66 -0
- package/dist/mui/src/menus/columnMenu.d.ts +45 -0
- package/dist/mui/src/menus/columnMenu.js +144 -0
- package/dist/mui/src/menus/copy.d.ts +5 -0
- package/dist/mui/src/menus/copy.js +25 -0
- package/dist/mui/src/menus/index.d.ts +6 -0
- package/dist/mui/src/menus/index.js +4 -0
- package/dist/mui/src/menus/resolve.d.ts +15 -0
- package/dist/mui/src/menus/resolve.js +60 -0
- package/dist/mui/src/menus/rowMenu.d.ts +46 -0
- package/dist/mui/src/menus/rowMenu.js +76 -0
- package/dist/mui/src/pagination/PaginationArea.d.ts +16 -0
- package/dist/mui/src/pagination/PaginationArea.js +23 -0
- package/dist/mui/src/pivot/PivotTable.d.ts +18 -0
- package/dist/mui/src/pivot/PivotTable.js +100 -0
- package/dist/mui/src/provider/KokoTableProvider.d.ts +184 -0
- package/dist/mui/src/provider/KokoTableProvider.js +118 -0
- package/dist/mui/src/provider/labels.d.ts +85 -0
- package/dist/mui/src/provider/labels.js +104 -0
- package/dist/mui/src/registry.d.ts +30 -0
- package/dist/mui/src/registry.js +5 -0
- package/dist/mui/src/toolbar/GlobalFilterField.d.ts +36 -0
- package/dist/mui/src/toolbar/GlobalFilterField.js +78 -0
- package/dist/mui/src/virtual/VirtualTableContent.d.ts +41 -0
- package/dist/mui/src/virtual/VirtualTableContent.js +58 -0
- package/dist/mui/src/virtual/VirtualTableScreen.d.ts +13 -0
- package/dist/mui/src/virtual/VirtualTableScreen.js +23 -0
- package/dist/mui/src/virtual.d.ts +4 -0
- package/dist/mui/src/virtual.js +4 -0
- package/dist/mui/src/virtualColumns/VirtualColumnCard.d.ts +26 -0
- package/dist/mui/src/virtualColumns/VirtualColumnCard.js +29 -0
- package/dist/mui/src/virtualColumns/VirtualColumnDialog.d.ts +27 -0
- package/dist/mui/src/virtualColumns/VirtualColumnDialog.js +154 -0
- package/dist/mui/src/virtualColumns/VirtualColumnSection.d.ts +31 -0
- package/dist/mui/src/virtualColumns/VirtualColumnSection.js +55 -0
- package/dist/mui/src/virtualColumns/expression.d.ts +68 -0
- package/dist/mui/src/virtualColumns/expression.js +489 -0
- package/dist/mui/src/virtualColumns/useVirtualColumns.d.ts +67 -0
- package/dist/mui/src/virtualColumns/useVirtualColumns.js +111 -0
- package/dist/mui/src/virtualColumns/virtualColumnDefs.d.ts +29 -0
- package/dist/mui/src/virtualColumns/virtualColumnDefs.js +51 -0
- package/dist/mui-virtual.d.ts +1 -0
- package/dist/mui-virtual.js +2 -0
- package/dist/mui.d.ts +1 -0
- package/dist/mui.js +2 -0
- package/dist/query/src/index.d.ts +6 -0
- package/dist/query/src/index.js +7 -0
- package/dist/query/src/serverPivot.d.ts +42 -0
- package/dist/query/src/serverPivot.js +153 -0
- package/dist/query/src/useServerGrouping.d.ts +52 -0
- package/dist/query/src/useServerGrouping.js +186 -0
- package/dist/query/src/useServerTable.d.ts +20 -0
- package/dist/query/src/useServerTable.js +106 -0
- package/dist/query/src/useServerTableData.d.ts +48 -0
- package/dist/query/src/useServerTableData.js +64 -0
- package/dist/query/src/wire.d.ts +16 -0
- package/dist/query/src/wire.js +32 -0
- package/dist/query.d.ts +1 -0
- package/dist/query.js +2 -0
- package/package.json +125 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useMemo, useState, } from "react";
|
|
3
|
+
import { Box, Button, CircularProgress, IconButton, LinearProgress, Skeleton, Stack, Table, TableBody, TableCell, TableContainer, TableFooter, TableHead, TableRow, Tooltip, Typography, } from "@mui/material";
|
|
4
|
+
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
|
5
|
+
import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight";
|
|
6
|
+
import { flexRender, } from "@tanstack/react-table";
|
|
7
|
+
import { columnMetaOf } from "../../../core/src/index";
|
|
8
|
+
import { HeaderCell, resolveCellAlign } from "./HeaderCell";
|
|
9
|
+
import { EditCell } from "./EditCell";
|
|
10
|
+
import { TableErrorFallback } from "./TableErrorFallback";
|
|
11
|
+
import { renderCellValue } from "./cellValue";
|
|
12
|
+
import { useCellRenderers } from "./cellRenderers";
|
|
13
|
+
import { useAggregations } from "../grouping/AggregateChip";
|
|
14
|
+
import { cellErrorSx, getCellError } from "./cellError";
|
|
15
|
+
import { explicitWidth, pinnedSx } from "./pinning";
|
|
16
|
+
import { resolveDensity } from "./density";
|
|
17
|
+
import { canExpandRow, isDisplayColumnId } from "../columns/displayColumns";
|
|
18
|
+
import { AggregateCellBox, GROUP_DEPTH_INDENT, GroupValueText, groupRowBgColor, groupRowHoverColor, renderAggregateCellContent, resolveGroupValue, } from "../grouping/groupRow";
|
|
19
|
+
import { resolveTableProps, useKokoTableConfig } from "../provider/KokoTableProvider";
|
|
20
|
+
import { ContextMenu, createRowMenuCtx, defaultRowMenuItems, } from "./menuItems";
|
|
21
|
+
/**
|
|
22
|
+
* 그룹 컬럼 배치 — 렌더 순서만 바꾼다.
|
|
23
|
+
* `reorder`: 왼쪽 고정 → 그룹 컬럼(grouping 순) → 나머지 → 오른쪽 고정.
|
|
24
|
+
* `remove`: 그룹 컬럼 제외.
|
|
25
|
+
* `false`: 선언 순서. `declaredRank`(선언 순서 + `columnOrder`, 그룹핑 무시)를 주면 그 순서로 되돌린다 —
|
|
26
|
+
* TanStack 은 `grouping` 상태가 있으면 자체 `groupedColumnMode`(기본 `reorder`)로 이미 그룹 컬럼을
|
|
27
|
+
* 앞으로 당겨 놓기 때문에, "그대로" 를 그리려면 되돌려야 한다.
|
|
28
|
+
* 고정 컬럼은 sticky 오프셋(`getStart`)이 TanStack 순서 기준이므로 자리를 옮기지 않는다.
|
|
29
|
+
*/
|
|
30
|
+
export function arrangeGroupedColumns(columns, grouping, mode, declaredRank) {
|
|
31
|
+
if (grouping.length === 0)
|
|
32
|
+
return columns;
|
|
33
|
+
const groupSet = new Set(grouping);
|
|
34
|
+
if (mode === "remove")
|
|
35
|
+
return columns.filter((column) => !groupSet.has(column.id));
|
|
36
|
+
if (!mode && !declaredRank)
|
|
37
|
+
return columns;
|
|
38
|
+
const left = [];
|
|
39
|
+
const right = [];
|
|
40
|
+
const rest = [];
|
|
41
|
+
const byId = new Map();
|
|
42
|
+
for (const column of columns) {
|
|
43
|
+
const pinned = column.getIsPinned();
|
|
44
|
+
if (pinned === "left")
|
|
45
|
+
left.push(column);
|
|
46
|
+
else if (pinned === "right")
|
|
47
|
+
right.push(column);
|
|
48
|
+
else if (mode && groupSet.has(column.id))
|
|
49
|
+
byId.set(column.id, column);
|
|
50
|
+
else
|
|
51
|
+
rest.push(column);
|
|
52
|
+
}
|
|
53
|
+
if (declaredRank) {
|
|
54
|
+
const rank = (column) => declaredRank.get(column.id) ?? Number.MAX_SAFE_INTEGER;
|
|
55
|
+
rest.sort((a, b) => rank(a) - rank(b));
|
|
56
|
+
}
|
|
57
|
+
const groupColumns = grouping.map((id) => byId.get(id)).filter((c) => !!c);
|
|
58
|
+
return [...left, ...groupColumns, ...rest, ...right];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 선언 순서(+ `columnOrder` 상태) 기준 리프 컬럼 순위 — TanStack `_getOrderColumnsFn` 과 같은 규칙에서
|
|
62
|
+
* 그룹핑 재배치만 뺀 것. `getAllFlatColumns()` 는 `orderColumns` 를 거치지 않아 선언 순서가 살아 있다.
|
|
63
|
+
*/
|
|
64
|
+
function declaredLeafRank(table) {
|
|
65
|
+
const leaves = table.getAllFlatColumns().filter((column) => !column.columns || column.columns.length === 0);
|
|
66
|
+
const order = table.getState().columnOrder ?? [];
|
|
67
|
+
let ordered = leaves;
|
|
68
|
+
if (order.length > 0) {
|
|
69
|
+
const remaining = [...leaves];
|
|
70
|
+
const picked = [];
|
|
71
|
+
for (const id of order) {
|
|
72
|
+
const index = remaining.findIndex((column) => column.id === id);
|
|
73
|
+
if (index > -1)
|
|
74
|
+
picked.push(remaining.splice(index, 1)[0]);
|
|
75
|
+
}
|
|
76
|
+
ordered = [...picked, ...remaining];
|
|
77
|
+
}
|
|
78
|
+
return new Map(ordered.map((column, index) => [column.id, index]));
|
|
79
|
+
}
|
|
80
|
+
/** 열 가상화에서 비어 있는 좌·우 폭을 채우는 셀 */
|
|
81
|
+
function PadCell({ width, component = "td" }) {
|
|
82
|
+
return (_jsx(TableCell, { component: component, "aria-hidden": true, "data-testid": `virtual-pad-${component === "th" ? "head" : "cell"}`, sx: { p: 0, border: 0, width, minWidth: width, maxWidth: width } }));
|
|
83
|
+
}
|
|
84
|
+
function formatAggregate(value) {
|
|
85
|
+
if (value === null || value === undefined)
|
|
86
|
+
return "-";
|
|
87
|
+
if (typeof value === "number") {
|
|
88
|
+
return Number.isInteger(value) ? value.toLocaleString("ko-KR") : value.toFixed(2);
|
|
89
|
+
}
|
|
90
|
+
return String(value);
|
|
91
|
+
}
|
|
92
|
+
function mergeSx(...parts) {
|
|
93
|
+
const list = [];
|
|
94
|
+
for (const part of parts) {
|
|
95
|
+
if (!part)
|
|
96
|
+
continue;
|
|
97
|
+
if (Array.isArray(part))
|
|
98
|
+
list.push(...part);
|
|
99
|
+
else
|
|
100
|
+
list.push(part);
|
|
101
|
+
}
|
|
102
|
+
return list;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 콘텐츠 구역 — 테이블 본문.
|
|
106
|
+
*
|
|
107
|
+
* 로딩·에러·빈 상태를 **제자리에서** 그린다. 조회 실패를 throw 해서 상위
|
|
108
|
+
* ErrorBoundary 로 올리면 테이블 트리가 통째로 파괴되고, 리마운트가 캐시의
|
|
109
|
+
* 에러를 다시 읽어 즉시 재-throw 하므로 경계 리셋으로 복구가 안 된다.
|
|
110
|
+
*
|
|
111
|
+
* 행 선택·확장·고정·리사이즈·순서는 TanStack 상태를 그대로 그린다. 기능을 켜는 것은
|
|
112
|
+
* `useDataTable` 의 `tableOptions` (`enableRowSelection`, `enableColumnResizing`,
|
|
113
|
+
* `getRowCanExpand` …) 몫이고, 체크박스·확장 토글·행 번호·행 액션 열은
|
|
114
|
+
* `withDisplayColumns()` 로 컬럼 목록에 끼운다.
|
|
115
|
+
*/
|
|
116
|
+
export function TableContent(props) {
|
|
117
|
+
return _jsx(TableContentBase, { ...props });
|
|
118
|
+
}
|
|
119
|
+
export function TableContentBase(rawProps) {
|
|
120
|
+
const config = useKokoTableConfig();
|
|
121
|
+
const { components, labels } = config;
|
|
122
|
+
const { table, content, sort, groupItems, onToggleGroup, isGroupExpanded, onLoadMore, emptyMessage = labels.ui.empty, renderEmpty, skeletonRows = 5, onResetFilters, renderError, dense = true, density, maxHeight, stickyHeader = true, getRowProps, selectOnRowClick = false, renderDetailPanel, getDetailPanelProps, editing, renderEditDialog, fieldErrors: fieldErrorsProp, onNavigate, enableColumnMenu = true, columnMenuItems, onGroupingChange, aggregation: aggregationProp, onAggregationChange, onOpenFilter, headerCellProps, renderHeader, rowMenuItems, onExpandGroupToDepth, groupedColumnMode = "reorder", grouping: groupingProp, groupRowLayout = "columns", groupRowHeight, aggregateSelection: aggregateSelectionProp, onAggregateSelectionChange, virtual, virtualColumns, containerRef, } = resolveTableProps(rawProps, config.defaults);
|
|
123
|
+
const HeaderCellComponent = (components.HeaderCell ?? HeaderCell);
|
|
124
|
+
const EditCellComponent = (components.EditCell ?? EditCell);
|
|
125
|
+
const status = content?.getStatus();
|
|
126
|
+
const visibleLeafColumns = table.getVisibleLeafColumns();
|
|
127
|
+
const rows = table.getRowModel().rows;
|
|
128
|
+
const grouped = !!groupItems && groupItems.length > 0;
|
|
129
|
+
const headerGroupCount = table.getHeaderGroups().length;
|
|
130
|
+
/**
|
|
131
|
+
* 그룹 컬럼 배치가 실제로 적용되는 기준. 열 가상화·다단 헤더에서는 끈다 —
|
|
132
|
+
* 가상화는 가운데 컬럼 인덱스로 그리고, 다단 헤더는 colSpan 을 다시 계산할 수 없다.
|
|
133
|
+
* `null` 이면 TanStack 이 준 순서를 그대로 쓴다.
|
|
134
|
+
*
|
|
135
|
+
* TanStack 은 `grouping` 상태만 있어도 자체 `groupedColumnMode`(기본 `reorder`)로 리프 순서를
|
|
136
|
+
* 바꿔 놓는다. 그래서 `false` 도 "아무것도 안 함" 이 아니라 **선언 순서로 되돌리는** 일이다.
|
|
137
|
+
* (TanStack 옵션이 `remove` 면 컬럼 자체가 없어 여기서 되살릴 수 없다 — `tableOptions` 에서 끄면 된다.)
|
|
138
|
+
*/
|
|
139
|
+
const groupingIds = groupingProp ?? table.getState().grouping ?? [];
|
|
140
|
+
const activeGroupedMode = grouped && groupingIds.length > 0 && !virtualColumns && headerGroupCount === 1
|
|
141
|
+
? groupedColumnMode
|
|
142
|
+
: null;
|
|
143
|
+
const groupingKey = groupingIds.join("|");
|
|
144
|
+
const columnOrderState = table.getState().columnOrder;
|
|
145
|
+
const allFlatColumns = table.getAllFlatColumns();
|
|
146
|
+
const declaredRank = useMemo(() => (activeGroupedMode === null ? undefined : declaredLeafRank(table)), [table, activeGroupedMode, columnOrderState, allFlatColumns]);
|
|
147
|
+
const leafColumns = useMemo(() => activeGroupedMode === null
|
|
148
|
+
? visibleLeafColumns
|
|
149
|
+
: arrangeGroupedColumns(visibleLeafColumns, groupingIds, activeGroupedMode, declaredRank), [visibleLeafColumns, groupingKey, activeGroupedMode, declaredRank]);
|
|
150
|
+
/**
|
|
151
|
+
* 열 가상화 — 고정(left/right) 컬럼은 전부, 가운데 컬럼은 보이는 범위만 그린다.
|
|
152
|
+
* 그려지지 않은 폭은 좌·우 패딩 셀 하나씩이 대신 차지해 스크롤 폭·고정 오프셋이 유지된다.
|
|
153
|
+
* 컬럼 그룹 헤더(2단 이상)는 colSpan 을 맞출 수 없어 가상화하지 않는다.
|
|
154
|
+
*/
|
|
155
|
+
const columnLayout = useMemo(() => {
|
|
156
|
+
const headerGroups = table.getHeaderGroups();
|
|
157
|
+
if (!virtualColumns || headerGroups.length !== 1)
|
|
158
|
+
return null;
|
|
159
|
+
const center = table.getCenterVisibleLeafColumns();
|
|
160
|
+
const items = virtualColumns.items;
|
|
161
|
+
const padLeft = items.length > 0 ? items[0].start : 0;
|
|
162
|
+
const padRight = items.length > 0 ? virtualColumns.totalSize - items[items.length - 1].end : 0;
|
|
163
|
+
return {
|
|
164
|
+
left: table.getLeftVisibleLeafColumns(),
|
|
165
|
+
center: items.map((item) => center[item.index]).filter(Boolean),
|
|
166
|
+
right: table.getRightVisibleLeafColumns(),
|
|
167
|
+
padLeft,
|
|
168
|
+
padRight,
|
|
169
|
+
};
|
|
170
|
+
}, [table, virtualColumns, leafColumns]);
|
|
171
|
+
/** 실제로 그려지는 열 수 — colSpan 이 이 값을 넘으면 표가 넓어진다 */
|
|
172
|
+
const columnCount = columnLayout
|
|
173
|
+
? columnLayout.left.length + columnLayout.center.length + columnLayout.right.length
|
|
174
|
+
+ (columnLayout.padLeft > 0 ? 1 : 0) + (columnLayout.padRight > 0 ? 1 : 0)
|
|
175
|
+
: leafColumns.length;
|
|
176
|
+
const renderedColumns = columnLayout
|
|
177
|
+
? [...columnLayout.left, ...columnLayout.center, ...columnLayout.right]
|
|
178
|
+
: leafColumns;
|
|
179
|
+
const spec = resolveDensity(density, dense);
|
|
180
|
+
const resizingEnabled = !!table.options.enableColumnResizing;
|
|
181
|
+
const fieldErrors = fieldErrorsProp ?? table.fieldErrors;
|
|
182
|
+
// Provider 레지스트리(셀·집계)는 내장 세트 위에 얹힌 것 — 예전 맵(`config.cellRenderers`)도 같은 레지스트리에 들어간다
|
|
183
|
+
const cellRegistry = useCellRenderers();
|
|
184
|
+
const aggregationRegistry = useAggregations();
|
|
185
|
+
const cellOptions = useMemo(() => ({ onNavigate, renderers: config.cellRenderers, registry: cellRegistry }), [onNavigate, config.cellRenderers, cellRegistry]);
|
|
186
|
+
// ── 행 컨텍스트 메뉴 ──
|
|
187
|
+
const [rowMenu, setRowMenu] = useState(null);
|
|
188
|
+
const openRowMenu = (event, partial) => {
|
|
189
|
+
if (event.defaultPrevented)
|
|
190
|
+
return;
|
|
191
|
+
const defaultItems = defaultRowMenuItems(partial, labels, config.menus.row);
|
|
192
|
+
const ctx = { ...createRowMenuCtx(partial, labels), defaultItems };
|
|
193
|
+
const items = rowMenuItems ? rowMenuItems(ctx) : defaultItems;
|
|
194
|
+
if (items.length === 0)
|
|
195
|
+
return;
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
event.stopPropagation();
|
|
198
|
+
setRowMenu({ position: { top: event.clientY, left: event.clientX }, ctx, items });
|
|
199
|
+
};
|
|
200
|
+
// ── 그룹 행 집계 함수 선택 ──
|
|
201
|
+
const [internalSelection, setInternalSelection] = useState({});
|
|
202
|
+
const aggregateSelection = aggregateSelectionProp ?? internalSelection;
|
|
203
|
+
const selectAggregate = (columnId, fn) => {
|
|
204
|
+
onAggregateSelectionChange?.(columnId, fn);
|
|
205
|
+
if (aggregateSelectionProp === undefined)
|
|
206
|
+
setInternalSelection((prev) => ({ ...prev, [columnId]: fn }));
|
|
207
|
+
};
|
|
208
|
+
// ── 편집 상태 ──
|
|
209
|
+
const [editingCell, setEditingCell] = useState(null);
|
|
210
|
+
const [internalEditingRowId, setInternalEditingRowId] = useState(null);
|
|
211
|
+
const [modalRow, setModalRow] = useState(null);
|
|
212
|
+
const editingRowId = editing?.editingRowId !== undefined ? editing.editingRowId : internalEditingRowId;
|
|
213
|
+
const commit = (value, row, columnId) => {
|
|
214
|
+
if (editing?.onCommit) {
|
|
215
|
+
editing.onCommit(value, row, columnId);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const editSpec = columnMetaOf(row.getAllCells().find((c) => c.column.id === columnId)?.column.columnDef)?.edit;
|
|
219
|
+
if (editSpec?.onCommit) {
|
|
220
|
+
editSpec.onCommit(value, row.original);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
row.original[columnId] = value;
|
|
224
|
+
};
|
|
225
|
+
const enterRowEdit = (row) => {
|
|
226
|
+
editing?.onEditRow?.(row);
|
|
227
|
+
if (editing?.mode === "row" && editing.editingRowId === undefined)
|
|
228
|
+
setInternalEditingRowId(row.id);
|
|
229
|
+
if (editing?.mode === "modal" && renderEditDialog)
|
|
230
|
+
setModalRow(row);
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* 원본 행 객체 → TanStack Row.
|
|
234
|
+
*
|
|
235
|
+
* 그룹 트리의 말단은 **그룹 순서**로 정렬돼 있고 `table.getRowModel().rows` 는
|
|
236
|
+
* 원본(정렬) 순서다. 순번으로 맞추면 펼친 그룹 아래에 다른 그룹의 행이 뜬다.
|
|
237
|
+
* 객체 동일성으로 찾아야 한다.
|
|
238
|
+
*/
|
|
239
|
+
const rowByOriginal = useMemo(() => {
|
|
240
|
+
const map = new Map();
|
|
241
|
+
for (const row of rows)
|
|
242
|
+
map.set(row.original, row);
|
|
243
|
+
return map;
|
|
244
|
+
}, [rows]);
|
|
245
|
+
const renderCell = (cell, row, index, indent, rowEditing) => {
|
|
246
|
+
const column = cell.column;
|
|
247
|
+
const meta = columnMetaOf(column.columnDef);
|
|
248
|
+
const isDisplay = isDisplayColumnId(column.id);
|
|
249
|
+
const editSpec = meta?.edit;
|
|
250
|
+
const editable = !!editing && !!editSpec?.enabled && !isDisplay;
|
|
251
|
+
const isEditingCell = editable && (editing.mode === "cell"
|
|
252
|
+
? editingCell?.rowId === row.id && editingCell.columnId === column.id
|
|
253
|
+
: editing.mode === "row" && rowEditing);
|
|
254
|
+
const error = fieldErrors ? getCellError(fieldErrors, row.index, column.id) : { hasError: false };
|
|
255
|
+
// 열 가상화는 모든 셀에 명시 폭이 있어야 패딩 셀과 합이 맞는다
|
|
256
|
+
const width = columnLayout ? column.getSize() : explicitWidth(column, table);
|
|
257
|
+
const sx = mergeSx(index === 0 && indent > 0 && { pl: 2 + indent * 2 }, meta?.ellipsis && { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: width ?? 200 }, spec.cellPy !== undefined && { py: spec.cellPy }, spec.cellPx !== undefined && { px: spec.cellPx }, width !== undefined && { width, minWidth: width, ...(resizingEnabled ? { maxWidth: width } : {}) }, pinnedSx(column, 1), editable && editing.mode === "cell" && !isEditingCell && {
|
|
258
|
+
cursor: "cell", "&:hover": { backgroundColor: "action.hover" },
|
|
259
|
+
}, isEditingCell && { py: 0.25 }, error.hasError && cellErrorSx);
|
|
260
|
+
const inner = isEditingCell ? (_jsx(EditCellComponent, { spec: editSpec, value: cell.getValue(), row: row.original, onCommit: (value) => {
|
|
261
|
+
commit(value, row, column.id);
|
|
262
|
+
if (editing.mode === "cell")
|
|
263
|
+
setEditingCell(null);
|
|
264
|
+
}, onCancel: () => {
|
|
265
|
+
if (editing.mode === "cell")
|
|
266
|
+
setEditingCell(null);
|
|
267
|
+
else if (editing.editingRowId === undefined)
|
|
268
|
+
setInternalEditingRowId(null);
|
|
269
|
+
editing.onCancel?.(row);
|
|
270
|
+
} })) : renderCellValue(cell, cellOptions);
|
|
271
|
+
const cellEl = (_jsx(TableCell, { align: isDisplay ? "center" : resolveCellAlign(column.columnDef), size: spec.size, "data-column-id": column.id, "aria-invalid": error.hasError || undefined, onDoubleClick: editable && editing.mode === "cell"
|
|
272
|
+
? (event) => { event.stopPropagation(); setEditingCell({ rowId: row.id, columnId: column.id }); }
|
|
273
|
+
: undefined, sx: sx, children: inner }, cell.id));
|
|
274
|
+
return error.hasError && error.message ? (_jsx(Tooltip, { title: error.message, arrow: true, placement: "top", children: cellEl }, cell.id)) : cellEl;
|
|
275
|
+
};
|
|
276
|
+
/** 행의 셀 — 열 가상화가 켜져 있으면 고정 + 보이는 가운데 + 패딩으로 구성한다 */
|
|
277
|
+
const renderRowCells = (row, indent, rowEditing) => {
|
|
278
|
+
if (!columnLayout) {
|
|
279
|
+
const cells = activeGroupedMode !== null
|
|
280
|
+
? arrangeGroupedColumns(row.getVisibleCells().map((cell) => ({ id: cell.column.id, getIsPinned: () => cell.column.getIsPinned(), cell })), groupingIds, activeGroupedMode, declaredRank).map((entry) => entry.cell)
|
|
281
|
+
: row.getVisibleCells();
|
|
282
|
+
return cells.map((cell, index) => renderCell(cell, row, index, indent, rowEditing));
|
|
283
|
+
}
|
|
284
|
+
const byColumn = new Map(row.getAllCells().map((cell) => [cell.column.id, cell]));
|
|
285
|
+
const cellOf = (columnId) => byColumn.get(columnId);
|
|
286
|
+
const out = [];
|
|
287
|
+
let index = 0;
|
|
288
|
+
for (const column of columnLayout.left)
|
|
289
|
+
out.push(renderCell(cellOf(column.id), row, index++, indent, rowEditing));
|
|
290
|
+
if (columnLayout.padLeft > 0)
|
|
291
|
+
out.push(_jsx(PadCell, { width: columnLayout.padLeft }, "pad-left"));
|
|
292
|
+
for (const column of columnLayout.center)
|
|
293
|
+
out.push(renderCell(cellOf(column.id), row, index++, indent, rowEditing));
|
|
294
|
+
if (columnLayout.padRight > 0)
|
|
295
|
+
out.push(_jsx(PadCell, { width: columnLayout.padRight }, "pad-right"));
|
|
296
|
+
for (const column of columnLayout.right)
|
|
297
|
+
out.push(renderCell(cellOf(column.id), row, index++, indent, rowEditing));
|
|
298
|
+
return out;
|
|
299
|
+
};
|
|
300
|
+
const renderLeafRow = (row, key, indent = 0, virtualIndex) => {
|
|
301
|
+
const rowProps = getRowProps?.(row, table) ?? {};
|
|
302
|
+
const { sx: rowSx, onClick: rowOnClick, onDoubleClick: rowOnDoubleClick, onContextMenu: rowOnContextMenu, selected, hover, ...restRowProps } = rowProps;
|
|
303
|
+
const rowEditing = editing?.mode === "row" && editingRowId === row.id;
|
|
304
|
+
const expanded = !!renderDetailPanel && canExpandRow(row, table) && row.getIsExpanded();
|
|
305
|
+
const clickable = selectOnRowClick && row.getCanSelect();
|
|
306
|
+
const onClick = (event) => {
|
|
307
|
+
rowOnClick?.(event);
|
|
308
|
+
if (clickable && !event.defaultPrevented)
|
|
309
|
+
row.toggleSelected();
|
|
310
|
+
};
|
|
311
|
+
const onDoubleClick = (event) => {
|
|
312
|
+
rowOnDoubleClick?.(event);
|
|
313
|
+
if (editing && (editing.mode === "row" || editing.mode === "modal") && !event.defaultPrevented)
|
|
314
|
+
enterRowEdit(row);
|
|
315
|
+
};
|
|
316
|
+
const onContextMenu = (event) => {
|
|
317
|
+
rowOnContextMenu?.(event);
|
|
318
|
+
openRowMenu(event, { row, table });
|
|
319
|
+
};
|
|
320
|
+
return (_jsxs(Fragment, { children: [_jsx(TableRow, { hover: hover ?? true, selected: selected ?? row.getIsSelected(), "data-index": virtualIndex, "data-row-id": row.id, ref: virtual?.measureElement, ...restRowProps, onClick: onClick, onDoubleClick: onDoubleClick, onContextMenu: onContextMenu, sx: mergeSx(clickable && { cursor: "pointer" }, rowEditing && { backgroundColor: "action.selected" }, rowSx), children: renderRowCells(row, indent, rowEditing) }), expanded && (() => {
|
|
321
|
+
const panelProps = getDetailPanelProps?.(row, table);
|
|
322
|
+
const { sx: panelRowSx, ...panelRowRest } = panelProps?.row ?? {};
|
|
323
|
+
const { sx: panelCellSx, ...panelCellRest } = panelProps?.cell ?? {};
|
|
324
|
+
const panelContent = renderDetailPanel(row, table);
|
|
325
|
+
if (components.DetailPanelRow) {
|
|
326
|
+
return components.DetailPanelRow({ row, table, columnCount, content: panelContent });
|
|
327
|
+
}
|
|
328
|
+
return (_jsx(TableRow, { className: "koko-detail-row", "data-row-id": row.id, ...panelRowRest, sx: panelRowSx, children: _jsx(TableCell, { colSpan: columnCount, ...panelCellRest, sx: mergeSx({ p: 0, borderBottom: "1px solid", borderColor: "divider" }, panelCellSx), children: _jsx(Box, { sx: { p: 2, backgroundColor: "background.default", borderTop: "1px solid", borderColor: "divider" }, children: panelContent }) }) }));
|
|
329
|
+
})()] }, key));
|
|
330
|
+
};
|
|
331
|
+
/**
|
|
332
|
+
* 그룹 행.
|
|
333
|
+
*
|
|
334
|
+
* `groupRowLayout="columns"`(기본) 는 ERP 의 `ReorderGroupRow`/`RemoveGroupRow` 와 같은 배치다 —
|
|
335
|
+
* 현재 depth 의 그룹 컬럼 셀에 펼침 아이콘 + 그룹값 + `(n)`, 다른 그룹 컬럼은 빈 셀,
|
|
336
|
+
* 나머지 컬럼에는 그 컬럼의 집계 칩. 그룹 컬럼이 그려지지 않는 경우(`remove` 모드·숨김)는 첫 데이터
|
|
337
|
+
* 컬럼 셀이 그룹값을 맡는다. 열 가상화가 켜져 있으면 말단 행과 같은 `PadCell` 규칙으로 채운다.
|
|
338
|
+
* 배경은 depth 별 단계 색이고 sticky 는 하지 않는다 — 가상화의 위치 계산과 충돌한다.
|
|
339
|
+
*/
|
|
340
|
+
const renderGroupRow = (item, virtualIndex) => {
|
|
341
|
+
const node = item.node;
|
|
342
|
+
const depth = item.depth;
|
|
343
|
+
const expanded = isGroupExpanded?.(node) ?? false;
|
|
344
|
+
const toggle = () => onToggleGroup?.(node);
|
|
345
|
+
const groupColumnId = groupingIds[depth] ?? node.groupKey;
|
|
346
|
+
const groupColumn = table.getAllFlatColumns().find((column) => column.id === groupColumnId);
|
|
347
|
+
const groupSet = new Set(groupingIds.length > 0 ? groupingIds : [node.groupKey]);
|
|
348
|
+
const toggleButton = (_jsx(IconButton, { size: "small", "aria-label": expanded ? labels.ui.collapse : labels.ui.expand, sx: { p: 0.25, color: expanded ? "primary.main" : "action.active" },
|
|
349
|
+
// 행에도 핸들러가 있으므로 버블링을 막지 않으면 두 번 토글된다
|
|
350
|
+
onClick: (event) => {
|
|
351
|
+
event.stopPropagation();
|
|
352
|
+
toggle();
|
|
353
|
+
}, children: node.isPending
|
|
354
|
+
? _jsx(CircularProgress, { size: 14 })
|
|
355
|
+
: expanded ? _jsx(KeyboardArrowDownIcon, { fontSize: "small" }) : _jsx(KeyboardArrowRightIcon, { fontSize: "small" }) }));
|
|
356
|
+
const groupValue = (_jsxs(_Fragment, { children: [_jsx(GroupValueText, { children: resolveGroupValue(node, groupColumn) }), _jsxs(Typography, { component: "span", variant: "caption", color: "text.secondary", sx: { whiteSpace: "nowrap" }, children: ["(", node.count, ")"] })] }));
|
|
357
|
+
const rowProps = {
|
|
358
|
+
"data-index": virtualIndex,
|
|
359
|
+
"data-group-depth": depth,
|
|
360
|
+
ref: virtual?.measureElement,
|
|
361
|
+
// 화살표만 누를 수 있으면 표적이 너무 작다 — 행 전체를 누를 수 있게 한다
|
|
362
|
+
onClick: toggle,
|
|
363
|
+
onKeyDown: (event) => {
|
|
364
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
365
|
+
event.preventDefault();
|
|
366
|
+
toggle();
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
tabIndex: onToggleGroup ? 0 : -1,
|
|
370
|
+
onContextMenu: (event) => openRowMenu(event, {
|
|
371
|
+
table,
|
|
372
|
+
group: {
|
|
373
|
+
node, depth, expanded, toggle,
|
|
374
|
+
subLevels: groupingIds.slice(depth + 1),
|
|
375
|
+
expandToDepth: onExpandGroupToDepth ? (target) => onExpandGroupToDepth(target, node) : undefined,
|
|
376
|
+
},
|
|
377
|
+
}),
|
|
378
|
+
sx: mergeSx({ backgroundColor: groupRowBgColor(depth) }, groupRowHeight !== undefined && { height: groupRowHeight }, !!onToggleGroup && { cursor: "pointer", "&:hover": { backgroundColor: groupRowHoverColor(depth) } }),
|
|
379
|
+
};
|
|
380
|
+
const rowKey = `group-${node.path.map((p) => String(p.value)).join("/")}`;
|
|
381
|
+
if (components.GroupRow) {
|
|
382
|
+
return (_jsx(Fragment, { children: components.GroupRow({
|
|
383
|
+
node, depth, columns: renderedColumns, aggregates: node.aggregates ?? {}, toggle, expanded, table,
|
|
384
|
+
columnCount, groupColumn, groupValue: resolveGroupValue(node, groupColumn), rowProps,
|
|
385
|
+
}) }, rowKey));
|
|
386
|
+
}
|
|
387
|
+
// 그룹값을 맡을 셀 — 그룹 컬럼이 그려지면 그 셀, 아니면(remove·숨김) 첫 데이터 컬럼
|
|
388
|
+
const dataColumns = renderedColumns.filter((column) => !isDisplayColumnId(column.id));
|
|
389
|
+
const hostId = dataColumns.some((column) => column.id === groupColumnId)
|
|
390
|
+
? groupColumnId
|
|
391
|
+
: (dataColumns.find((column) => !groupSet.has(column.id)) ?? dataColumns[0])?.id;
|
|
392
|
+
if (groupRowLayout === "inline" || hostId === undefined) {
|
|
393
|
+
const aggregates = node.aggregates ?? {};
|
|
394
|
+
return (_jsx(TableRow, { ...rowProps, children: _jsx(TableCell, { colSpan: columnCount, size: spec.size, sx: { pl: 1 + depth * GROUP_DEPTH_INDENT, py: 0.5 }, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { flexWrap: "wrap", rowGap: 0.25 }, children: [toggleButton, groupValue, Object.entries(aggregates).map(([columnId, perFn]) => Object.entries(perFn).map(([fn, value]) => (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: [columnId, " ", labels.aggregationsShort[fn] ?? fn, " ", formatAggregate(value)] }, `${columnId}-${fn}`))))] }) }) }, rowKey));
|
|
395
|
+
}
|
|
396
|
+
const groupCell = (column) => {
|
|
397
|
+
const width = columnLayout ? column.getSize() : explicitWidth(column, table);
|
|
398
|
+
const pinned = !!column.getIsPinned();
|
|
399
|
+
const base = mergeSx({ py: 0.5 }, spec.cellPx !== undefined && { px: spec.cellPx }, width !== undefined && { width, minWidth: width, ...(resizingEnabled || columnLayout ? { maxWidth: width } : {}) }, pinnedSx(column, 1),
|
|
400
|
+
// 고정 셀은 sticky 배경이 필요하다 — 행 배경 대신 같은 단계 색을 준다
|
|
401
|
+
pinned && { backgroundColor: groupRowBgColor(depth) });
|
|
402
|
+
if (column.id === hostId) {
|
|
403
|
+
return (_jsx(TableCell, { size: spec.size, "data-column-id": column.id, "data-group-cell": "value", sx: base, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, sx: { pl: depth * GROUP_DEPTH_INDENT, minWidth: 0 }, children: [toggleButton, groupValue] }) }, column.id));
|
|
404
|
+
}
|
|
405
|
+
if (isDisplayColumnId(column.id)) {
|
|
406
|
+
return _jsx(TableCell, { size: spec.size, "data-column-id": column.id, "data-group-cell": "display", sx: base }, column.id);
|
|
407
|
+
}
|
|
408
|
+
if (groupSet.has(column.id)) {
|
|
409
|
+
return (_jsx(TableCell, { size: spec.size, "data-column-id": column.id, "data-group-cell": "empty", sx: mergeSx(base, { borderBottom: "none" }) }, column.id));
|
|
410
|
+
}
|
|
411
|
+
const content = renderAggregateCellContent({
|
|
412
|
+
column, node, selection: aggregateSelection, onSelect: selectAggregate, chip: components.AggregateChip,
|
|
413
|
+
registry: aggregationRegistry,
|
|
414
|
+
});
|
|
415
|
+
return (_jsx(TableCell, { size: spec.size, "data-column-id": column.id, "data-group-cell": "aggregate", align: "center", sx: base, children: content === null || content === undefined ? null : _jsx(AggregateCellBox, { children: content }) }, column.id));
|
|
416
|
+
};
|
|
417
|
+
let cells;
|
|
418
|
+
if (!columnLayout) {
|
|
419
|
+
cells = renderedColumns.map(groupCell);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
const out = [];
|
|
423
|
+
for (const column of columnLayout.left)
|
|
424
|
+
out.push(groupCell(column));
|
|
425
|
+
if (columnLayout.padLeft > 0)
|
|
426
|
+
out.push(_jsx(PadCell, { width: columnLayout.padLeft }, "pad-left"));
|
|
427
|
+
for (const column of columnLayout.center)
|
|
428
|
+
out.push(groupCell(column));
|
|
429
|
+
if (columnLayout.padRight > 0)
|
|
430
|
+
out.push(_jsx(PadCell, { width: columnLayout.padRight }, "pad-right"));
|
|
431
|
+
for (const column of columnLayout.right)
|
|
432
|
+
out.push(groupCell(column));
|
|
433
|
+
cells = out;
|
|
434
|
+
}
|
|
435
|
+
return _jsx(TableRow, { ...rowProps, children: cells }, rowKey);
|
|
436
|
+
};
|
|
437
|
+
/**
|
|
438
|
+
* 서버 그룹핑에서 하위가 페이지 단위로 온다. 몇 개 중 몇 개를 받았는지
|
|
439
|
+
* 같이 보여 주지 않으면 사용자가 "이게 전부인가" 를 알 수 없다.
|
|
440
|
+
*/
|
|
441
|
+
const renderLoadMoreRow = (item, virtualIndex) => (_jsx(TableRow, { "data-index": virtualIndex, ref: virtual?.measureElement, children: _jsx(TableCell, { colSpan: columnCount, size: spec.size, sx: { pl: 1 + item.depth * 2, py: 0.25 }, children: _jsx(Button, { size: "small", disabled: item.isPending, onClick: () => onLoadMore?.(item.path), children: item.isPending ? labels.ui.loading : labels.ui.loadMore(item.loaded, item.total) }) }) }, `more-${item.index}`));
|
|
442
|
+
const renderGroupItem = (item, virtualIndex) => {
|
|
443
|
+
if (item.type === "group")
|
|
444
|
+
return renderGroupRow(item, virtualIndex);
|
|
445
|
+
if (item.type === "loadMore")
|
|
446
|
+
return renderLoadMoreRow(item, virtualIndex);
|
|
447
|
+
const row = rowByOriginal.get(item.row);
|
|
448
|
+
if (row)
|
|
449
|
+
return renderLeafRow(row, `leaf-${item.index}`, item.depth, virtualIndex);
|
|
450
|
+
// 그룹 트리에는 있는데 테이블 행 모델에 없다 — 서버 그룹핑에서
|
|
451
|
+
// `useServerGrouping().rows` 를 3층에 안 넘긴 경우다.
|
|
452
|
+
// 조용히 비우면 "행이 없는 그룹" 으로 보여 원인을 찾기 어렵다.
|
|
453
|
+
return (_jsx(TableRow, { "data-index": virtualIndex, ref: virtual?.measureElement, children: _jsx(TableCell, { colSpan: columnCount, sx: { pl: 1 + item.depth * 2, py: 0.5, color: "warning.main" }, children: labels.ui.missingRows }) }, `missing-${item.index}`));
|
|
454
|
+
};
|
|
455
|
+
const renderItem = (index, virtualIndex) => grouped ? renderGroupItem(groupItems[index], virtualIndex) : renderLeafRow(rows[index], rows[index].id, 0, virtualIndex);
|
|
456
|
+
const itemCount = grouped ? groupItems.length : rows.length;
|
|
457
|
+
const renderSkeleton = () => {
|
|
458
|
+
if (components.LoadingSkeleton) {
|
|
459
|
+
const LoadingSkeleton = components.LoadingSkeleton;
|
|
460
|
+
return _jsx(LoadingSkeleton, { rows: skeletonRows, columns: renderedColumns, columnCount: columnCount, size: spec.size });
|
|
461
|
+
}
|
|
462
|
+
if (skeletonRows <= 0) {
|
|
463
|
+
return (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columnCount, sx: { py: 6, textAlign: "center", color: "text.secondary" }, children: labels.ui.loading }) }));
|
|
464
|
+
}
|
|
465
|
+
return Array.from({ length: skeletonRows }, (_, i) => (_jsx(TableRow, { "data-testid": "skeleton-row", children: renderedColumns.map((column) => (_jsx(TableCell, { size: spec.size, children: _jsx(Skeleton, { variant: "text", animation: "wave" }) }, column.id))) }, `skeleton-${i}`)));
|
|
466
|
+
};
|
|
467
|
+
const body = () => {
|
|
468
|
+
if (status?.isError) {
|
|
469
|
+
const retry = () => content?.refetch();
|
|
470
|
+
const ErrorFallback = components.ErrorFallback ?? TableErrorFallback;
|
|
471
|
+
return (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columnCount, sx: { py: 2 }, children: renderError
|
|
472
|
+
? renderError(status.error, retry)
|
|
473
|
+
: (_jsx(ErrorFallback, { compact: true, error: status.error, onRetry: content ? retry : undefined, onResetFilters: onResetFilters })) }) }));
|
|
474
|
+
}
|
|
475
|
+
if (status?.isInitialLoading)
|
|
476
|
+
return renderSkeleton();
|
|
477
|
+
if (itemCount === 0) {
|
|
478
|
+
const EmptyState = components.EmptyState;
|
|
479
|
+
return (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columnCount, sx: { py: 6, textAlign: "center", color: "text.secondary" }, children: renderEmpty
|
|
480
|
+
? renderEmpty()
|
|
481
|
+
: EmptyState
|
|
482
|
+
? _jsx(EmptyState, { message: emptyMessage, table: table, columnCount: columnCount })
|
|
483
|
+
: emptyMessage }) }));
|
|
484
|
+
}
|
|
485
|
+
if (virtual) {
|
|
486
|
+
const items = virtual.items;
|
|
487
|
+
const paddingTop = items.length > 0 ? items[0].start : 0;
|
|
488
|
+
const paddingBottom = items.length > 0 ? virtual.totalSize - items[items.length - 1].end : 0;
|
|
489
|
+
return (_jsxs(_Fragment, { children: [paddingTop > 0 && (_jsx(TableRow, { "aria-hidden": true, "data-testid": "virtual-pad-top", children: _jsx(TableCell, { colSpan: columnCount, sx: { p: 0, border: 0, height: paddingTop } }) })), items.map((item) => renderItem(item.index, item.index)), paddingBottom > 0 && (_jsx(TableRow, { "aria-hidden": true, "data-testid": "virtual-pad-bottom", children: _jsx(TableCell, { colSpan: columnCount, sx: { p: 0, border: 0, height: paddingBottom } }) }))] }));
|
|
490
|
+
}
|
|
491
|
+
return _jsx(_Fragment, { children: Array.from({ length: itemCount }, (_, index) => renderItem(index)) });
|
|
492
|
+
};
|
|
493
|
+
const hasFooter = leafColumns.some((column) => column.columnDef.footer !== undefined);
|
|
494
|
+
/**
|
|
495
|
+
* 열 가상화 헤더/푸터 — 단일 헤더 그룹의 header 를 컬럼 id 로 찾아 고정·가운데·패딩 순으로 놓는다.
|
|
496
|
+
* `HeaderCell` 은 그대로 쓴다(가상화를 몰라도 된다).
|
|
497
|
+
*/
|
|
498
|
+
const renderVirtualHeaderRow = (headers, renderOne) => {
|
|
499
|
+
if (!columnLayout)
|
|
500
|
+
return null;
|
|
501
|
+
const byColumn = new Map(headers.map((header) => [header.column.id, header]));
|
|
502
|
+
const one = (columnId) => {
|
|
503
|
+
const header = byColumn.get(columnId);
|
|
504
|
+
return header ? renderOne(header) : null;
|
|
505
|
+
};
|
|
506
|
+
return (_jsxs(_Fragment, { children: [columnLayout.left.map((column) => one(column.id)), columnLayout.padLeft > 0 && _jsx(PadCell, { component: "th", width: columnLayout.padLeft }, "pad-left"), columnLayout.center.map((column) => one(column.id)), columnLayout.padRight > 0 && _jsx(PadCell, { component: "th", width: columnLayout.padRight }, "pad-right"), columnLayout.right.map((column) => one(column.id))] }));
|
|
507
|
+
};
|
|
508
|
+
/** 그룹 컬럼 배치를 헤더·푸터 행에도 적용한다 (가상화가 아닐 때만 — 가상화는 위 함수가 맡는다) */
|
|
509
|
+
const arrangeHeaders = (headers) => {
|
|
510
|
+
if (activeGroupedMode === null)
|
|
511
|
+
return headers;
|
|
512
|
+
return arrangeGroupedColumns(headers.map((header) => ({ id: header.column.id, getIsPinned: () => header.column.getIsPinned(), header })), groupingIds, activeGroupedMode, declaredRank).map((entry) => entry.header);
|
|
513
|
+
};
|
|
514
|
+
const renderFooterCell = (header) => (_jsx(TableCell, { colSpan: header.colSpan, size: spec.size, sx: mergeSx({ fontWeight: 600, color: "text.primary", borderTop: "2px solid", borderColor: "divider", whiteSpace: "nowrap" }, columnLayout && { width: header.column.getSize(), minWidth: header.column.getSize() }, pinnedSx(header.column, 1)), children: header.isPlaceholder ? null : flexRender(header.column.columnDef.footer, header.getContext()) }, header.id));
|
|
515
|
+
const fixedLayout = resizingEnabled || !!columnLayout;
|
|
516
|
+
const renderHeaderCell = (header) => (_jsx(HeaderCellComponent, { header: header, sort: sort, table: table, enableColumnMenu: enableColumnMenu, size: spec.size, columnMenuItems: columnMenuItems, grouping: groupingProp, onGroupingChange: onGroupingChange, aggregation: aggregationProp, onAggregationChange: onAggregationChange, onOpenFilter: onOpenFilter, headerCellProps: headerCellProps ? (header, tbl) => headerCellProps(header, tbl ?? table) : undefined, renderHeader: renderHeader }, header.id));
|
|
517
|
+
return (_jsxs(Box, { sx: { position: "relative" }, children: [status?.isLoading && !status.isInitialLoading && (_jsx(LinearProgress, { sx: { position: "absolute", top: 0, left: 0, right: 0, zIndex: 2 } })), _jsx(TableContainer, { ref: containerRef, sx: { maxHeight, border: "1px solid", borderColor: "divider", borderRadius: 1 }, children: _jsxs(Table, { stickyHeader: stickyHeader, size: spec.size, sx: fixedLayout ? { tableLayout: "fixed", width: table.getTotalSize() } : undefined, children: [columnLayout && (
|
|
518
|
+
// 고정 레이아웃에서 열 폭은 첫 행(헤더)이 정한다 — 헤더 셀에 폭이 없는 컬럼(기본 150)도
|
|
519
|
+
// 패딩 셀과 합이 맞아야 하므로 colgroup 으로 폭을 못박는다
|
|
520
|
+
_jsxs("colgroup", { children: [columnLayout.left.map((column) => _jsx("col", { style: { width: column.getSize() } }, column.id)), columnLayout.padLeft > 0 && _jsx("col", { style: { width: columnLayout.padLeft } }, "pad-left"), columnLayout.center.map((column) => _jsx("col", { style: { width: column.getSize() } }, column.id)), columnLayout.padRight > 0 && _jsx("col", { style: { width: columnLayout.padRight } }, "pad-right"), columnLayout.right.map((column) => _jsx("col", { style: { width: column.getSize() } }, column.id))] })), _jsx(TableHead, { children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { children: columnLayout
|
|
521
|
+
? renderVirtualHeaderRow(headerGroup.headers, (header) => renderHeaderCell(header))
|
|
522
|
+
: arrangeHeaders(headerGroup.headers).map((header) => renderHeaderCell(header)) }, headerGroup.id))) }), _jsx(TableBody, { children: body() }), hasFooter && (_jsx(TableFooter, { children: table.getFooterGroups().map((footerGroup) => (_jsx(TableRow, { children: columnLayout
|
|
523
|
+
? renderVirtualHeaderRow(footerGroup.headers, (header) => renderFooterCell(header))
|
|
524
|
+
: arrangeHeaders(footerGroup.headers).map((header) => renderFooterCell(header)) }, footerGroup.id))) }))] }) }), modalRow && renderEditDialog && renderEditDialog(modalRow, () => setModalRow(null)), _jsx(ContextMenu, { position: rowMenu?.position ?? null, items: rowMenu?.items ?? [], ctx: rowMenu?.ctx ?? { ...createRowMenuCtx({ table }, labels), defaultItems: [] }, onClose: () => setRowMenu(null) })] }));
|
|
525
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface TableErrorFallbackProps {
|
|
3
|
+
error?: Error | null;
|
|
4
|
+
/** 같은 조건으로 다시 조회 (`data.refetch`) */
|
|
5
|
+
onRetry?: () => void;
|
|
6
|
+
/** 있으면 "필터 초기화" 버튼을 그린다 (`search.reset`) */
|
|
7
|
+
onResetFilters?: () => void;
|
|
8
|
+
/** 400 이 아닐 때의 제목. 기본 `labels.ui.loadFailed` */
|
|
9
|
+
title?: ReactNode;
|
|
10
|
+
/** 표 본문 안의 행에 들어갈 때 여백을 줄인다 */
|
|
11
|
+
compact?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** axios(`response.status`)·fetch 래퍼(`status`) 양쪽에서 HTTP 상태를 읽는다 */
|
|
14
|
+
export declare function errorStatus(error: unknown): number | undefined;
|
|
15
|
+
export declare function isBadRequestError(error: unknown): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* 조회 실패 UI — 기존 `TableErrorFallback.tsx` 대응.
|
|
18
|
+
*
|
|
19
|
+
* 400 이면 "필터를 해석할 수 없습니다"(서버가 컬럼 경로·연산자를 거부한 것) 로 구분하고,
|
|
20
|
+
* 재시도와 함께 `onResetFilters` 가 있을 때만 "필터 초기화" 를 보여 준다.
|
|
21
|
+
* ErrorBoundary 를 쓰지 않으므로 "테이블 재시작" 은 없다 — 에러는 `TableData.error` 로
|
|
22
|
+
* 내려와 제자리에서 그려지고, 재조회하면 그대로 복구된다.
|
|
23
|
+
*/
|
|
24
|
+
export declare function TableErrorFallback({ error, onRetry, onResetFilters, title, compact, }: TableErrorFallbackProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Alert, Box, Button, Stack, Typography } from "@mui/material";
|
|
3
|
+
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
|
4
|
+
import RefreshIcon from "@mui/icons-material/Refresh";
|
|
5
|
+
import { useKokoTableConfig } from "../provider/KokoTableProvider";
|
|
6
|
+
/** axios(`response.status`)·fetch 래퍼(`status`) 양쪽에서 HTTP 상태를 읽는다 */
|
|
7
|
+
export function errorStatus(error) {
|
|
8
|
+
if (!error || typeof error !== "object")
|
|
9
|
+
return undefined;
|
|
10
|
+
const e = error;
|
|
11
|
+
const status = e.status ?? e.response?.status;
|
|
12
|
+
return typeof status === "number" ? status : undefined;
|
|
13
|
+
}
|
|
14
|
+
export function isBadRequestError(error) {
|
|
15
|
+
return errorStatus(error) === 400;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 조회 실패 UI — 기존 `TableErrorFallback.tsx` 대응.
|
|
19
|
+
*
|
|
20
|
+
* 400 이면 "필터를 해석할 수 없습니다"(서버가 컬럼 경로·연산자를 거부한 것) 로 구분하고,
|
|
21
|
+
* 재시도와 함께 `onResetFilters` 가 있을 때만 "필터 초기화" 를 보여 준다.
|
|
22
|
+
* ErrorBoundary 를 쓰지 않으므로 "테이블 재시작" 은 없다 — 에러는 `TableData.error` 로
|
|
23
|
+
* 내려와 제자리에서 그려지고, 재조회하면 그대로 복구된다.
|
|
24
|
+
*/
|
|
25
|
+
export function TableErrorFallback({ error, onRetry, onResetFilters, title, compact = false, }) {
|
|
26
|
+
const { labels } = useKokoTableConfig();
|
|
27
|
+
const badRequest = isBadRequestError(error);
|
|
28
|
+
return (_jsxs(Box, { role: "alert", sx: { p: compact ? 1 : 3, textAlign: "center" }, children: [_jsxs(Alert, { severity: "error", icon: _jsx(ErrorOutlineIcon, {}), sx: { mb: compact ? 1 : 2, textAlign: "left" }, children: [_jsx(Typography, { variant: compact ? "subtitle2" : "h6", sx: { mb: 0.5 }, children: badRequest ? labels.ui.badRequest : (title ?? labels.ui.loadFailed) }), error?.message && (_jsx(Typography, { variant: "body2", color: "text.secondary", children: error.message }))] }), _jsxs(Stack, { direction: "row", spacing: 1.5, justifyContent: "center", children: [onRetry && (_jsx(Button, { size: "small", variant: "contained", startIcon: _jsx(RefreshIcon, {}), onClick: onRetry, children: labels.ui.retry })), onResetFilters && (_jsx(Button, { size: "small", variant: "outlined", onClick: onResetFilters, children: labels.ui.resetFilters }))] })] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SxProps, Theme } from "@mui/material";
|
|
2
|
+
import type { FieldErrors } from "../../../core/src/index";
|
|
3
|
+
export interface CellErrorInfo {
|
|
4
|
+
hasError: boolean;
|
|
5
|
+
message?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `fieldErrors[rowIndex][columnId]` 를 읽는다. react-hook-form `FieldErrors` 와 같은 모양이라
|
|
9
|
+
* 폼 검증 결과를 그대로 넘길 수 있다. `rowIndex` 는 TanStack `row.index`(페이지 내 순번)다.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getCellError(fieldErrors: FieldErrors, rowIndex: number, columnId: string): CellErrorInfo;
|
|
12
|
+
/** 에러 셀 스타일 — 빨간 배경 + 셀 안쪽 outline (기존 `CellError.tsx` 와 같다) */
|
|
13
|
+
export declare const cellErrorSx: SxProps<Theme>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fieldErrors[rowIndex][columnId]` 를 읽는다. react-hook-form `FieldErrors` 와 같은 모양이라
|
|
3
|
+
* 폼 검증 결과를 그대로 넘길 수 있다. `rowIndex` 는 TanStack `row.index`(페이지 내 순번)다.
|
|
4
|
+
*/
|
|
5
|
+
export function getCellError(fieldErrors, rowIndex, columnId) {
|
|
6
|
+
const cellError = fieldErrors?.[rowIndex]?.[columnId];
|
|
7
|
+
if (!cellError)
|
|
8
|
+
return { hasError: false };
|
|
9
|
+
const message = typeof cellError === "object" && "message" in cellError
|
|
10
|
+
? cellError.message
|
|
11
|
+
: undefined;
|
|
12
|
+
return { hasError: true, message };
|
|
13
|
+
}
|
|
14
|
+
/** 에러 셀 스타일 — 빨간 배경 + 셀 안쪽 outline (기존 `CellError.tsx` 와 같다) */
|
|
15
|
+
export const cellErrorSx = {
|
|
16
|
+
backgroundColor: "rgba(211, 47, 47, 0.08)",
|
|
17
|
+
outline: "1px solid",
|
|
18
|
+
outlineColor: "error.main",
|
|
19
|
+
outlineOffset: "-2px",
|
|
20
|
+
};
|