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,30 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { RegistryPatch } from "../../core/src/index";
|
|
3
|
+
/**
|
|
4
|
+
* 확장점 공통 레지스트리 (설계 문서 4절) — 구현은 core 에 있다(집계·export 포맷터는 core 가 소비하므로).
|
|
5
|
+
* mui 는 같은 것을 re-export 한다: `createRegistry` · `Registry` · `RegistryPatch` · `applyPatch` · `isRegistry` · `toRegistry`.
|
|
6
|
+
*/
|
|
7
|
+
export { createRegistry, applyPatch, isRegistry, toRegistry, type Registry, type RegistryPatch, } from "../../core/src/index";
|
|
8
|
+
export interface MenuItemDef<Ctx> {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string | ((ctx: Ctx) => string);
|
|
11
|
+
icon?: ReactNode | ((ctx: Ctx) => ReactNode);
|
|
12
|
+
/** 노출 조건 — 거짓이면 항목이 빠진다 */
|
|
13
|
+
when?: (ctx: Ctx) => boolean;
|
|
14
|
+
run: (ctx: Ctx) => void;
|
|
15
|
+
disabled?: (ctx: Ctx) => boolean;
|
|
16
|
+
/** 같은 group 끼리 묶이고 group 사이에 구분선 */
|
|
17
|
+
group?: string;
|
|
18
|
+
/** 숫자 작을수록 앞. 미지정이면 레지스트리 위치(바로 앞 `order` 항목 뒤)를 지킨다 */
|
|
19
|
+
order?: number;
|
|
20
|
+
/** 이 항목 앞에 구분선 */
|
|
21
|
+
divider?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* 하나의 정의가 여러 항목으로 펼쳐질 때 (예: "N단계까지 펼치기" 를 단계마다).
|
|
24
|
+
* 있으면 이 정의 자리에 돌려준 항목들이 들어가고 자신의 `run` 은 쓰이지 않는다.
|
|
25
|
+
*/
|
|
26
|
+
items?: (ctx: Ctx) => Array<MenuItemDef<Ctx>>;
|
|
27
|
+
}
|
|
28
|
+
export type MenuPatch<Ctx> = RegistryPatch<MenuItemDef<Ctx>>;
|
|
29
|
+
export type { CellRendererDef, CellContext, CellRendererId } from "./content/cellRenderers";
|
|
30
|
+
export type { AggregationDef, ExportFormatterDef } from "../../core/src/index";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 확장점 공통 레지스트리 (설계 문서 4절) — 구현은 core 에 있다(집계·export 포맷터는 core 가 소비하므로).
|
|
3
|
+
* mui 는 같은 것을 re-export 한다: `createRegistry` · `Registry` · `RegistryPatch` · `applyPatch` · `isRegistry` · `toRegistry`.
|
|
4
|
+
*/
|
|
5
|
+
export { createRegistry, applyPatch, isRegistry, toRegistry, } from "../../core/src/index";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type SxProps, type Theme } from "@mui/material";
|
|
3
|
+
import type { RowData } from "@tanstack/react-table";
|
|
4
|
+
import type { TableSearch } from "../../../core/src/index";
|
|
5
|
+
export interface GlobalFilterFieldProps<T extends RowData> {
|
|
6
|
+
/**
|
|
7
|
+
* `useTableSearch` 결과. `draft.globalFilter` 를 보고 `setGlobalFilter` / `apply` 를 부른다.
|
|
8
|
+
* `pendingApply` 가 있으면(코어 debounce 대기 중) 입력 끝에 스피너를 그린다.
|
|
9
|
+
*/
|
|
10
|
+
search: Pick<TableSearch<T>, "draft" | "setGlobalFilter" | "apply"> & Partial<Pick<TableSearch<T>, "pendingApply">>;
|
|
11
|
+
/** Enter 로 즉시 검색. 끄면 blur 시 draft 만 갱신되고 검색 버튼이 적용한다 */
|
|
12
|
+
applyOnEnter?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* 타이핑만으로 조회한다 — 마지막 입력 뒤 `debounceMs` 가 지나면 `setGlobalFilter` + `apply`.
|
|
15
|
+
* Enter 는 대기를 건너뛰고 즉시 적용한다. 대기 중에는 입력 끝에 작은 스피너가 돈다.
|
|
16
|
+
*/
|
|
17
|
+
applyOnChange?: boolean;
|
|
18
|
+
/** `applyOnChange` 의 지연(ms). 기본 300 */
|
|
19
|
+
debounceMs?: number;
|
|
20
|
+
/**
|
|
21
|
+
* 입력 오른쪽 슬롯 — ERP 의 AI 검색 버튼(`AiQueryLauncher`) 이 여기 꽂힌다.
|
|
22
|
+
* 전역검색어와 무관한 별도 진입점이므로 값 상태에 손대지 않는다.
|
|
23
|
+
*/
|
|
24
|
+
actions?: ReactNode;
|
|
25
|
+
label?: string;
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
fullWidth?: boolean;
|
|
28
|
+
sx?: SxProps<Theme>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 전역검색 입력 — 기존 `GlobalFilterField.tsx` 대응.
|
|
32
|
+
* 로컬 상태로 타이핑하고 blur/Enter 에만 draft 에 반영한다(글자마다 검색 상태를 흔들지 않는다).
|
|
33
|
+
* `applyOnChange` 면 여기서 debounce 해 `setGlobalFilter` + `apply` 를 한 번에 낸다 —
|
|
34
|
+
* 코어 옵션(`globalFilterApplyOnChange`) 없이도 필드 하나로 동작한다.
|
|
35
|
+
*/
|
|
36
|
+
export declare function GlobalFilterField<T extends RowData>({ search, applyOnEnter, applyOnChange, debounceMs, actions, label, placeholder, fullWidth, sx, }: GlobalFilterFieldProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { CircularProgress, InputAdornment, TextField } from "@mui/material";
|
|
4
|
+
import SearchIcon from "@mui/icons-material/Search";
|
|
5
|
+
/**
|
|
6
|
+
* 전역검색 입력 — 기존 `GlobalFilterField.tsx` 대응.
|
|
7
|
+
* 로컬 상태로 타이핑하고 blur/Enter 에만 draft 에 반영한다(글자마다 검색 상태를 흔들지 않는다).
|
|
8
|
+
* `applyOnChange` 면 여기서 debounce 해 `setGlobalFilter` + `apply` 를 한 번에 낸다 —
|
|
9
|
+
* 코어 옵션(`globalFilterApplyOnChange`) 없이도 필드 하나로 동작한다.
|
|
10
|
+
*/
|
|
11
|
+
export function GlobalFilterField({ search, applyOnEnter = true, applyOnChange = false, debounceMs = 300, actions, label = "전역검색", placeholder = "검색어를 입력하세요", fullWidth = false, sx, }) {
|
|
12
|
+
const draftValue = search.draft.globalFilter ?? "";
|
|
13
|
+
const [local, setLocal] = useState(draftValue);
|
|
14
|
+
const inputRef = useRef(null);
|
|
15
|
+
// 필드 자체 debounce — 코어의 pendingApply 와 합쳐 스피너를 그린다
|
|
16
|
+
const timerRef = useRef(null);
|
|
17
|
+
const [localPending, setLocalPending] = useState(false);
|
|
18
|
+
const searchRef = useRef(search);
|
|
19
|
+
searchRef.current = search;
|
|
20
|
+
const cancelTimer = () => {
|
|
21
|
+
if (timerRef.current !== null) {
|
|
22
|
+
clearTimeout(timerRef.current);
|
|
23
|
+
timerRef.current = null;
|
|
24
|
+
}
|
|
25
|
+
setLocalPending(false);
|
|
26
|
+
};
|
|
27
|
+
useEffect(() => () => { if (timerRef.current !== null)
|
|
28
|
+
clearTimeout(timerRef.current); }, []);
|
|
29
|
+
// 초기화 버튼 등으로 밖에서 바뀌면 따라간다
|
|
30
|
+
useEffect(() => { setLocal(draftValue); }, [draftValue]);
|
|
31
|
+
const applyValue = (value) => {
|
|
32
|
+
// setGlobalFilter 가 draftRef 를 즉시 갱신하므로 같은 핸들러에서 apply 해도 최신 값을 본다
|
|
33
|
+
searchRef.current.setGlobalFilter(value);
|
|
34
|
+
searchRef.current.apply();
|
|
35
|
+
};
|
|
36
|
+
const onChange = (value) => {
|
|
37
|
+
setLocal(value);
|
|
38
|
+
if (!applyOnChange)
|
|
39
|
+
return;
|
|
40
|
+
if (timerRef.current !== null)
|
|
41
|
+
clearTimeout(timerRef.current);
|
|
42
|
+
if (!(debounceMs > 0)) {
|
|
43
|
+
applyValue(value);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
setLocalPending(true);
|
|
47
|
+
timerRef.current = setTimeout(() => {
|
|
48
|
+
timerRef.current = null;
|
|
49
|
+
setLocalPending(false);
|
|
50
|
+
applyValue(value);
|
|
51
|
+
}, debounceMs);
|
|
52
|
+
};
|
|
53
|
+
const flush = () => {
|
|
54
|
+
// 대기 중이면 타이머가 처리한다 — blur 로 draft 만 바꾸면 조회 없이 isDirty 만 오른다
|
|
55
|
+
if (localPending)
|
|
56
|
+
return;
|
|
57
|
+
if (local !== draftValue)
|
|
58
|
+
search.setGlobalFilter(local);
|
|
59
|
+
};
|
|
60
|
+
const onKeyDown = (event) => {
|
|
61
|
+
event.stopPropagation();
|
|
62
|
+
if (event.key !== "Enter")
|
|
63
|
+
return;
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
cancelTimer();
|
|
66
|
+
search.setGlobalFilter(local);
|
|
67
|
+
if (applyOnEnter || applyOnChange)
|
|
68
|
+
search.apply();
|
|
69
|
+
inputRef.current?.focus();
|
|
70
|
+
};
|
|
71
|
+
const pending = localPending || search.pendingApply === true;
|
|
72
|
+
return (_jsx(TextField, { inputRef: inputRef, size: "small", label: label, placeholder: placeholder, value: local, fullWidth: fullWidth, onChange: (event) => onChange(event.target.value), onBlur: flush, onKeyDown: onKeyDown, slotProps: {
|
|
73
|
+
input: {
|
|
74
|
+
startAdornment: (_jsx(InputAdornment, { position: "start", children: _jsx(SearchIcon, { fontSize: "small", sx: { color: "text.disabled" } }) })),
|
|
75
|
+
endAdornment: pending || actions ? (_jsxs(InputAdornment, { position: "end", children: [pending && (_jsx(CircularProgress, { size: 14, thickness: 5, "aria-label": "\uC870\uD68C \uB300\uAE30 \uC911", sx: { mr: actions ? 0.5 : 0 } })), actions] })) : undefined,
|
|
76
|
+
},
|
|
77
|
+
}, sx: { minWidth: fullWidth ? "100%" : 220, ...sx } }));
|
|
78
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { RowData } from "@tanstack/react-table";
|
|
2
|
+
import { type TableContentProps } from "../content/TableContent";
|
|
3
|
+
export interface VirtualizeOptions {
|
|
4
|
+
/** 행 높이 추정치(px). 실제 높이는 렌더 후 측정해 보정한다. 기본 small 밀도 기준 37 */
|
|
5
|
+
estimateRowHeight?: number;
|
|
6
|
+
/** 화면 밖에 미리 그릴 행 수. 기본 8 */
|
|
7
|
+
overscan?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface VirtualizeColumnsOptions {
|
|
10
|
+
/** 화면 밖에 미리 그릴 열 수. 기본 3 */
|
|
11
|
+
overscan?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface VirtualTableContentProps<T extends RowData> extends TableContentProps<T> {
|
|
14
|
+
/** `true` 또는 옵션. false 면 일반 `TableContent` 와 같다 */
|
|
15
|
+
virtualize?: boolean | VirtualizeOptions;
|
|
16
|
+
/**
|
|
17
|
+
* 열 가상화 — 고정되지 않은 가운데 컬럼 중 **가로 스크롤 범위에 보이는 것만** 그린다.
|
|
18
|
+
* 고정(left/right) 컬럼은 항상 그린다. 폭은 `column.getSize()` 를 그대로 쓰므로(측정하지 않음)
|
|
19
|
+
* 컬럼에 `size` 를 지정해 두는 편이 정확하다. 컬럼 그룹 헤더(2단 이상)에서는 자동으로 꺼진다.
|
|
20
|
+
*/
|
|
21
|
+
virtualizeColumns?: boolean | VirtualizeColumnsOptions;
|
|
22
|
+
/**
|
|
23
|
+
* 가상화는 스크롤 컨테이너가 있어야 성립한다 — `maxHeight` 가 없으면 컨테이너가 내용만큼
|
|
24
|
+
* 늘어나 모든 행이 "화면 안" 이 되고, 결국 전부 그린다. 반드시 지정한다.
|
|
25
|
+
*/
|
|
26
|
+
maxHeight: number | string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 행(·열) 가상화 테이블 — `@tanstack/react-virtual` 로 보이는 행만 그린다.
|
|
30
|
+
*
|
|
31
|
+
* 별도 진입점 `kokotable/mui/virtual` 에서만 export 한다. 메인 진입점에 두면 가상화를
|
|
32
|
+
* 쓰지 않는 소비자 번들에도 virtual-core 가 들어가고, `@tanstack/react-virtual` 을 설치하지
|
|
33
|
+
* 않은 소비자는 번들러 해석 단계에서 실패한다(optional peer).
|
|
34
|
+
*
|
|
35
|
+
* 그룹핑(`groupItems`) 도 평탄화 항목 단위로 가상화된다. detail panel 이 펼쳐진 행은
|
|
36
|
+
* 본문 행만 측정되므로 스크롤 위치 추정이 조금 어긋날 수 있다(그려지는 데는 문제 없다).
|
|
37
|
+
*
|
|
38
|
+
* 열 가상화(`virtualizeColumns`)는 같은 스크롤 컨테이너의 `scrollLeft` 를 읽는 가로 virtualizer 다.
|
|
39
|
+
* 그려지지 않은 폭은 좌·우 패딩 셀이 대신 차지해 스크롤 폭과 고정 컬럼 오프셋이 유지된다.
|
|
40
|
+
*/
|
|
41
|
+
export declare function VirtualTableContent<T extends RowData>({ virtualize, virtualizeColumns, ...props }: VirtualTableContentProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
4
|
+
import { TableContentBase } from "../content/TableContent";
|
|
5
|
+
/**
|
|
6
|
+
* 행(·열) 가상화 테이블 — `@tanstack/react-virtual` 로 보이는 행만 그린다.
|
|
7
|
+
*
|
|
8
|
+
* 별도 진입점 `kokotable/mui/virtual` 에서만 export 한다. 메인 진입점에 두면 가상화를
|
|
9
|
+
* 쓰지 않는 소비자 번들에도 virtual-core 가 들어가고, `@tanstack/react-virtual` 을 설치하지
|
|
10
|
+
* 않은 소비자는 번들러 해석 단계에서 실패한다(optional peer).
|
|
11
|
+
*
|
|
12
|
+
* 그룹핑(`groupItems`) 도 평탄화 항목 단위로 가상화된다. detail panel 이 펼쳐진 행은
|
|
13
|
+
* 본문 행만 측정되므로 스크롤 위치 추정이 조금 어긋날 수 있다(그려지는 데는 문제 없다).
|
|
14
|
+
*
|
|
15
|
+
* 열 가상화(`virtualizeColumns`)는 같은 스크롤 컨테이너의 `scrollLeft` 를 읽는 가로 virtualizer 다.
|
|
16
|
+
* 그려지지 않은 폭은 좌·우 패딩 셀이 대신 차지해 스크롤 폭과 고정 컬럼 오프셋이 유지된다.
|
|
17
|
+
*/
|
|
18
|
+
export function VirtualTableContent({ virtualize = true, virtualizeColumns = false, ...props }) {
|
|
19
|
+
const options = typeof virtualize === "object" ? virtualize : {};
|
|
20
|
+
const enabled = virtualize !== false;
|
|
21
|
+
const columnOptions = typeof virtualizeColumns === "object" ? virtualizeColumns : {};
|
|
22
|
+
const columnsEnabled = virtualizeColumns !== false;
|
|
23
|
+
const containerRef = useRef(null);
|
|
24
|
+
const grouped = !!props.groupItems && props.groupItems.length > 0;
|
|
25
|
+
const count = grouped ? props.groupItems.length : props.table.getRowModel().rows.length;
|
|
26
|
+
const virtualizer = useVirtualizer({
|
|
27
|
+
count: enabled ? count : 0,
|
|
28
|
+
getScrollElement: () => containerRef.current,
|
|
29
|
+
estimateSize: () => options.estimateRowHeight ?? 37,
|
|
30
|
+
overscan: options.overscan ?? 8,
|
|
31
|
+
// data-index 를 읽어 어느 행을 측정했는지 안다 (TableContentBase 가 행마다 붙인다)
|
|
32
|
+
indexAttribute: "data-index",
|
|
33
|
+
});
|
|
34
|
+
// ── 열 가상화 ──
|
|
35
|
+
const centerColumns = props.table.getCenterVisibleLeafColumns();
|
|
36
|
+
const columnSizing = props.table.getState().columnSizing;
|
|
37
|
+
// 폭은 측정하지 않고 컬럼 정의(리사이즈 상태 포함)를 그대로 믿는다
|
|
38
|
+
const sizeKey = useMemo(() => centerColumns.map((column) => `${column.id}:${column.getSize()}`).join("|"), [centerColumns, columnSizing]);
|
|
39
|
+
const columnVirtualizer = useVirtualizer({
|
|
40
|
+
horizontal: true,
|
|
41
|
+
count: columnsEnabled ? centerColumns.length : 0,
|
|
42
|
+
getScrollElement: () => containerRef.current,
|
|
43
|
+
estimateSize: (index) => centerColumns[index]?.getSize() ?? 150,
|
|
44
|
+
overscan: columnOptions.overscan ?? 3,
|
|
45
|
+
});
|
|
46
|
+
// 리사이즈·표시 토글로 폭 배열이 바뀌면 캐시된 측정치를 버린다
|
|
47
|
+
useEffect(() => { columnVirtualizer.measure(); }, [sizeKey, columnVirtualizer]);
|
|
48
|
+
if (!enabled && !columnsEnabled)
|
|
49
|
+
return _jsx(TableContentBase, { ...props });
|
|
50
|
+
return (_jsx(TableContentBase, { ...props, containerRef: containerRef, virtual: enabled ? {
|
|
51
|
+
items: virtualizer.getVirtualItems(),
|
|
52
|
+
totalSize: virtualizer.getTotalSize(),
|
|
53
|
+
measureElement: virtualizer.measureElement,
|
|
54
|
+
} : undefined, virtualColumns: columnsEnabled ? {
|
|
55
|
+
items: columnVirtualizer.getVirtualItems(),
|
|
56
|
+
totalSize: columnVirtualizer.getTotalSize(),
|
|
57
|
+
} : undefined }));
|
|
58
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RowData } from "@tanstack/react-table";
|
|
2
|
+
import { type TableScreenProps } from "../TableScreen";
|
|
3
|
+
export type VirtualTableScreenProps<T extends RowData> = TableScreenProps<T>;
|
|
4
|
+
/**
|
|
5
|
+
* 가상화 화면 — `TableScreen` 과 같은 props 에 본문만 `VirtualTableContent` 로 바꾼 것.
|
|
6
|
+
*
|
|
7
|
+
* `definition.features.virtualize` 를 읽는다: `true` 면 행 가상화, 객체면 `rows`/`columns`/
|
|
8
|
+
* `estimateRowHeight`/`maxHeight`. 스크롤 컨테이너 높이는 `contentProps.maxHeight` →
|
|
9
|
+
* `features.virtualize.maxHeight` → 520 순이다 (없으면 가상화가 성립하지 않는다).
|
|
10
|
+
*
|
|
11
|
+
* 별도 진입점 `kokotable/mui/virtual` 에서만 export 한다 — `@tanstack/react-virtual` 은 optional peer 다.
|
|
12
|
+
*/
|
|
13
|
+
export declare function VirtualTableScreen<T extends RowData>(props: VirtualTableScreenProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { TableScreenBase } from "../TableScreen";
|
|
3
|
+
import { VirtualTableContent } from "./VirtualTableContent";
|
|
4
|
+
const DEFAULT_MAX_HEIGHT = 520;
|
|
5
|
+
/**
|
|
6
|
+
* 가상화 화면 — `TableScreen` 과 같은 props 에 본문만 `VirtualTableContent` 로 바꾼 것.
|
|
7
|
+
*
|
|
8
|
+
* `definition.features.virtualize` 를 읽는다: `true` 면 행 가상화, 객체면 `rows`/`columns`/
|
|
9
|
+
* `estimateRowHeight`/`maxHeight`. 스크롤 컨테이너 높이는 `contentProps.maxHeight` →
|
|
10
|
+
* `features.virtualize.maxHeight` → 520 순이다 (없으면 가상화가 성립하지 않는다).
|
|
11
|
+
*
|
|
12
|
+
* 별도 진입점 `kokotable/mui/virtual` 에서만 export 한다 — `@tanstack/react-virtual` 은 optional peer 다.
|
|
13
|
+
*/
|
|
14
|
+
export function VirtualTableScreen(props) {
|
|
15
|
+
const feature = props.bundle.definition.features?.virtualize;
|
|
16
|
+
const options = typeof feature === "object" ? feature : {};
|
|
17
|
+
const rows = feature === false ? false : (options.rows ?? true);
|
|
18
|
+
const virtualize = rows === true
|
|
19
|
+
? (options.estimateRowHeight !== undefined ? { estimateRowHeight: options.estimateRowHeight } : true)
|
|
20
|
+
: rows;
|
|
21
|
+
const virtualizeColumns = options.columns ?? false;
|
|
22
|
+
return (_jsx(TableScreenBase, { ...props, renderContent: (contentProps) => (_jsx(VirtualTableContent, { ...contentProps, virtualize: virtualize, virtualizeColumns: virtualizeColumns, maxHeight: contentProps.maxHeight ?? options.maxHeight ?? DEFAULT_MAX_HEIGHT })) }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { VirtualTableContent } from "./virtual/VirtualTableContent";
|
|
2
|
+
export type { VirtualTableContentProps, VirtualizeOptions, VirtualizeColumnsOptions } from "./virtual/VirtualTableContent";
|
|
3
|
+
export { VirtualTableScreen } from "./virtual/VirtualTableScreen";
|
|
4
|
+
export type { VirtualTableScreenProps } from "./virtual/VirtualTableScreen";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FilterOperator } from "../../../core/src/index";
|
|
2
|
+
import { type VirtualColumnField } from "./expression";
|
|
3
|
+
import type { VirtualColumn } from "./useVirtualColumns";
|
|
4
|
+
/** 숫자 가상 열 필터 연산자 (기존 ERP 카드와 같은 목록) */
|
|
5
|
+
export declare const VIRTUAL_NUMBER_OPERATORS: ReadonlyArray<{
|
|
6
|
+
value: FilterOperator;
|
|
7
|
+
label: string;
|
|
8
|
+
}>;
|
|
9
|
+
export interface VirtualColumnCardProps {
|
|
10
|
+
column: VirtualColumn;
|
|
11
|
+
onEdit: () => void;
|
|
12
|
+
onRemove: () => void;
|
|
13
|
+
/** 수식 표시에 쓸 필드 (키 → 헤더 치환) */
|
|
14
|
+
fields?: ReadonlyArray<VirtualColumnField>;
|
|
15
|
+
/**
|
|
16
|
+
* 필터 입력. 주면 카드 아래에 (숫자면 연산자 + 값, 아니면 텍스트) 입력이 붙는다.
|
|
17
|
+
* `search.setFilter(slotKey, value)` / `search.setOperator(slotKey, op)` 로 잇는다.
|
|
18
|
+
*/
|
|
19
|
+
filter?: {
|
|
20
|
+
value: unknown;
|
|
21
|
+
operator: FilterOperator;
|
|
22
|
+
onChange: (value: unknown, operator: FilterOperator) => void;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** 가상 열 카드 — 이름 · 수식 요약 · 타입 · 편집/삭제 (+ 선택적 필터) */
|
|
26
|
+
export declare function VirtualColumnCard({ column, onEdit, onRemove, fields, filter }: VirtualColumnCardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Chip, IconButton, MenuItem, Select, TextField, Tooltip, Typography } from "@mui/material";
|
|
3
|
+
import EditIcon from "@mui/icons-material/Edit";
|
|
4
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
5
|
+
import { describeExpression } from "./expression";
|
|
6
|
+
/** 숫자 가상 열 필터 연산자 (기존 ERP 카드와 같은 목록) */
|
|
7
|
+
export const VIRTUAL_NUMBER_OPERATORS = [
|
|
8
|
+
{ value: "greaterThanOrEqualTo", label: ">=" },
|
|
9
|
+
{ value: "lessThanOrEqualTo", label: "<=" },
|
|
10
|
+
{ value: "equals", label: "=" },
|
|
11
|
+
{ value: "greaterThan", label: ">" },
|
|
12
|
+
{ value: "lessThan", label: "<" },
|
|
13
|
+
];
|
|
14
|
+
const TYPE_LABELS = { number: "숫자", string: "문자열", boolean: "참/거짓" };
|
|
15
|
+
/** 가상 열 카드 — 이름 · 수식 요약 · 타입 · 편집/삭제 (+ 선택적 필터) */
|
|
16
|
+
export function VirtualColumnCard({ column, onEdit, onRemove, fields = [], filter }) {
|
|
17
|
+
const isNumber = column.type === "number";
|
|
18
|
+
const display = fields.length > 0 ? describeExpression(column.expr, fields) : column.expr;
|
|
19
|
+
return (_jsxs(Box, { "data-testid": `virtual-column-card-${column.slotKey}`, sx: {
|
|
20
|
+
display: "flex", flexDirection: "column",
|
|
21
|
+
border: "1px solid", borderColor: "divider", borderRadius: 1,
|
|
22
|
+
borderLeft: "4px solid", borderLeftColor: "info.main",
|
|
23
|
+
p: 1, minWidth: 0,
|
|
24
|
+
}, children: [_jsxs(Box, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", mb: 0.5, gap: 0.5 }, children: [_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 0.75, minWidth: 0 }, children: [_jsx(Typography, { variant: "body2", sx: { fontWeight: 600, color: "info.dark", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: column.name }), _jsx(Chip, { label: TYPE_LABELS[column.type], size: "small", variant: "outlined", sx: { height: 18, fontSize: 11 } })] }), _jsxs(Box, { sx: { display: "flex", flexShrink: 0 }, children: [_jsx(Tooltip, { title: "\uD3B8\uC9D1", children: _jsx(IconButton, { size: "small", "aria-label": `${column.name} 편집`, onClick: onEdit, sx: { p: 0.25 }, children: _jsx(EditIcon, { sx: { fontSize: 16 } }) }) }), _jsx(Tooltip, { title: "\uC0AD\uC81C", children: _jsx(IconButton, { size: "small", "aria-label": `${column.name} 삭제`, onClick: onRemove, sx: { p: 0.25 }, children: _jsx(CloseIcon, { sx: { fontSize: 16 } }) }) })] })] }), _jsx(Tooltip, { title: `${column.slotKey}: ${column.expr}`, placement: "top", children: _jsxs(Typography, { variant: "caption", sx: { color: "text.secondary", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: "monospace" }, children: ["= ", display] }) }), filter && (_jsx(Box, { sx: { display: "flex", gap: 0.5, alignItems: "center", mt: 1 }, children: isNumber ? (_jsxs(_Fragment, { children: [_jsx(Select, { size: "small", value: filter.operator, onChange: (event) => filter.onChange(filter.value, event.target.value), inputProps: { "aria-label": `${column.name} 연산자` }, sx: { minWidth: 64, fontSize: "0.8rem" }, children: VIRTUAL_NUMBER_OPERATORS.map((op) => (_jsx(MenuItem, { value: op.value, sx: { fontSize: "0.8rem" }, children: op.label }, op.value))) }), _jsx(TextField, { size: "small", type: "number", placeholder: "\uAC12", value: filter.value ?? "", inputProps: { "aria-label": `${column.name} 필터` }, onChange: (event) => {
|
|
25
|
+
const raw = event.target.value;
|
|
26
|
+
const parsed = Number.parseFloat(raw);
|
|
27
|
+
filter.onChange(raw === "" || Number.isNaN(parsed) ? undefined : parsed, filter.operator);
|
|
28
|
+
}, sx: { flex: 1, "& input": { fontSize: "0.8rem" } } })] })) : (_jsx(TextField, { size: "small", placeholder: "\uD544\uD130 \uAC12 \uC785\uB825", fullWidth: true, value: filter.value ?? "", inputProps: { "aria-label": `${column.name} 필터` }, onChange: (event) => filter.onChange(event.target.value || undefined, filter.operator), sx: { "& input": { fontSize: "0.8rem" } } })) }))] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ExpressionValidation, type VirtualColumnField } from "./expression";
|
|
2
|
+
import type { VirtualColumn, VirtualColumnInput } from "./useVirtualColumns";
|
|
3
|
+
export interface VirtualColumnDialogProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
/**
|
|
7
|
+
* 저장. 실패(`ok: false`)를 돌려주면 다이얼로그가 닫히지 않고 메시지를 보여 준다 —
|
|
8
|
+
* `useVirtualColumns().add/update` 를 그대로 넘기면 된다.
|
|
9
|
+
*/
|
|
10
|
+
onSubmit: (column: VirtualColumnInput) => ExpressionValidation | {
|
|
11
|
+
ok: true;
|
|
12
|
+
slotKey: string;
|
|
13
|
+
} | void;
|
|
14
|
+
/** 편집 모드일 때 기존 값 */
|
|
15
|
+
initialValues?: VirtualColumn;
|
|
16
|
+
/** 수식에 쓸 수 있는 필드 (`virtualColumnFields(columns)`) */
|
|
17
|
+
fields: ReadonlyArray<VirtualColumnField>;
|
|
18
|
+
/** 저장 전 검증. 없으면 `validateExpression` 만 건다 */
|
|
19
|
+
validate?: (column: VirtualColumnInput) => ExpressionValidation;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 가상 열 추가·편집 다이얼로그 — 이름 · 칩 기반 수식 빌더 · 연산자 · 필드 · 타입.
|
|
23
|
+
*
|
|
24
|
+
* 기존 ERP `CustomColumnDialog` 의 UX 를 그대로 옮겼다(칩 삽입, Backspace 로 마지막 세그먼트
|
|
25
|
+
* 제거, Enter 저장, 한글 헤더 미리보기). 검증 실패는 인라인 Alert 로 보여 주고 닫지 않는다.
|
|
26
|
+
*/
|
|
27
|
+
export declare function VirtualColumnDialog({ open, onClose, onSubmit, initialValues, fields, validate, }: VirtualColumnDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { Alert, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, FormLabel, IconButton, Radio, RadioGroup, TextField, Tooltip, Typography, } from "@mui/material";
|
|
4
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
5
|
+
import { TEMPLATE_FIELD_PATTERN, validateExpression, } from "./expression";
|
|
6
|
+
const OPERATORS = [
|
|
7
|
+
{ label: "+", value: " + ", tooltip: "더하기" },
|
|
8
|
+
{ label: "-", value: " - ", tooltip: "빼기" },
|
|
9
|
+
{ label: "*", value: " * ", tooltip: "곱하기" },
|
|
10
|
+
{ label: "/", value: " / ", tooltip: "나누기" },
|
|
11
|
+
{ label: "(", value: "(", tooltip: "여는 괄호" },
|
|
12
|
+
{ label: ")", value: ")", tooltip: "닫는 괄호" },
|
|
13
|
+
];
|
|
14
|
+
const TYPE_OPTIONS = [
|
|
15
|
+
{ value: "number", label: "숫자" },
|
|
16
|
+
{ value: "string", label: "문자열" },
|
|
17
|
+
{ value: "boolean", label: "참/거짓" },
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* 수식 문자열 → 세그먼트. `{key}` 는 필드 칩, 필드 목록에 있는 맨 식별자도 칩으로 올린다.
|
|
21
|
+
* 나머지는 텍스트다.
|
|
22
|
+
*/
|
|
23
|
+
function parseSegments(expr, fields) {
|
|
24
|
+
if (!expr)
|
|
25
|
+
return [];
|
|
26
|
+
const byKey = new Map(fields.map((f) => [f.key, f.header]));
|
|
27
|
+
const segments = [];
|
|
28
|
+
const pushText = (value) => {
|
|
29
|
+
if (!value)
|
|
30
|
+
return;
|
|
31
|
+
const last = segments[segments.length - 1];
|
|
32
|
+
if (last && last.type === "text")
|
|
33
|
+
last.value += value;
|
|
34
|
+
else
|
|
35
|
+
segments.push({ type: "text", value });
|
|
36
|
+
};
|
|
37
|
+
const bare = [...byKey.keys()].sort((a, b) => b.length - a.length)
|
|
38
|
+
.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
39
|
+
const pattern = new RegExp(`${TEMPLATE_FIELD_PATTERN.source}${bare.length > 0 ? `|(?<![\\w.])(${bare.join("|")})(?![\\w.])` : ""}`, "g");
|
|
40
|
+
let cursor = 0;
|
|
41
|
+
for (const match of expr.matchAll(pattern)) {
|
|
42
|
+
pushText(expr.slice(cursor, match.index));
|
|
43
|
+
const key = match[1] ?? match[2];
|
|
44
|
+
segments.push({ type: "field", key, header: byKey.get(key) ?? key });
|
|
45
|
+
cursor = match.index + match[0].length;
|
|
46
|
+
}
|
|
47
|
+
pushText(expr.slice(cursor));
|
|
48
|
+
return segments;
|
|
49
|
+
}
|
|
50
|
+
/** 세그먼트 → 수식. 필드는 항상 `{key}` 로 통일한다 (서버가 정규화한다) */
|
|
51
|
+
function segmentsToExpr(segments, pending) {
|
|
52
|
+
const all = pending ? [...segments, { type: "text", value: pending }] : segments;
|
|
53
|
+
return all.map((s) => (s.type === "field" ? `{${s.key}}` : s.value)).join("");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 가상 열 추가·편집 다이얼로그 — 이름 · 칩 기반 수식 빌더 · 연산자 · 필드 · 타입.
|
|
57
|
+
*
|
|
58
|
+
* 기존 ERP `CustomColumnDialog` 의 UX 를 그대로 옮겼다(칩 삽입, Backspace 로 마지막 세그먼트
|
|
59
|
+
* 제거, Enter 저장, 한글 헤더 미리보기). 검증 실패는 인라인 Alert 로 보여 주고 닫지 않는다.
|
|
60
|
+
*/
|
|
61
|
+
export function VirtualColumnDialog({ open, onClose, onSubmit, initialValues, fields, validate, }) {
|
|
62
|
+
const [name, setName] = useState("");
|
|
63
|
+
const [segments, setSegments] = useState([]);
|
|
64
|
+
const [textInput, setTextInput] = useState("");
|
|
65
|
+
const [type, setType] = useState("number");
|
|
66
|
+
const [error, setError] = useState(null);
|
|
67
|
+
const inputRef = useRef(null);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!open)
|
|
70
|
+
return;
|
|
71
|
+
setError(null);
|
|
72
|
+
setTextInput("");
|
|
73
|
+
if (initialValues) {
|
|
74
|
+
setName(initialValues.name);
|
|
75
|
+
setType(initialValues.type);
|
|
76
|
+
setSegments(parseSegments(initialValues.expr, fields));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
setName("");
|
|
80
|
+
setType("number");
|
|
81
|
+
setSegments([]);
|
|
82
|
+
}
|
|
83
|
+
}, [open, initialValues, fields]);
|
|
84
|
+
const commitText = useCallback(() => {
|
|
85
|
+
if (!textInput)
|
|
86
|
+
return;
|
|
87
|
+
setSegments((prev) => [...prev, { type: "text", value: textInput }]);
|
|
88
|
+
setTextInput("");
|
|
89
|
+
}, [textInput]);
|
|
90
|
+
const addField = (field) => {
|
|
91
|
+
commitText();
|
|
92
|
+
setSegments((prev) => [...prev, { type: "field", key: field.key, header: field.header }]);
|
|
93
|
+
setError(null);
|
|
94
|
+
inputRef.current?.focus();
|
|
95
|
+
};
|
|
96
|
+
const addOperator = (value) => {
|
|
97
|
+
commitText();
|
|
98
|
+
setSegments((prev) => [...prev, { type: "text", value }]);
|
|
99
|
+
setError(null);
|
|
100
|
+
inputRef.current?.focus();
|
|
101
|
+
};
|
|
102
|
+
const removeSegment = (index) => {
|
|
103
|
+
setSegments((prev) => prev.filter((_, i) => i !== index));
|
|
104
|
+
inputRef.current?.focus();
|
|
105
|
+
};
|
|
106
|
+
const expr = segmentsToExpr(segments, textInput).trim();
|
|
107
|
+
const hasExpr = expr.length > 0;
|
|
108
|
+
const canSubmit = name.trim().length > 0 && hasExpr;
|
|
109
|
+
const preview = useMemo(() => {
|
|
110
|
+
const all = textInput ? [...segments, { type: "text", value: textInput }] : segments;
|
|
111
|
+
return all.map((s) => (s.type === "field" ? s.header : s.value)).join("");
|
|
112
|
+
}, [segments, textInput]);
|
|
113
|
+
const handleSubmit = () => {
|
|
114
|
+
if (!canSubmit)
|
|
115
|
+
return;
|
|
116
|
+
const column = { name: name.trim(), expr, type };
|
|
117
|
+
const check = validate ? validate(column) : validateExpression(expr, { type, fields });
|
|
118
|
+
if (!check.ok) {
|
|
119
|
+
setError(check.message);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const result = onSubmit(column);
|
|
123
|
+
if (result && result.ok === false) {
|
|
124
|
+
setError(result.message);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
onClose();
|
|
128
|
+
};
|
|
129
|
+
const handleKeyDown = (event) => {
|
|
130
|
+
if (event.key === "Backspace" && textInput === "" && segments.length > 0) {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
const last = segments[segments.length - 1];
|
|
133
|
+
setSegments((prev) => prev.slice(0, -1));
|
|
134
|
+
if (last.type === "text")
|
|
135
|
+
setTextInput(last.value);
|
|
136
|
+
}
|
|
137
|
+
if (event.key === "Enter") {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
handleSubmit();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const isEdit = !!initialValues;
|
|
143
|
+
return (_jsxs(Dialog, { open: open, onClose: onClose, maxWidth: "sm", fullWidth: true, "aria-labelledby": "virtual-column-dialog-title", children: [_jsxs(DialogTitle, { id: "virtual-column-dialog-title", sx: { display: "flex", alignItems: "center", justifyContent: "space-between", pb: 1 }, children: [_jsx(Typography, { variant: "h6", component: "span", children: isEdit ? "가상 열 편집" : "가상 열 추가" }), _jsx(IconButton, { size: "small", onClick: onClose, "aria-label": "\uB2EB\uAE30", children: _jsx(CloseIcon, {}) })] }), _jsx(DialogContent, { dividers: true, children: _jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 2.5 }, children: [_jsx(TextField, { label: "\uC774\uB984", placeholder: "\uC608: \uD560\uC778\uC561", value: name, onChange: (event) => { setName(event.target.value); setError(null); }, size: "small", fullWidth: true, autoFocus: true, inputProps: { "aria-label": "가상 열 이름" } }), _jsxs(Box, { children: [_jsx(Typography, { variant: "body2", sx: { fontWeight: 500, color: "text.secondary", mb: 0.5 }, children: "\uC218\uC2DD" }), _jsxs(Box, { onClick: () => inputRef.current?.focus(), sx: {
|
|
144
|
+
display: "flex", flexWrap: "wrap", alignItems: "center", gap: 0.5,
|
|
145
|
+
border: "1px solid", borderColor: error ? "error.main" : "grey.400", borderRadius: 1,
|
|
146
|
+
px: 1, py: 0.75, minHeight: 42, cursor: "text",
|
|
147
|
+
"&:focus-within": { borderColor: error ? "error.main" : "primary.main" },
|
|
148
|
+
}, children: [segments.map((segment, index) => segment.type === "field" ? (_jsx(Chip, { label: segment.header, size: "small", color: "primary", variant: "outlined", onDelete: () => removeSegment(index), sx: { fontWeight: 500 } }, index)) : (_jsx(Typography, { variant: "body2", component: "span", sx: { fontFamily: "monospace", fontWeight: 600, color: "text.secondary", whiteSpace: "pre" }, children: segment.value }, index))), _jsx("input", { ref: inputRef, "aria-label": "\uC218\uC2DD \uC785\uB825", value: textInput, onChange: (event) => { setTextInput(event.target.value); setError(null); }, onKeyDown: handleKeyDown, placeholder: segments.length === 0 ? "아래 필드와 연산자를 클릭하거나 직접 입력하세요" : "", style: {
|
|
149
|
+
border: "none", outline: "none", flex: 1, minWidth: 80,
|
|
150
|
+
fontFamily: "monospace", fontSize: "0.875rem", padding: 0, background: "transparent",
|
|
151
|
+
} })] }), _jsx(Typography, { variant: "caption", sx: { color: "text.secondary", display: "block", mt: 0.5 }, children: type === "string"
|
|
152
|
+
? "문자열 템플릿 — {필드} 참조가 하나 이상 필요합니다. 예: {customer} / {channel}"
|
|
153
|
+
: "사칙연산 · 괄호 · 숫자 · DATE_TRUNC('month', 날짜) · EXTRACT('year', 날짜)" }), preview && (_jsxs(Box, { sx: { mt: 1, px: 1.5, py: 0.75, bgcolor: "action.hover", borderRadius: 1, border: "1px solid", borderColor: "divider" }, children: [_jsx(Typography, { variant: "caption", sx: { color: "text.secondary" }, children: "\uBBF8\uB9AC\uBCF4\uAE30" }), _jsxs(Typography, { variant: "body2", sx: { fontWeight: 500 }, "data-testid": "virtual-column-preview", children: ["= ", preview] })] }))] }), _jsxs(Box, { children: [_jsx(Typography, { variant: "body2", sx: { fontWeight: 500, color: "text.secondary", mb: 0.5 }, children: "\uC5F0\uC0B0\uC790" }), _jsx(Box, { sx: { display: "flex", gap: 0.5 }, children: OPERATORS.map((op) => (_jsx(Tooltip, { title: op.tooltip, placement: "top", children: _jsx(Button, { variant: "outlined", size: "small", "aria-label": `연산자 ${op.label}`, onClick: () => addOperator(op.value), sx: { minWidth: 36, px: 1, fontFamily: "monospace", fontWeight: 700, fontSize: "1rem" }, children: op.label }) }, op.label))) })] }), fields.length > 0 && (_jsxs(Box, { children: [_jsx(Typography, { variant: "body2", sx: { fontWeight: 500, color: "text.secondary", mb: 1 }, children: "\uD544\uB4DC (\uD074\uB9AD\uD558\uC5EC \uC218\uC2DD\uC5D0 \uC0BD\uC785)" }), _jsx(Box, { sx: { display: "flex", flexWrap: "wrap", gap: 0.5 }, children: fields.map((field) => (_jsx(Chip, { label: field.header, size: "small", variant: "outlined", onClick: () => addField(field), "data-testid": `virtual-field-${field.key}`, sx: { cursor: "pointer", "&:hover": { borderColor: "primary.main" } } }, field.key))) })] })), _jsxs(FormControl, { children: [_jsx(FormLabel, { sx: { fontSize: "0.875rem", fontWeight: 500 }, children: "\uACB0\uACFC \uD0C0\uC785" }), _jsx(RadioGroup, { row: true, value: type, onChange: (event) => { setType(event.target.value); setError(null); }, children: TYPE_OPTIONS.map((option) => (_jsx(FormControlLabel, { value: option.value, control: _jsx(Radio, { size: "small" }), label: option.label }, option.value))) })] }), error && _jsx(Alert, { severity: "error", role: "alert", children: error })] }) }), _jsxs(DialogActions, { sx: { px: 3, py: 2 }, children: [_jsx(Button, { onClick: onClose, color: "inherit", children: "\uCDE8\uC18C" }), _jsx(Button, { variant: "contained", onClick: handleSubmit, disabled: !canSubmit, children: isEdit ? "수정" : "추가" })] })] }));
|
|
154
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { RowData } from "@tanstack/react-table";
|
|
3
|
+
import type { VirtualColumnField } from "./expression";
|
|
4
|
+
import type { UseVirtualColumnsReturn } from "./useVirtualColumns";
|
|
5
|
+
export interface VirtualColumnSectionProps<T extends RowData = RowData> {
|
|
6
|
+
/** `useVirtualColumns(search)` 결과 */
|
|
7
|
+
virtualColumns: UseVirtualColumnsReturn<T>;
|
|
8
|
+
/** 수식에 쓸 필드. 생략하면 훅의 `fields` 를 쓴다 */
|
|
9
|
+
fields?: ReadonlyArray<VirtualColumnField>;
|
|
10
|
+
/**
|
|
11
|
+
* `FilterArea direction="grid"` 의 children 으로 넣을 때 카드 하나의 Grid size —
|
|
12
|
+
* `resolveFilterGridSize(gridCols)` 값을 그대로. 없으면 카드를 가로로 흘려 놓는다(툴바용).
|
|
13
|
+
*/
|
|
14
|
+
gridSize?: Record<string, number>;
|
|
15
|
+
/**
|
|
16
|
+
* 카드에 필터 입력을 붙인다. 값은 `search.setFilter(slotKey, …)` / `setOperator` 로 들어가
|
|
17
|
+
* 다른 필터와 같이 draft 에 쌓인다 (검색 버튼으로 적용).
|
|
18
|
+
*/
|
|
19
|
+
enableFilter?: boolean;
|
|
20
|
+
/** 추가 버튼 문구 */
|
|
21
|
+
addLabel?: ReactNode;
|
|
22
|
+
/** 카드 폭 (inline 배치일 때). 기본 220 */
|
|
23
|
+
cardWidth?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 가상 열 섹션 — 카드 목록 + 추가 버튼 + 추가/편집 다이얼로그.
|
|
27
|
+
*
|
|
28
|
+
* 기존 ERP `VirtualColumnSection` 대응. `FilterArea` 의 children 이나 툴바 어디든 놓을 수 있고
|
|
29
|
+
* 다이얼로그 상태를 스스로 관리한다. 상태는 훅(`useVirtualColumns`)이 검색 상태 위에서 들고 있다.
|
|
30
|
+
*/
|
|
31
|
+
export declare function VirtualColumnSection<T extends RowData>({ virtualColumns, fields: fieldsProp, gridSize, enableFilter, addLabel, cardWidth, }: VirtualColumnSectionProps<T>): import("react").JSX.Element;
|