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,162 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Box, Button, Chip, Collapse, Divider, IconButton, Paper, Stack, Tooltip, Typography, useMediaQuery, useTheme, } from "@mui/material";
|
|
4
|
+
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
|
5
|
+
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
|
6
|
+
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
|
7
|
+
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
8
|
+
import FilterListIcon from "@mui/icons-material/FilterList";
|
|
9
|
+
import RestartAltIcon from "@mui/icons-material/RestartAlt";
|
|
10
|
+
import SearchIcon from "@mui/icons-material/Search";
|
|
11
|
+
import { ResizeHandle, clampWidth, useResizeWidth } from "./layout/ResizeHandle";
|
|
12
|
+
import { useKokoTableConfig } from "./provider/KokoTableProvider";
|
|
13
|
+
/** 검색·초기화 버튼 — 필터 구역 아래에 붙는다 */
|
|
14
|
+
export function SearchBar({ onSearch, onReset, isDirty, disabled }) {
|
|
15
|
+
const { labels } = useKokoTableConfig();
|
|
16
|
+
return (_jsxs(Stack, { direction: "row", spacing: 1, justifyContent: "flex-end", children: [_jsx(Button, { size: "small", startIcon: _jsx(RestartAltIcon, {}), onClick: onReset, disabled: disabled, children: labels.ui.reset }), _jsx(Button, { size: "small", variant: isDirty ? "contained" : "outlined", startIcon: _jsx(SearchIcon, {}), onClick: onSearch, disabled: disabled, children: labels.ui.search })] }));
|
|
17
|
+
}
|
|
18
|
+
/** 제목 + 좌·우 두 슬롯. 셋 다 없으면 그리지 않는다 */
|
|
19
|
+
function ToolbarRow({ title, left, right }) {
|
|
20
|
+
if (!title && !left && !right)
|
|
21
|
+
return null;
|
|
22
|
+
return (_jsxs(Stack, { direction: "row", alignItems: "center", justifyContent: "space-between", spacing: 2, sx: { flexWrap: "wrap", rowGap: 1 }, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 2, sx: { minWidth: 0, flex: "1 1 auto" }, children: [title && _jsx(Typography, { variant: "h6", sx: { whiteSpace: "nowrap" }, children: title }), left && _jsx(Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { flex: "1 1 auto", minWidth: 0 }, children: left })] }), right && _jsx(Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { flexShrink: 0 }, children: right })] }));
|
|
23
|
+
}
|
|
24
|
+
function readStored(storageKey) {
|
|
25
|
+
if (!storageKey || typeof window === "undefined")
|
|
26
|
+
return {};
|
|
27
|
+
try {
|
|
28
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
29
|
+
if (!raw)
|
|
30
|
+
return {};
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function writeStored(storageKey, patch) {
|
|
39
|
+
if (!storageKey || typeof window === "undefined")
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
window.localStorage.setItem(storageKey, JSON.stringify({ ...readStored(storageKey), ...patch }));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// 저장 실패는 조용히 — 기억 못 하는 것뿐이다
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** xs(모바일) 화면인가 — SSR 첫 렌더는 false */
|
|
49
|
+
function useIsXs() {
|
|
50
|
+
const theme = useTheme();
|
|
51
|
+
return useMediaQuery(theme.breakpoints.down("sm"), { noSsr: true });
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 접힘 상태 — 제어/비제어 겸용.
|
|
55
|
+
* 비제어이고 `defaultCollapsed` 가 없으면 xs 로 **바뀔 때** 접히고, xs 를 벗어나면 펼쳐진다.
|
|
56
|
+
*/
|
|
57
|
+
function useCollapsed(props, isXs) {
|
|
58
|
+
const { collapsible = false, collapsed: controlled, defaultCollapsed, onCollapsedChange, storageKey } = props;
|
|
59
|
+
const [internal, setInternal] = useState(() => {
|
|
60
|
+
if (!collapsible)
|
|
61
|
+
return false;
|
|
62
|
+
const stored = readStored(storageKey).collapsed;
|
|
63
|
+
if (stored !== undefined)
|
|
64
|
+
return stored;
|
|
65
|
+
return defaultCollapsed ?? isXs;
|
|
66
|
+
});
|
|
67
|
+
const isControlled = controlled !== undefined;
|
|
68
|
+
const value = collapsible ? (isControlled ? controlled : internal) : false;
|
|
69
|
+
const onChangeRef = useRef(onCollapsedChange);
|
|
70
|
+
onChangeRef.current = onCollapsedChange;
|
|
71
|
+
const set = useCallback((next) => {
|
|
72
|
+
if (!isControlled)
|
|
73
|
+
setInternal(next);
|
|
74
|
+
writeStored(storageKey, { collapsed: next });
|
|
75
|
+
onChangeRef.current?.(next);
|
|
76
|
+
}, [isControlled, storageKey]);
|
|
77
|
+
// xs 진입·이탈에 반응한다 — 첫 렌더는 초기값이 이미 반영했으므로 건너뛴다
|
|
78
|
+
const previousXs = useRef(isXs);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (previousXs.current === isXs)
|
|
81
|
+
return;
|
|
82
|
+
previousXs.current = isXs;
|
|
83
|
+
if (!collapsible || isControlled || defaultCollapsed !== undefined)
|
|
84
|
+
return;
|
|
85
|
+
setInternal(isXs);
|
|
86
|
+
onChangeRef.current?.(isXs);
|
|
87
|
+
}, [isXs, collapsible, isControlled, defaultCollapsed]);
|
|
88
|
+
return [value, set];
|
|
89
|
+
}
|
|
90
|
+
function CountChip({ count, small }) {
|
|
91
|
+
if (!count)
|
|
92
|
+
return null;
|
|
93
|
+
return (_jsx(Chip, { label: count, size: "small", color: "primary", sx: small ? { height: 18, minWidth: 18, fontSize: "0.7rem", "& .MuiChip-label": { px: 0.5 } } : { height: 20 } }));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 상단 필터 레이아웃 — 필터 / 콘텐츠 / 페이지네이션 세 구역을 세로로 쌓는다.
|
|
97
|
+
*
|
|
98
|
+
* 레이아웃은 세 구역을 **배치만** 한다. 각 구역이 무엇에 의존하는지는
|
|
99
|
+
* 구역 컴포넌트가 각자 정하므로, 여기서 테이블 인스턴스를 요구하지 않는다.
|
|
100
|
+
* `collapsible` 이면 필터 Paper 에 접기 헤더가 생기고, 접힌 상태에는 요약 문구만 남는다.
|
|
101
|
+
*/
|
|
102
|
+
export function TopFilterLayout(props) {
|
|
103
|
+
const { filter, content, pagination, toolbar, toolbarLeft, toolbarRight, title, collapsible = false, activeCount, filterTitle = "검색 조건", collapsedSummary, } = props;
|
|
104
|
+
const isXs = useIsXs();
|
|
105
|
+
const [collapsed, setCollapsed] = useCollapsed(props, isXs);
|
|
106
|
+
const summary = collapsedSummary ?? (activeCount ? `${activeCount}개 조건 적용 중` : "조건 없음");
|
|
107
|
+
return (_jsxs(Stack, { spacing: 2, children: [_jsx(ToolbarRow, { title: title, left: toolbarLeft, right: toolbarRight ?? toolbar }), filter && (_jsxs(Paper, { variant: "outlined", sx: { p: collapsible ? 0 : 2 }, children: [collapsible && (_jsxs(Box, { onClick: () => setCollapsed(!collapsed), sx: {
|
|
108
|
+
display: "flex", alignItems: "center", justifyContent: "space-between", gap: 1,
|
|
109
|
+
px: 2, py: 1, cursor: "pointer", userSelect: "none",
|
|
110
|
+
borderBottom: collapsed ? "none" : "1px solid", borderColor: "divider",
|
|
111
|
+
"&:hover": { bgcolor: "action.hover" },
|
|
112
|
+
}, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { minWidth: 0 }, children: [_jsx(FilterListIcon, { fontSize: "small", color: "action" }), _jsx(Typography, { variant: "subtitle2", sx: { whiteSpace: "nowrap" }, children: filterTitle }), _jsx(CountChip, { count: activeCount }), collapsed && (_jsx(Typography, { variant: "body2", color: "text.secondary", noWrap: true, "data-testid": "koko-collapsed-summary", children: summary }))] }), _jsx(IconButton, { size: "small", "aria-label": collapsed ? "필터 펼치기" : "필터 접기", "aria-expanded": !collapsed, onClick: (event) => { event.stopPropagation(); setCollapsed(!collapsed); }, children: collapsed ? _jsx(ExpandMoreIcon, { fontSize: "small" }) : _jsx(ExpandLessIcon, { fontSize: "small" }) })] })), collapsible ? (_jsx(Collapse, { in: !collapsed, timeout: 200, unmountOnExit: true, children: _jsx(Box, { sx: { p: 2 }, children: filter }) })) : filter] })), _jsx(Box, { children: content }), pagination] }));
|
|
113
|
+
}
|
|
114
|
+
const COLLAPSED_WIDTH = 44;
|
|
115
|
+
/**
|
|
116
|
+
* 사이드 필터 레이아웃 — 필터를 옆 패널로 뺀다 (기본 오른쪽).
|
|
117
|
+
*
|
|
118
|
+
* - `collapsible`: 패널을 세로 탭으로 접는다. xs 에서는 가로 막대가 된다
|
|
119
|
+
* - `resizable`: 안쪽 변의 드래그 핸들로 폭을 바꾼다 (`minWidth`~`maxWidth`)
|
|
120
|
+
* - `storageKey`: 접힘·폭을 localStorage 에 기억한다
|
|
121
|
+
*/
|
|
122
|
+
export function SideFilterLayout(props) {
|
|
123
|
+
const { filter, content, pagination, toolbar, toolbarLeft, toolbarRight, title, filterWidth = 280, filterPosition = "right", collapsible = false, resizable = false, minWidth = 200, maxWidth = 640, onFilterWidthChange, activeCount, filterTitle = "검색 조건", storageKey, collapsedWidth = COLLAPSED_WIDTH, } = props;
|
|
124
|
+
const isXs = useIsXs();
|
|
125
|
+
const [collapsed, setCollapsed] = useCollapsed(props, isXs);
|
|
126
|
+
// 폭: 저장값 → prop 순. prop 이 바뀌면(제어) 따라간다
|
|
127
|
+
const [width, setWidth] = useState(() => {
|
|
128
|
+
const stored = resizable ? readStored(storageKey).width : undefined;
|
|
129
|
+
return clampWidth(stored ?? filterWidth, minWidth, maxWidth);
|
|
130
|
+
});
|
|
131
|
+
const previousProp = useRef(filterWidth);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (previousProp.current === filterWidth)
|
|
134
|
+
return;
|
|
135
|
+
previousProp.current = filterWidth;
|
|
136
|
+
setWidth(clampWidth(filterWidth, minWidth, maxWidth));
|
|
137
|
+
}, [filterWidth, minWidth, maxWidth]);
|
|
138
|
+
const onWidthChangeRef = useRef(onFilterWidthChange);
|
|
139
|
+
onWidthChangeRef.current = onFilterWidthChange;
|
|
140
|
+
const handle = useResizeWidth({
|
|
141
|
+
width, min: minWidth, max: maxWidth,
|
|
142
|
+
reversed: filterPosition === "right",
|
|
143
|
+
onChange: (next) => { setWidth(next); onWidthChangeRef.current?.(next); },
|
|
144
|
+
onDragEnd: (next) => writeStored(storageKey, { width: next }),
|
|
145
|
+
});
|
|
146
|
+
const left = filterPosition === "left";
|
|
147
|
+
const expandIcon = left ? _jsx(ChevronRightIcon, { fontSize: "small" }) : _jsx(ChevronLeftIcon, { fontSize: "small" });
|
|
148
|
+
const collapseIcon = left ? _jsx(ChevronLeftIcon, { fontSize: "small" }) : _jsx(ChevronRightIcon, { fontSize: "small" });
|
|
149
|
+
const panel = filter && (collapsed ? (isXs ? (_jsxs(Button, { fullWidth: true, variant: "outlined", color: "inherit", size: "small", startIcon: _jsx(FilterListIcon, {}), onClick: () => setCollapsed(false), "aria-label": "\uD544\uD130 \uD3BC\uCE58\uAE30", "aria-expanded": false, sx: { justifyContent: "flex-start", gap: 1 }, children: [filterTitle, _jsx(CountChip, { count: activeCount, small: true })] })) : (_jsxs(Paper, { variant: "outlined", role: "button", tabIndex: 0, "aria-label": "\uD544\uD130 \uD3BC\uCE58\uAE30", "aria-expanded": false, onClick: () => setCollapsed(false), onKeyDown: (event) => { if (event.key === "Enter" || event.key === " ") {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
setCollapsed(false);
|
|
152
|
+
} }, sx: {
|
|
153
|
+
width: collapsedWidth, flexShrink: 0, alignSelf: "stretch", minHeight: 160,
|
|
154
|
+
display: "flex", flexDirection: "column", alignItems: "center", gap: 1, py: 1,
|
|
155
|
+
cursor: "pointer", "&:hover": { bgcolor: "action.hover" },
|
|
156
|
+
}, children: [_jsx(Tooltip, { title: "\uD544\uD130 \uD3BC\uCE58\uAE30", placement: left ? "right" : "left", children: _jsx("span", { children: expandIcon }) }), _jsx(FilterListIcon, { fontSize: "small", color: "action" }), _jsx(CountChip, { count: activeCount, small: true }), _jsx(Typography, { variant: "caption", color: "text.secondary", sx: { writingMode: "vertical-rl", textOrientation: "mixed", letterSpacing: 1, whiteSpace: "nowrap" }, children: filterTitle })] }))) : (_jsxs(Paper, { variant: "outlined", sx: {
|
|
157
|
+
p: 2, position: "relative", flexShrink: 0,
|
|
158
|
+
width: { xs: "100%", md: width },
|
|
159
|
+
transition: handle.isDragging ? "none" : "width 0.2s ease",
|
|
160
|
+
}, children: [resizable && (_jsx(Box, { sx: { display: { xs: "none", md: "block" } }, children: _jsx(ResizeHandle, { ...handle, position: left ? "right" : "left", label: "\uD544\uD130 \uD328\uB110 \uD3ED \uC870\uC808" }) })), _jsxs(Stack, { direction: "row", alignItems: "center", justifyContent: "space-between", sx: { mb: 1 }, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Typography, { variant: "subtitle2", children: filterTitle }), _jsx(CountChip, { count: activeCount })] }), collapsible && (_jsx(IconButton, { size: "small", onClick: () => setCollapsed(true), "aria-label": "\uD544\uD130 \uC811\uAE30", "aria-expanded": true, children: isXs ? _jsx(ExpandLessIcon, { fontSize: "small" }) : collapseIcon }))] }), _jsx(Divider, { sx: { mb: 2 } }), filter] })));
|
|
161
|
+
return (_jsxs(Stack, { spacing: 2, children: [_jsx(ToolbarRow, { title: title, left: toolbarLeft, right: toolbarRight ?? toolbar }), _jsxs(Stack, { direction: { xs: "column", md: "row" }, spacing: 2, alignItems: "flex-start", children: [left && panel, _jsxs(Box, { sx: { flex: 1, minWidth: 0, width: "100%" }, children: [content, pagination] }), !left && panel] })] }));
|
|
162
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { Row, RowData, Table as TanTable } from "@tanstack/react-table";
|
|
3
|
+
import type { TableBundle, TableDefinition } from "../../core/src/index";
|
|
4
|
+
import { type SideFilterLayoutProps, type TopFilterLayoutProps } from "./TableLayout";
|
|
5
|
+
import { type FilterAreaProps } from "./filter/FilterArea";
|
|
6
|
+
import { type TableContentProps } from "./content/TableContent";
|
|
7
|
+
import { type PaginationAreaProps } from "./pagination/PaginationArea";
|
|
8
|
+
import { type WithDisplayColumnsOptions } from "./columns/displayColumns";
|
|
9
|
+
/** 슬롯 값 — 노드를 직접 주거나, 번들을 받아 그리는 함수. `null` 이면 그 구역을 비운다 */
|
|
10
|
+
export type SlotValue<T extends RowData> = ReactNode | ((bundle: TableBundle<T>) => ReactNode);
|
|
11
|
+
export interface TableScreenSlots<T extends RowData> {
|
|
12
|
+
/** 필터 구역 전체(FilterArea + SearchBar) 대체 */
|
|
13
|
+
filter?: SlotValue<T>;
|
|
14
|
+
/** 테이블 본문 대체 — 카드 그리드 등 */
|
|
15
|
+
content?: SlotValue<T>;
|
|
16
|
+
pagination?: SlotValue<T>;
|
|
17
|
+
/** 그룹핑 바 대체 (`definition.grouping.enabled` 일 때만 그려진다) */
|
|
18
|
+
groupingBar?: SlotValue<T>;
|
|
19
|
+
/** 선택 액션바의 액션 — `null` 이면 바 자체를 뺀다 */
|
|
20
|
+
selectionActions?: ((selectedRows: T[], bundle: TableBundle<T>) => ReactNode) | null;
|
|
21
|
+
/** 0건일 때 본문 (`TableContent renderEmpty`) */
|
|
22
|
+
emptyState?: () => ReactNode;
|
|
23
|
+
/** 펼친 행 아래 패널 — 주면 확장 열이 켜진다 */
|
|
24
|
+
detailPanel?: (row: Row<T>, table: TanTable<T>) => ReactNode;
|
|
25
|
+
/** 행 액션 열 렌더 — `features.rowActions.render` 보다 우선한다 */
|
|
26
|
+
rowActions?: (row: Row<T>, table: TanTable<T>) => ReactNode;
|
|
27
|
+
}
|
|
28
|
+
type LayoutPassthrough = Partial<Omit<TopFilterLayoutProps & SideFilterLayoutProps, "filter" | "content" | "pagination" | "title" | "toolbar" | "toolbarLeft" | "toolbarRight">>;
|
|
29
|
+
export interface TableScreenProps<T extends RowData> {
|
|
30
|
+
/** `useTableFromDefinition` / `useServerTable` 결과 */
|
|
31
|
+
bundle: TableBundle<T>;
|
|
32
|
+
/** 필터 위치 (기본 top) */
|
|
33
|
+
layout?: "top" | "side";
|
|
34
|
+
title?: ReactNode;
|
|
35
|
+
/** 제목 옆 좌측 슬롯 (기존 `renderTopToolbarCustomActions`) */
|
|
36
|
+
toolbarLeft?: ReactNode;
|
|
37
|
+
/** 우측 슬롯 (기존 `renderToolbarInternalActions`) */
|
|
38
|
+
toolbarRight?: ReactNode;
|
|
39
|
+
slots?: TableScreenSlots<T>;
|
|
40
|
+
/** `TableContent` 에 그대로 얹는다 — 정의에서 파생한 값 위에 덮인다 */
|
|
41
|
+
contentProps?: Partial<Omit<TableContentProps<T>, "table">>;
|
|
42
|
+
filterProps?: Partial<Pick<FilterAreaProps<T>, "direction" | "gridCols" | "collapseDetail">>;
|
|
43
|
+
/** 레이아웃 옵션(접기·폭·storageKey …) */
|
|
44
|
+
layoutProps?: LayoutPassthrough;
|
|
45
|
+
paginationProps?: Partial<Omit<PaginationAreaProps<T>, "source">>;
|
|
46
|
+
}
|
|
47
|
+
/** 내부 — 가상화 진입점(`VirtualTableScreen`)이 본문 렌더만 바꿔 끼운다 */
|
|
48
|
+
export interface TableScreenBaseProps<T extends RowData> extends TableScreenProps<T> {
|
|
49
|
+
renderContent: (props: TableContentProps<T>) => ReactNode;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 정의의 `features` → `withDisplayColumns` 옵션.
|
|
53
|
+
* 직접 조립하는 화면이 `bundle.setTableColumns(withDisplayColumns(columns, displayColumnOptionsOf(def)))` 로 쓴다.
|
|
54
|
+
*/
|
|
55
|
+
export declare function displayColumnOptionsOf<T extends RowData>(definition: TableDefinition<T>, overrides?: {
|
|
56
|
+
rowActions?: (row: Row<T>, table: TanTable<T>) => ReactNode;
|
|
57
|
+
detailPanel?: boolean;
|
|
58
|
+
}): WithDisplayColumnsOptions<T>;
|
|
59
|
+
/**
|
|
60
|
+
* 정의 한 장으로 화면을 그린다 — 필터 / 콘텐츠 / 페이지네이션 세 구역 + 툴바 + 그룹핑 바 + 선택 액션바.
|
|
61
|
+
*
|
|
62
|
+
* - `features` 대로 표시 전용 열(`withDisplayColumns`)을 3층에 등록하고(`bundle.setTableColumns`),
|
|
63
|
+
* 서버 모드면 요청에 실린 가상 컬럼의 값 열(`virtualColumnDefs`)도 함께 끼운다
|
|
64
|
+
* - `slots` 로 구역을 통째로 바꾼다. `null` 은 비움
|
|
65
|
+
* - 가상화(`features.virtualize`)는 **여기서 그리지 않는다** — `kokotable/mui/virtual` 의
|
|
66
|
+
* `VirtualTableScreen` 을 쓴다. 메인 진입점에 virtualizer 를 두면 쓰지 않는 소비자 번들에도 들어간다
|
|
67
|
+
*/
|
|
68
|
+
export declare function TableScreen<T extends RowData>(props: TableScreenProps<T>): import("react").JSX.Element;
|
|
69
|
+
export declare function TableScreenBase<T extends RowData>({ bundle, layout, title, toolbarLeft, toolbarRight, slots, contentProps: contentOverrides, filterProps, layoutProps, paginationProps, renderContent, }: TableScreenBaseProps<T>): import("react").JSX.Element;
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { Stack } from "@mui/material";
|
|
4
|
+
import { contentSource, deriveAggregation, paginationSource, sortSource } from "../../core/src/index";
|
|
5
|
+
import { SearchBar, SideFilterLayout, TopFilterLayout, } from "./TableLayout";
|
|
6
|
+
import { FilterArea, resolveFilterGridSize } from "./filter/FilterArea";
|
|
7
|
+
import { TableContent } from "./content/TableContent";
|
|
8
|
+
import { SelectionActionBar } from "./content/SelectionActionBar";
|
|
9
|
+
import { ColumnVisibilityMenu } from "./content/ColumnVisibilityMenu";
|
|
10
|
+
import { PaginationArea } from "./pagination/PaginationArea";
|
|
11
|
+
import { GroupingBar } from "./grouping/GroupingBar";
|
|
12
|
+
import { StepExpandButton } from "./grouping/StepExpandButton";
|
|
13
|
+
import { aggregatableColumns } from "./grouping/AggregationChips";
|
|
14
|
+
import { AggregationSettingsButton } from "./grouping/AggregationSettingsButton";
|
|
15
|
+
import { GlobalFilterField } from "./toolbar/GlobalFilterField";
|
|
16
|
+
import { FilterStatusButton } from "./addons/FilterStatusModal";
|
|
17
|
+
import { countActiveConditions } from "./addons/searchSummary";
|
|
18
|
+
import { withDisplayColumns } from "./columns/displayColumns";
|
|
19
|
+
import { useVirtualColumns } from "./virtualColumns/useVirtualColumns";
|
|
20
|
+
import { virtualColumnDefs, virtualColumnFields } from "./virtualColumns/virtualColumnDefs";
|
|
21
|
+
import { VirtualColumnSection } from "./virtualColumns/VirtualColumnSection";
|
|
22
|
+
function resolveSlot(slot, bundle) {
|
|
23
|
+
return typeof slot === "function" ? slot(bundle) : slot;
|
|
24
|
+
}
|
|
25
|
+
/** 기능 토글의 **원시 값만** 추린 키 — 정의 객체가 인라인이어도 컬럼 등록이 렌더마다 반복되지 않게 */
|
|
26
|
+
function featureKey(features) {
|
|
27
|
+
const f = features ?? {};
|
|
28
|
+
const rowNumbers = typeof f.rowNumbers === "object"
|
|
29
|
+
? { id: f.rowNumbers.id, size: f.rowNumbers.size, header: typeof f.rowNumbers.header === "string" ? f.rowNumbers.header : !!f.rowNumbers.header }
|
|
30
|
+
: !!f.rowNumbers;
|
|
31
|
+
const selection = typeof f.selection === "object"
|
|
32
|
+
? { selectAll: f.selection.selectAll, multi: f.selection.multi }
|
|
33
|
+
: !!f.selection;
|
|
34
|
+
const expand = typeof f.expand === "object" ? { expandAll: f.expand.expandAll } : !!f.expand;
|
|
35
|
+
const rowActions = f.rowActions
|
|
36
|
+
? { id: f.rowActions.id, size: f.rowActions.size, position: f.rowActions.position, header: typeof f.rowActions.header === "string" ? f.rowActions.header : !!f.rowActions.header }
|
|
37
|
+
: false;
|
|
38
|
+
return JSON.stringify({ rowNumbers, selection, expand, rowActions, legacyIds: !!f.legacyIds });
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 정의의 `features` → `withDisplayColumns` 옵션.
|
|
42
|
+
* 직접 조립하는 화면이 `bundle.setTableColumns(withDisplayColumns(columns, displayColumnOptionsOf(def)))` 로 쓴다.
|
|
43
|
+
*/
|
|
44
|
+
export function displayColumnOptionsOf(definition, overrides = {}) {
|
|
45
|
+
const f = definition.features ?? {};
|
|
46
|
+
const options = { legacyIds: f.legacyIds };
|
|
47
|
+
if (f.rowNumbers)
|
|
48
|
+
options.rowNumber = typeof f.rowNumbers === "object" ? f.rowNumbers : true;
|
|
49
|
+
if (f.selection) {
|
|
50
|
+
const s = typeof f.selection === "object" ? f.selection : {};
|
|
51
|
+
options.selection = s.selectAll === undefined ? true : { selectAll: s.selectAll };
|
|
52
|
+
}
|
|
53
|
+
if (f.expand || overrides.detailPanel) {
|
|
54
|
+
const e = typeof f.expand === "object" ? f.expand : {};
|
|
55
|
+
options.expand = e.expandAll === undefined ? true : { expandAll: e.expandAll };
|
|
56
|
+
}
|
|
57
|
+
const render = overrides.rowActions ?? f.rowActions?.render;
|
|
58
|
+
if (render) {
|
|
59
|
+
const { render: _ignored, ...rest } = f.rowActions ?? { render };
|
|
60
|
+
options.rowActions = { ...rest, render };
|
|
61
|
+
}
|
|
62
|
+
return options;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 정의 한 장으로 화면을 그린다 — 필터 / 콘텐츠 / 페이지네이션 세 구역 + 툴바 + 그룹핑 바 + 선택 액션바.
|
|
66
|
+
*
|
|
67
|
+
* - `features` 대로 표시 전용 열(`withDisplayColumns`)을 3층에 등록하고(`bundle.setTableColumns`),
|
|
68
|
+
* 서버 모드면 요청에 실린 가상 컬럼의 값 열(`virtualColumnDefs`)도 함께 끼운다
|
|
69
|
+
* - `slots` 로 구역을 통째로 바꾼다. `null` 은 비움
|
|
70
|
+
* - 가상화(`features.virtualize`)는 **여기서 그리지 않는다** — `kokotable/mui/virtual` 의
|
|
71
|
+
* `VirtualTableScreen` 을 쓴다. 메인 진입점에 virtualizer 를 두면 쓰지 않는 소비자 번들에도 들어간다
|
|
72
|
+
*/
|
|
73
|
+
export function TableScreen(props) {
|
|
74
|
+
const virtualize = !!props.bundle.definition.features?.virtualize;
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (virtualize && typeof console !== "undefined") {
|
|
77
|
+
console.warn(`[koko-table] TableScreen("${props.bundle.definition.key}"): features.virtualize 는 무시됩니다 — ` +
|
|
78
|
+
'import { VirtualTableScreen } from "kokotable/mui/virtual" 을 쓰세요.');
|
|
79
|
+
}
|
|
80
|
+
}, [virtualize, props.bundle.definition.key]);
|
|
81
|
+
return _jsx(TableScreenBase, { ...props, renderContent: (contentProps) => _jsx(TableContent, { ...contentProps }) });
|
|
82
|
+
}
|
|
83
|
+
export function TableScreenBase({ bundle, layout = "top", title, toolbarLeft, toolbarRight, slots = {}, contentProps: contentOverrides, filterProps, layoutProps, paginationProps, renderContent, }) {
|
|
84
|
+
const { definition, search, data, table, grouping, mode } = bundle;
|
|
85
|
+
const features = definition.features ?? {};
|
|
86
|
+
const selection = typeof features.selection === "object" ? features.selection : (features.selection ? {} : undefined);
|
|
87
|
+
// ── 가상 컬럼 (필터 섹션 + 값 열) ──
|
|
88
|
+
const vcFeature = typeof features.virtualColumns === "object" ? features.virtualColumns : {};
|
|
89
|
+
const vcEnabled = !!features.virtualColumns;
|
|
90
|
+
const vcFields = useMemo(() => virtualColumnFields(definition.columns), [definition.columns]);
|
|
91
|
+
const vc = useVirtualColumns(search, {
|
|
92
|
+
applyImmediately: vcFeature.applyImmediately ?? true,
|
|
93
|
+
maxSlots: vcFeature.maxSlots,
|
|
94
|
+
fields: vcFields,
|
|
95
|
+
});
|
|
96
|
+
const virtualSpec = bundle.request.virtualColumns;
|
|
97
|
+
const virtualDefs = useMemo(
|
|
98
|
+
// 클라이언트 모드에는 값이 없다(2층 엔진은 식을 평가하지 않는다) — 서버 응답 값만 열로 그린다
|
|
99
|
+
() => (mode === "server" ? virtualColumnDefs(virtualSpec, { names: vc.names }) : []), [mode, virtualSpec, vc.names]);
|
|
100
|
+
// ── 표시 전용 열 등록 ──
|
|
101
|
+
// 렌더 함수는 ref 로 넘긴다 — 인라인 함수가 렌더마다 새로 만들어져도 컬럼 등록이 반복되지 않는다
|
|
102
|
+
const rowActionsRef = useRef(slots.rowActions ?? features.rowActions?.render);
|
|
103
|
+
rowActionsRef.current = slots.rowActions ?? features.rowActions?.render;
|
|
104
|
+
const hasRowActions = !!rowActionsRef.current;
|
|
105
|
+
const hasDetailPanel = !!slots.detailPanel;
|
|
106
|
+
const fKey = featureKey(features);
|
|
107
|
+
const tableColumns = useMemo(() => {
|
|
108
|
+
const options = displayColumnOptionsOf(definition, {
|
|
109
|
+
rowActions: hasRowActions ? (row, tan) => rowActionsRef.current?.(row, tan) : undefined,
|
|
110
|
+
detailPanel: hasDetailPanel,
|
|
111
|
+
});
|
|
112
|
+
return withDisplayColumns([...definition.columns, ...virtualDefs], options);
|
|
113
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
114
|
+
}, [definition.columns, virtualDefs, fKey, hasRowActions, hasDetailPanel]);
|
|
115
|
+
const { setTableColumns } = bundle;
|
|
116
|
+
useLayoutEffect(() => {
|
|
117
|
+
setTableColumns(tableColumns);
|
|
118
|
+
}, [setTableColumns, tableColumns]);
|
|
119
|
+
// ── 툴바 ──
|
|
120
|
+
const left = (features.globalFilter || toolbarLeft) ? (_jsxs(_Fragment, { children: [features.globalFilter && _jsx(GlobalFilterField, { search: search }), toolbarLeft] })) : undefined;
|
|
121
|
+
const right = (features.filterStatus || features.columnVisibilityMenu || toolbarRight) ? (_jsxs(_Fragment, { children: [toolbarRight, features.filterStatus && _jsx(FilterStatusButton, { search: search, onApply: search.apply }), features.columnVisibilityMenu && _jsx(ColumnVisibilityMenu, { table: table })] })) : undefined;
|
|
122
|
+
// ── 필터 구역 ──
|
|
123
|
+
const direction = filterProps?.direction ?? (layout === "side" ? "column" : "grid");
|
|
124
|
+
const gridSize = direction === "grid" ? resolveFilterGridSize(filterProps?.gridCols) : undefined;
|
|
125
|
+
const hasFilterFields = search.getFilterFields().length > 0;
|
|
126
|
+
let filterNode;
|
|
127
|
+
if (slots.filter !== undefined) {
|
|
128
|
+
filterNode = resolveSlot(slots.filter, bundle);
|
|
129
|
+
}
|
|
130
|
+
else if (hasFilterFields || vcEnabled) {
|
|
131
|
+
filterNode = (_jsxs(Stack, { spacing: 2, children: [_jsx(FilterArea, { source: search, direction: direction, gridCols: filterProps?.gridCols, collapseDetail: filterProps?.collapseDetail ?? true, children: vcEnabled && (_jsx(VirtualColumnSection, { virtualColumns: vc, gridSize: gridSize, enableFilter: vcFeature.enableFilter ?? true })) }), _jsx(SearchBar, { onSearch: search.apply, onReset: search.reset, isDirty: search.isDirty })] }));
|
|
132
|
+
}
|
|
133
|
+
// ── 선택 액션바 ──
|
|
134
|
+
const selectionBar = selection && slots.selectionActions !== null ? (_jsx(SelectionActionBar, { source: table, renderActions: slots.selectionActions ? (rows) => slots.selectionActions(rows, bundle) : undefined })) : null;
|
|
135
|
+
// ── 그룹핑 바 ──
|
|
136
|
+
let groupingBar = null;
|
|
137
|
+
if (grouping && slots.groupingBar !== undefined) {
|
|
138
|
+
groupingBar = resolveSlot(slots.groupingBar, bundle);
|
|
139
|
+
}
|
|
140
|
+
else if (grouping) {
|
|
141
|
+
const aggregatable = aggregatableColumns(definition.columns, { serverOnly: mode === "server" }).length > 0;
|
|
142
|
+
groupingBar = (_jsx(GroupingBar, { grouping: grouping.grouping, onChange: grouping.setGrouping, columns: definition.columns, batchSize: grouping.batchSize, onBatchSizeChange: grouping.setBatchSize, extra: _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", sx: { flexWrap: "wrap", gap: 0.5 }, children: [aggregatable && grouping.enabled && (_jsx(AggregationSettingsButton, { value: search.params.aggregation ?? deriveAggregation(definition.columns) ?? {}, onChange: (next) => search.patchParams({ aggregation: next }), columns: definition.columns, serverOnly: mode === "server" })), _jsx(StepExpandButton, { expand: grouping.expand })] }) }));
|
|
143
|
+
}
|
|
144
|
+
// ── 본문 ──
|
|
145
|
+
const groupingActive = !!grouping && grouping.enabled;
|
|
146
|
+
const contentProps = {
|
|
147
|
+
table,
|
|
148
|
+
content: contentSource(data),
|
|
149
|
+
sort: sortSource(search),
|
|
150
|
+
groupItems: groupingActive ? grouping.items : undefined,
|
|
151
|
+
onToggleGroup: grouping ? (node) => grouping.expand.toggle(node.path) : undefined,
|
|
152
|
+
isGroupExpanded: grouping ? (node) => grouping.expand.isExpanded(node.path) : undefined,
|
|
153
|
+
onLoadMore: grouping?.loadMore,
|
|
154
|
+
renderDetailPanel: slots.detailPanel,
|
|
155
|
+
selectOnRowClick: selection?.selectOnRowClick,
|
|
156
|
+
onResetFilters: search.reset,
|
|
157
|
+
renderEmpty: slots.emptyState,
|
|
158
|
+
groupedColumnMode: features.groupedColumnMode,
|
|
159
|
+
aggregation: groupingActive ? (search.params.aggregation ?? deriveAggregation(definition.columns) ?? {}) : undefined,
|
|
160
|
+
onAggregationChange: groupingActive ? (next) => search.patchParams({ aggregation: next }) : undefined,
|
|
161
|
+
density: features.density,
|
|
162
|
+
enableColumnMenu: features.columnMenu,
|
|
163
|
+
...contentOverrides,
|
|
164
|
+
};
|
|
165
|
+
const contentNode = slots.content !== undefined ? resolveSlot(slots.content, bundle) : renderContent(contentProps);
|
|
166
|
+
const contentRegion = (_jsxs(Stack, { spacing: 1, children: [selectionBar, groupingBar, contentNode] }));
|
|
167
|
+
// ── 페이지네이션 ──
|
|
168
|
+
const paginationNode = slots.pagination !== undefined
|
|
169
|
+
? resolveSlot(slots.pagination, bundle)
|
|
170
|
+
: _jsx(PaginationArea, { source: paginationSource(search, data), ...paginationProps });
|
|
171
|
+
const activeCount = countActiveConditions(search.params, search.operators);
|
|
172
|
+
const Layout = layout === "side" ? SideFilterLayout : TopFilterLayout;
|
|
173
|
+
return (_jsx(Layout, { title: title, toolbarLeft: left, toolbarRight: right, filter: filterNode, content: contentRegion, pagination: paginationNode, activeCount: activeCount, ...layoutProps }));
|
|
174
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { RowData } from "@tanstack/react-table";
|
|
3
|
+
import type { AnyFilterSource, TableSearch } from "../../../core/src/index";
|
|
4
|
+
import { type GridColCount, type GridColsConfig } from "../filter/FilterArea";
|
|
5
|
+
/** 모달이 요구하는 최소 검색 상태 — 검색·취소·초기화가 각각 부르는 메서드 */
|
|
6
|
+
export type FilterModalSearch<T extends RowData> = Pick<TableSearch<T>, "params" | "operators" | "isDirty" | "apply" | "reset" | "setDraft" | "getFilterFields">;
|
|
7
|
+
export interface FilterModalProps<T extends RowData> {
|
|
8
|
+
search: FilterModalSearch<T>;
|
|
9
|
+
/** 필터 필드 출처. 없으면 `search` (표시 전용 컬럼이 섞인 테이블을 쓰려면 `table`) */
|
|
10
|
+
source?: AnyFilterSource<T>;
|
|
11
|
+
open: boolean;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
title?: ReactNode;
|
|
14
|
+
/** 기본 column (모달은 세로 일렬). grid 면 `gridCols` 가 적용된다 */
|
|
15
|
+
direction?: "column" | "grid";
|
|
16
|
+
gridCols?: Partial<GridColsConfig> | GridColCount;
|
|
17
|
+
collapseDetail?: boolean;
|
|
18
|
+
maxWidth?: "xs" | "sm" | "md" | "lg";
|
|
19
|
+
/** FilterArea 끝에 덧붙일 항목 (가상 컬럼 섹션 등) */
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
labels?: Partial<{
|
|
22
|
+
search: string;
|
|
23
|
+
cancel: string;
|
|
24
|
+
reset: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** 초기화 뒤 모달을 닫는다 (기본 false — 비워진 필드를 확인하고 닫는다) */
|
|
27
|
+
closeOnReset?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 필터 모달 — 기존 ERP `ModalFilter` + `HeadlessFilterModal` 대응.
|
|
31
|
+
*
|
|
32
|
+
* - 검색: `apply()` 뒤 닫힘
|
|
33
|
+
* - 취소(배경 클릭·ESC 포함): `setDraft(params)` 로 편집분을 버린다
|
|
34
|
+
* - 초기화: `reset()` — 즉시 적용된다
|
|
35
|
+
*/
|
|
36
|
+
export declare function FilterModal<T extends RowData>({ search, source, open, onClose, title, direction, gridCols, collapseDetail, maxWidth, children, labels, closeOnReset, }: FilterModalProps<T>): import("react").JSX.Element;
|
|
37
|
+
export interface FilterModalButtonProps<T extends RowData> extends Omit<FilterModalProps<T>, "open" | "onClose"> {
|
|
38
|
+
/** 버튼 라벨. 없으면 아이콘 버튼 */
|
|
39
|
+
label?: ReactNode;
|
|
40
|
+
tooltip?: string;
|
|
41
|
+
size?: "small" | "medium";
|
|
42
|
+
variant?: "text" | "outlined" | "contained";
|
|
43
|
+
}
|
|
44
|
+
/** 적용 조건 수 배지가 달린 트리거 버튼 — 모달 열림 상태를 스스로 가진다 */
|
|
45
|
+
export declare function FilterModalButton<T extends RowData>({ label, tooltip, size, variant, ...modalProps }: FilterModalButtonProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Badge, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Tooltip, } from "@mui/material";
|
|
4
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
5
|
+
import FilterListIcon from "@mui/icons-material/FilterList";
|
|
6
|
+
import { FilterArea } from "../filter/FilterArea";
|
|
7
|
+
import { countActiveConditions } from "./searchSummary";
|
|
8
|
+
/**
|
|
9
|
+
* 필터 모달 — 기존 ERP `ModalFilter` + `HeadlessFilterModal` 대응.
|
|
10
|
+
*
|
|
11
|
+
* - 검색: `apply()` 뒤 닫힘
|
|
12
|
+
* - 취소(배경 클릭·ESC 포함): `setDraft(params)` 로 편집분을 버린다
|
|
13
|
+
* - 초기화: `reset()` — 즉시 적용된다
|
|
14
|
+
*/
|
|
15
|
+
export function FilterModal({ search, source, open, onClose, title = "검색 조건", direction = "column", gridCols, collapseDetail = true, maxWidth = "xs", children, labels, closeOnReset = false, }) {
|
|
16
|
+
const text = { search: "검색", cancel: "취소", reset: "초기화", ...labels };
|
|
17
|
+
const count = countActiveConditions(search.params, search.operators);
|
|
18
|
+
const cancel = () => {
|
|
19
|
+
search.setDraft(search.params);
|
|
20
|
+
onClose();
|
|
21
|
+
};
|
|
22
|
+
const apply = () => {
|
|
23
|
+
search.apply();
|
|
24
|
+
onClose();
|
|
25
|
+
};
|
|
26
|
+
const reset = () => {
|
|
27
|
+
search.reset();
|
|
28
|
+
if (closeOnReset)
|
|
29
|
+
onClose();
|
|
30
|
+
};
|
|
31
|
+
return (_jsxs(Dialog, { open: open, onClose: cancel, maxWidth: maxWidth, fullWidth: true, "aria-labelledby": "koko-filter-modal-title", children: [_jsxs(DialogTitle, { id: "koko-filter-modal-title", sx: { display: "flex", alignItems: "center", gap: 1, pr: 6 }, children: [_jsx(FilterListIcon, { fontSize: "small" }), _jsx("span", { children: title }), count > 0 && _jsx(Chip, { size: "small", color: "primary", label: `${count}개 적용 중`, sx: { height: 22 } }), _jsx(IconButton, { size: "small", onClick: cancel, "aria-label": "\uB2EB\uAE30", sx: { position: "absolute", right: 12, top: 12 }, children: _jsx(CloseIcon, { fontSize: "small" }) })] }), _jsx(DialogContent, { dividers: true, children: _jsx(FilterArea, { source: (source ?? search), direction: direction, gridCols: gridCols, collapseDetail: collapseDetail, children: children }) }), _jsxs(DialogActions, { sx: { justifyContent: "space-between" }, children: [_jsx(Button, { size: "small", color: "inherit", onClick: reset, children: text.reset }), _jsxs(Box, { sx: { display: "flex", gap: 1 }, children: [_jsx(Button, { size: "small", onClick: cancel, children: text.cancel }), _jsx(Button, { size: "small", variant: "contained", onClick: apply, children: text.search })] })] })] }));
|
|
32
|
+
}
|
|
33
|
+
/** 적용 조건 수 배지가 달린 트리거 버튼 — 모달 열림 상태를 스스로 가진다 */
|
|
34
|
+
export function FilterModalButton({ label, tooltip = "필터 설정", size = "small", variant = "outlined", ...modalProps }) {
|
|
35
|
+
const [open, setOpen] = useState(false);
|
|
36
|
+
const count = countActiveConditions(modalProps.search.params, modalProps.search.operators);
|
|
37
|
+
const trigger = label !== undefined ? (_jsx(Button, { size: size, variant: variant, startIcon: _jsx(FilterListIcon, {}), onClick: () => setOpen(true), "aria-label": tooltip, children: _jsx(Badge, { badgeContent: count, color: "primary", sx: { "& .MuiBadge-badge": { right: -14, top: 8 } }, children: label }) })) : (_jsx(IconButton, { size: size, color: "primary", onClick: () => setOpen(true), "aria-label": tooltip, children: _jsx(Badge, { badgeContent: count, color: "primary", children: _jsx(FilterListIcon, { fontSize: "small" }) }) }));
|
|
38
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tooltip, { title: tooltip, children: trigger }), _jsx(FilterModal, { ...modalProps, open: open, onClose: () => setOpen(false) })] }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { RowData } from "@tanstack/react-table";
|
|
3
|
+
import { type TableColumnDefs, type TableSearch } from "../../../core/src/index";
|
|
4
|
+
import { type SearchSummary } from "./searchSummary";
|
|
5
|
+
/** 모달·버튼·내용이 공통으로 요구하는 최소 검색 상태 */
|
|
6
|
+
export type FilterStatusSearch<T extends RowData> = Pick<TableSearch<T>, "columns" | "params" | "draft" | "operators" | "draftOperators" | "isDirty">;
|
|
7
|
+
export interface FilterStatusContentProps<T extends RowData> {
|
|
8
|
+
search: FilterStatusSearch<T>;
|
|
9
|
+
/** 헤더·선택지를 읽을 컬럼. 없으면 `search.columns` */
|
|
10
|
+
columns?: TableColumnDefs<T>;
|
|
11
|
+
/** draft 와 비교해 미적용 항목을 표시한다 (기본 true) */
|
|
12
|
+
compareDraft?: boolean;
|
|
13
|
+
/** 정렬·그룹핑·페이지(즉시 적용 항목) 를 보인다 (기본 true) */
|
|
14
|
+
showImmediate?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 적용 조건 표 — `search.params` 를 컬럼 헤더 · 연산자 라벨 · 값으로 풀어 보인다.
|
|
18
|
+
* draft 와 다른 항목은 "미적용" 칩과 함께 `적용값 → 편집값` 으로 그린다.
|
|
19
|
+
*/
|
|
20
|
+
export declare function FilterStatusContent<T extends RowData>({ search, columns, compareDraft, showImmediate, }: FilterStatusContentProps<T>): import("react").JSX.Element;
|
|
21
|
+
/** `summarizeSearch` 의 메모 버전 — 버튼 배지와 내용이 같은 요약을 본다 */
|
|
22
|
+
export declare function useFilterStatusSummary<T extends RowData>(search: FilterStatusSearch<T>, columns?: TableColumnDefs<T>, compareDraft?: boolean): SearchSummary;
|
|
23
|
+
export interface FilterStatusModalProps<T extends RowData> extends FilterStatusContentProps<T> {
|
|
24
|
+
open: boolean;
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
title?: ReactNode;
|
|
27
|
+
/** 미적용 변경이 있을 때 "편집 중인 조건 적용" 버튼을 보인다 — `search.apply` 가 필요하다 */
|
|
28
|
+
onApply?: () => void;
|
|
29
|
+
maxWidth?: "xs" | "sm" | "md";
|
|
30
|
+
}
|
|
31
|
+
/** 적용 조건 확인 모달 — 기존 ERP `FilterStatusModal` 대응 */
|
|
32
|
+
export declare function FilterStatusModal<T extends RowData>({ open, onClose, title, onApply, maxWidth, ...contentProps }: FilterStatusModalProps<T>): import("react").JSX.Element;
|
|
33
|
+
export interface FilterStatusButtonProps<T extends RowData> extends Omit<FilterStatusModalProps<T>, "open" | "onClose"> {
|
|
34
|
+
tooltip?: string;
|
|
35
|
+
size?: "small" | "medium";
|
|
36
|
+
/** 미적용 변경이 있으면 배지를 warning 색으로 (기본 true) */
|
|
37
|
+
highlightDirty?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** 적용 조건 수 배지가 달린 트리거 버튼 — 모달 열림 상태를 스스로 가진다 */
|
|
40
|
+
export declare function FilterStatusButton<T extends RowData>({ tooltip, size, highlightDirty, ...modalProps }: FilterStatusButtonProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { Badge, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, IconButton, Stack, Table, TableBody, TableCell, TableHead, TableRow, Tooltip, Typography, } from "@mui/material";
|
|
4
|
+
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
|
|
5
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
6
|
+
import VisibilityIcon from "@mui/icons-material/Visibility";
|
|
7
|
+
import { DEFAULT_DESCRIBE_LABELS } from "../../../core/src/index";
|
|
8
|
+
import { summarizeSearch, } from "./searchSummary";
|
|
9
|
+
import { useKokoTableConfig } from "../provider/KokoTableProvider";
|
|
10
|
+
const STATUS_LABEL = {
|
|
11
|
+
changed: "미적용(변경)",
|
|
12
|
+
added: "미적용(추가)",
|
|
13
|
+
removed: "미적용(삭제)",
|
|
14
|
+
};
|
|
15
|
+
function PendingChip({ status }) {
|
|
16
|
+
if (status === "applied")
|
|
17
|
+
return null;
|
|
18
|
+
return _jsx(Chip, { size: "small", color: "warning", variant: "outlined", label: STATUS_LABEL[status], sx: { height: 20 } });
|
|
19
|
+
}
|
|
20
|
+
function ValueCell({ row }) {
|
|
21
|
+
if (row.status === "applied") {
|
|
22
|
+
return _jsx(Typography, { variant: "body2", sx: { wordBreak: "break-all" }, children: row.value });
|
|
23
|
+
}
|
|
24
|
+
return (_jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", sx: { flexWrap: "wrap", rowGap: 0.5 }, children: [_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { wordBreak: "break-all" }, children: row.value }), _jsx(ArrowForwardIcon, { sx: { fontSize: 14, color: "warning.main" } }), _jsx(Typography, { variant: "body2", sx: { fontWeight: 600, wordBreak: "break-all" }, children: row.draftValue })] }));
|
|
25
|
+
}
|
|
26
|
+
function OperatorCell({ row }) {
|
|
27
|
+
const changed = row.status !== "applied" && row.draftOperator && row.draftOperator !== row.operator;
|
|
28
|
+
return (_jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", children: [_jsx(Chip, { size: "small", label: row.operatorLabel, sx: { height: 20 } }), changed && (_jsxs(_Fragment, { children: [_jsx(ArrowForwardIcon, { sx: { fontSize: 14, color: "warning.main" } }), _jsx(Chip, { size: "small", color: "warning", label: row.draftOperatorLabel, sx: { height: 20 } })] }))] }));
|
|
29
|
+
}
|
|
30
|
+
function SectionTitle({ children, extra }) {
|
|
31
|
+
return (_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", sx: { mb: 0.5 }, children: [_jsx(Typography, { variant: "subtitle2", sx: { color: "primary.main" }, children: children }), extra] }));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 적용 조건 표 — `search.params` 를 컬럼 헤더 · 연산자 라벨 · 값으로 풀어 보인다.
|
|
35
|
+
* draft 와 다른 항목은 "미적용" 칩과 함께 `적용값 → 편집값` 으로 그린다.
|
|
36
|
+
*/
|
|
37
|
+
export function FilterStatusContent({ search, columns, compareDraft = true, showImmediate = true, }) {
|
|
38
|
+
const summary = useFilterStatusSummary(search, columns, compareDraft);
|
|
39
|
+
const { conditions, globalFilter, sorting, grouping, pagination } = summary;
|
|
40
|
+
return (_jsxs(Stack, { spacing: 2, children: [_jsxs(Box, { children: [_jsx(SectionTitle, { children: "\uC804\uC5ED\uAC80\uC0C9" }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "body2", color: globalFilter.value ? "text.primary" : "text.disabled", children: globalFilter.value || "(없음)" }), globalFilter.pending && (_jsxs(_Fragment, { children: [_jsx(ArrowForwardIcon, { sx: { fontSize: 14, color: "warning.main" } }), _jsx(Typography, { variant: "body2", sx: { fontWeight: 600 }, children: globalFilter.draftValue || "(없음)" }), _jsx(PendingChip, { status: "changed" })] }))] })] }), _jsx(Divider, {}), _jsxs(Box, { children: [_jsx(SectionTitle, { extra: summary.appliedCount > 0 && _jsx(Chip, { size: "small", color: "primary", label: `${summary.appliedCount}개 적용`, sx: { height: 20 } }), children: "\uCEEC\uB7FC \uC870\uAC74" }), conditions.length === 0 ? (_jsx(Typography, { variant: "body2", color: "text.disabled", children: "\uC801\uC6A9\uB41C \uCEEC\uB7FC \uC870\uAC74 \uC5C6\uC74C" })) : (_jsx(Box, { sx: { overflowX: "auto" }, children: _jsxs(Table, { size: "small", "aria-label": "\uC801\uC6A9 \uC870\uAC74", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { sx: { whiteSpace: "nowrap" }, children: "\uCEEC\uB7FC" }), _jsx(TableCell, { sx: { whiteSpace: "nowrap" }, children: "\uC5F0\uC0B0\uC790" }), _jsx(TableCell, { children: "\uAC12" }), compareDraft && _jsx(TableCell, { sx: { whiteSpace: "nowrap" }, children: "\uC0C1\uD0DC" })] }) }), _jsx(TableBody, { children: conditions.map((row) => (_jsxs(TableRow, { "data-status": row.status, sx: row.status !== "applied" ? { bgcolor: "warning.50" } : undefined, children: [_jsx(TableCell, { sx: { whiteSpace: "nowrap", fontWeight: 500 }, children: row.header }), _jsx(TableCell, { children: _jsx(OperatorCell, { row: row }) }), _jsx(TableCell, { children: _jsx(ValueCell, { row: row }) }), compareDraft && _jsx(TableCell, { children: _jsx(PendingChip, { status: row.status }) })] }, row.id))) })] }) }))] }), showImmediate && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(Box, { children: [_jsx(SectionTitle, { children: "\uC989\uC2DC \uC801\uC6A9 \uD56D\uBAA9" }), _jsx(Typography, { variant: "caption", color: "text.secondary", component: "div", sx: { mb: 1 }, children: "\uC815\uB82C\u00B7\uADF8\uB8F9\uD551\u00B7\uD398\uC774\uC9C0\uB294 \uAC80\uC0C9 \uBC84\uD2BC \uC5C6\uC774 \uC989\uC2DC \uBC18\uC601\uB429\uB2C8\uB2E4." }), _jsxs(Stack, { direction: "row", spacing: 1, sx: { flexWrap: "wrap", rowGap: 1 }, children: [_jsx(Chip, { size: "small", variant: "outlined", label: `정렬: ${sorting.length > 0 ? sorting.map((s) => s.label).join(", ") : "(없음)"}` }), _jsx(Chip, { size: "small", variant: "outlined", label: `그룹핑: ${grouping.length > 0 ? grouping.join(" > ") : "(없음)"}` }), _jsx(Chip, { size: "small", variant: "outlined", label: `페이지: ${pagination.pageIndex + 1} / ${pagination.pageSize}개씩` })] })] })] }))] }));
|
|
41
|
+
}
|
|
42
|
+
/** `summarizeSearch` 의 메모 버전 — 버튼 배지와 내용이 같은 요약을 본다 */
|
|
43
|
+
export function useFilterStatusSummary(search, columns, compareDraft = true) {
|
|
44
|
+
const resolved = columns ?? search.columns;
|
|
45
|
+
// Provider 의 연산자 라벨을 kind describe 에도 흘린다 (다국어 통로 하나)
|
|
46
|
+
const { labels: providerLabels } = useKokoTableConfig();
|
|
47
|
+
const labels = useMemo(() => ({ ...DEFAULT_DESCRIBE_LABELS, operators: providerLabels.operators }), [providerLabels.operators]);
|
|
48
|
+
return useMemo(() => summarizeSearch({
|
|
49
|
+
columns: resolved,
|
|
50
|
+
params: search.params,
|
|
51
|
+
draft: compareDraft ? search.draft : undefined,
|
|
52
|
+
operators: search.operators,
|
|
53
|
+
draftOperators: search.draftOperators,
|
|
54
|
+
labels,
|
|
55
|
+
}), [resolved, search.params, search.draft, search.operators, search.draftOperators, compareDraft, labels]);
|
|
56
|
+
}
|
|
57
|
+
/** 적용 조건 확인 모달 — 기존 ERP `FilterStatusModal` 대응 */
|
|
58
|
+
export function FilterStatusModal({ open, onClose, title = "적용 조건", onApply, maxWidth = "sm", ...contentProps }) {
|
|
59
|
+
const { search } = contentProps;
|
|
60
|
+
return (_jsxs(Dialog, { open: open, onClose: onClose, maxWidth: maxWidth, fullWidth: true, "aria-labelledby": "koko-filter-status-title", children: [_jsxs(DialogTitle, { id: "koko-filter-status-title", sx: { display: "flex", alignItems: "center", gap: 1, pr: 6 }, children: [_jsx(VisibilityIcon, { fontSize: "small" }), _jsx("span", { children: title }), search.isDirty && _jsx(Chip, { size: "small", color: "warning", label: "\uBBF8\uC801\uC6A9 \uBCC0\uACBD \uC788\uC74C", sx: { height: 22 } }), _jsx(IconButton, { size: "small", onClick: onClose, "aria-label": "\uB2EB\uAE30", sx: { position: "absolute", right: 12, top: 12 }, children: _jsx(CloseIcon, { fontSize: "small" }) })] }), _jsx(DialogContent, { dividers: true, children: _jsx(FilterStatusContent, { ...contentProps }) }), _jsxs(DialogActions, { children: [onApply && search.isDirty && (_jsx(Button, { onClick: () => { onApply(); onClose(); }, children: "\uD3B8\uC9D1 \uC911\uC778 \uC870\uAC74 \uC801\uC6A9" })), _jsx(Button, { onClick: onClose, children: "\uB2EB\uAE30" })] })] }));
|
|
61
|
+
}
|
|
62
|
+
/** 적용 조건 수 배지가 달린 트리거 버튼 — 모달 열림 상태를 스스로 가진다 */
|
|
63
|
+
export function FilterStatusButton({ tooltip = "적용 조건 보기", size = "small", highlightDirty = true, ...modalProps }) {
|
|
64
|
+
const [open, setOpen] = useState(false);
|
|
65
|
+
const summary = useFilterStatusSummary(modalProps.search, modalProps.columns, modalProps.compareDraft ?? true);
|
|
66
|
+
const dirty = highlightDirty && modalProps.search.isDirty;
|
|
67
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tooltip, { title: dirty ? `${tooltip} (미적용 변경 있음)` : tooltip, children: _jsx(IconButton, { size: size, onClick: () => setOpen(true), "aria-label": tooltip, children: _jsx(Badge, { badgeContent: summary.appliedCount, color: dirty ? "warning" : "primary", max: 99, children: _jsx(VisibilityIcon, { fontSize: "small" }) }) }) }), _jsx(FilterStatusModal, { ...modalProps, open: open, onClose: () => setOpen(false) })] }));
|
|
68
|
+
}
|