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,434 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { isListValueOperator, isValuelessOperator } from "../types/operators";
|
|
3
|
+
import { columnKey, deriveOperators, resolveOperatorOptions, resolveVariant, } from "../columns/deriveOperators";
|
|
4
|
+
import { pruneCascadeValues, resolveCascadeOptions } from "../columns/cascade";
|
|
5
|
+
import { expandSorting } from "../columns/expandSorting";
|
|
6
|
+
import { deriveAggregation } from "../client/aggregate";
|
|
7
|
+
import { toListFilterValue } from "../filters/list";
|
|
8
|
+
import { isFilterValueEmpty, wireOperator } from "../filters/kind";
|
|
9
|
+
import { filterKindMap } from "../filters/codec";
|
|
10
|
+
import { DEFAULT_PAGINATION, DEFAULT_PAGE_SIZE } from "../defaults";
|
|
11
|
+
function baseState() {
|
|
12
|
+
return {
|
|
13
|
+
columnFilters: [],
|
|
14
|
+
columnFilterFns: {},
|
|
15
|
+
globalFilter: "",
|
|
16
|
+
pagination: DEFAULT_PAGINATION,
|
|
17
|
+
sorting: [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 초기 상태.
|
|
22
|
+
*
|
|
23
|
+
* 연산자는 **컬럼 선언에서 그대로 읽어** 처음부터 채운다. 원본은 이걸 effect 로
|
|
24
|
+
* 뒤늦게 채우느라(`useFilterFnsInit`) 그동안 첫 조회를 `filterFnsReady` 로 막아야
|
|
25
|
+
* 했는데, 컬럼 정의는 렌더 시점에 이미 존재하므로 기다릴 이유가 없다.
|
|
26
|
+
*/
|
|
27
|
+
function initialState(columns, initial, initialOperators) {
|
|
28
|
+
return {
|
|
29
|
+
...baseState(),
|
|
30
|
+
...initial,
|
|
31
|
+
columnFilterFns: {
|
|
32
|
+
...deriveOperators(columns),
|
|
33
|
+
...initialOperators,
|
|
34
|
+
...initial?.columnFilterFns,
|
|
35
|
+
},
|
|
36
|
+
// 참조 공유를 끊는다 — 날짜 범위처럼 배열 값을 제자리에서 바꾸는 입력이
|
|
37
|
+
// 초기값까지 함께 바꾸면 reset() 이 돌아갈 곳을 잃는다.
|
|
38
|
+
columnFilters: (initial?.columnFilters ?? []).map((f) => ({
|
|
39
|
+
...f,
|
|
40
|
+
value: Array.isArray(f.value) ? [...f.value] : f.value,
|
|
41
|
+
})),
|
|
42
|
+
sorting: [...(initial?.sorting ?? [])],
|
|
43
|
+
pagination: { ...(initial?.pagination ?? DEFAULT_PAGINATION) },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 미입력 필터를 걷어낸 비교·요청용 형태.
|
|
48
|
+
*
|
|
49
|
+
* 값을 쓰지 않는 연산자(`empty`/`isEmpty`/`hasChild` …)의 항목은 **값이 비어 있어도
|
|
50
|
+
* 남긴다** — 서버는 `value None` 인 필터를 통째로 건너뛰므로 항목이 사라지면
|
|
51
|
+
* 연산자만 바뀌고 아무 일도 일어나지 않는다. 값은 `true` 로 정규화한다
|
|
52
|
+
* (`false` 는 방향 반전이라 그대로 둔다).
|
|
53
|
+
*
|
|
54
|
+
* 컬럼에 kind 가 있으면 "비어 있음" 은 `kind.isEmpty`, "값 없는 연산자" 는
|
|
55
|
+
* `operators[op].valueless` 가 정한다. 연산자가 kind 에 없으면 공통 규칙으로 떨어진다.
|
|
56
|
+
*/
|
|
57
|
+
function normalizeFilters(filters, operators, kinds) {
|
|
58
|
+
const result = [];
|
|
59
|
+
for (const filter of filters) {
|
|
60
|
+
if (!filter)
|
|
61
|
+
continue;
|
|
62
|
+
const operator = operators[filter.id];
|
|
63
|
+
const kind = kinds?.get(filter.id);
|
|
64
|
+
const def = kind && operator ? kind.operators[operator] : undefined;
|
|
65
|
+
const unset = isFilterValueEmpty(kind, filter.value);
|
|
66
|
+
const valueless = def ? def.valueless === true : !!operator && isValuelessOperator(operator);
|
|
67
|
+
if (valueless) {
|
|
68
|
+
result.push(unset ? { ...filter, value: true } : filter);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!unset)
|
|
72
|
+
result.push(filter);
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 요청에 실을 필터 값 정규화 — 리스트 연산자의 스칼라 값을 `[value]` 로 감싼다.
|
|
78
|
+
*
|
|
79
|
+
* 단일 select 컬럼의 기본 연산자가 `inArray` 인데 값이 문자열 하나면, 서버
|
|
80
|
+
* `ListFilter.apply` 가 문자열을 글자 단위로 쪼개 enum 컬럼에서 500 을 낸다.
|
|
81
|
+
* 센티널(`EMPTY`/`_ALL_SELECTED`)도 배열 원소로 감싸져 의미가 보존된다.
|
|
82
|
+
*
|
|
83
|
+
* 컬럼 kind 에 연산자가 있으면 `operators[op].serialize` 가 값을 정한다 (내장 enum 류가
|
|
84
|
+
* 같은 배열 감싸기를 한다). 값 없는 연산자의 `true` 는 직렬화하지 않는다.
|
|
85
|
+
*/
|
|
86
|
+
function toRequestFilters(filters, operators, kinds) {
|
|
87
|
+
let changed = false;
|
|
88
|
+
const result = filters.map((filter) => {
|
|
89
|
+
const operator = operators[filter.id];
|
|
90
|
+
const kind = kinds?.get(filter.id);
|
|
91
|
+
const def = kind && operator ? kind.operators[operator] : undefined;
|
|
92
|
+
if (def) {
|
|
93
|
+
if (!def.serialize || def.valueless)
|
|
94
|
+
return filter;
|
|
95
|
+
const value = def.serialize(filter.value);
|
|
96
|
+
if (value === filter.value)
|
|
97
|
+
return filter;
|
|
98
|
+
changed = true;
|
|
99
|
+
return { ...filter, value };
|
|
100
|
+
}
|
|
101
|
+
if (!operator || !isListValueOperator(operator) || Array.isArray(filter.value))
|
|
102
|
+
return filter;
|
|
103
|
+
changed = true;
|
|
104
|
+
return { ...filter, value: toListFilterValue(filter.value) };
|
|
105
|
+
});
|
|
106
|
+
return changed ? result : filters;
|
|
107
|
+
}
|
|
108
|
+
/** 요청에 실을 연산자 이름 — kind 가 `wire` 를 선언한 것만 바꾼다. 바뀐 것이 없으면 같은 참조 */
|
|
109
|
+
function toRequestOperators(operators, kinds) {
|
|
110
|
+
if (kinds.size === 0)
|
|
111
|
+
return operators;
|
|
112
|
+
let changed = false;
|
|
113
|
+
const result = {};
|
|
114
|
+
for (const [id, operator] of Object.entries(operators)) {
|
|
115
|
+
const wire = wireOperator(kinds.get(id), operator);
|
|
116
|
+
if (wire !== operator)
|
|
117
|
+
changed = true;
|
|
118
|
+
result[id] = wire;
|
|
119
|
+
}
|
|
120
|
+
return changed ? result : operators;
|
|
121
|
+
}
|
|
122
|
+
function setEntry(filters, id, value) {
|
|
123
|
+
const rest = filters.filter((f) => f.id !== id);
|
|
124
|
+
if (value === undefined)
|
|
125
|
+
return rest;
|
|
126
|
+
return [...rest, { id, value }];
|
|
127
|
+
}
|
|
128
|
+
/** 부분 상태 병합 — `columnFilterFns` 만 얕게 합친다 */
|
|
129
|
+
function mergeState(base, partial) {
|
|
130
|
+
const next = { ...base, ...partial };
|
|
131
|
+
if (partial.columnFilterFns) {
|
|
132
|
+
next.columnFilterFns = { ...base.columnFilterFns, ...partial.columnFilterFns };
|
|
133
|
+
}
|
|
134
|
+
return next;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 1층 — 검색 상태. **테이블 인스턴스를 만들지 않는다.**
|
|
138
|
+
*
|
|
139
|
+
* 반환값이 `FilterSource` 를 만족하므로 필터 패널이 테이블 없이 바로 붙는다.
|
|
140
|
+
* 페이지네이션 UI 는 총 건수가 필요하므로 `paginationSource(search, data)` 로
|
|
141
|
+
* 2층과 합쳐 넘긴다.
|
|
142
|
+
*/
|
|
143
|
+
export function useTableSearch(options) {
|
|
144
|
+
const { columns, initial, initialOperators, onApply, debounceMs = 0, globalFilterApplyOnChange = false, } = options;
|
|
145
|
+
const initialRef = useRef(null);
|
|
146
|
+
if (initialRef.current === null) {
|
|
147
|
+
initialRef.current = initialState(columns, initial, initialOperators);
|
|
148
|
+
}
|
|
149
|
+
const [draft, setDraftState] = useState(() => initialState(columns, initialRef.current));
|
|
150
|
+
const [params, setParamsState] = useState(() => initialRef.current);
|
|
151
|
+
// 같은 이벤트 핸들러 안에서 setDraft 직후 apply() 를 불러도 최신 값을 보게 한다
|
|
152
|
+
const draftRef = useRef(draft);
|
|
153
|
+
draftRef.current = draft;
|
|
154
|
+
// 정렬·페이지처럼 draft 를 거치지 않는 조작이 **적용된 값** 위에서 일어나게 한다
|
|
155
|
+
const paramsRef = useRef(params);
|
|
156
|
+
paramsRef.current = params;
|
|
157
|
+
/**
|
|
158
|
+
* 컬럼에서 파생된 기본 연산자. 상태 밑에 깔아 두는 이유는 컬럼이 나중에
|
|
159
|
+
* 추가돼도 그 컬럼만 연산자가 비지 않게 하기 위해서다.
|
|
160
|
+
*/
|
|
161
|
+
const derived = useMemo(() => deriveOperators(columns), [columns]);
|
|
162
|
+
const operators = useMemo(() => ({ ...derived, ...params.columnFilterFns }), [derived, params.columnFilterFns]);
|
|
163
|
+
const draftOperators = useMemo(() => ({ ...derived, ...draft.columnFilterFns }), [derived, draft.columnFilterFns]);
|
|
164
|
+
const columnsById = useMemo(() => {
|
|
165
|
+
const map = new Map();
|
|
166
|
+
for (const column of columns) {
|
|
167
|
+
const key = columnKey(column);
|
|
168
|
+
if (key)
|
|
169
|
+
map.set(key, column);
|
|
170
|
+
}
|
|
171
|
+
return map;
|
|
172
|
+
}, [columns]);
|
|
173
|
+
/** 컬럼 id → 필터 kind (kind 를 선언한 컬럼만). 요청 조립·미입력 판정의 근거 */
|
|
174
|
+
const kindsById = useMemo(() => filterKindMap(columns), [columns]);
|
|
175
|
+
/**
|
|
176
|
+
* 다음 draft 를 **호출 시점에 즉시** 계산해 ref 와 state 에 함께 넣는다.
|
|
177
|
+
*
|
|
178
|
+
* 원본은 updater 안에서 ref 를 갱신했는데, React 는 updater 를 지연 실행할 수
|
|
179
|
+
* 있어(eager state 최적화는 큐가 비어 있을 때만 걸린다) 같은 핸들러에서
|
|
180
|
+
* `setFilter()` 직후 `apply()` 를 부르면 ref 가 낡은 값을 들고 있었다.
|
|
181
|
+
*/
|
|
182
|
+
const setDraft = useCallback((updater) => {
|
|
183
|
+
const next = typeof updater === "function" ? updater(draftRef.current) : updater;
|
|
184
|
+
draftRef.current = next;
|
|
185
|
+
setDraftState(next);
|
|
186
|
+
}, []);
|
|
187
|
+
const setParams = useCallback((next) => {
|
|
188
|
+
draftRef.current = next;
|
|
189
|
+
setDraftState(next);
|
|
190
|
+
paramsRef.current = next;
|
|
191
|
+
setParamsState(next);
|
|
192
|
+
}, []);
|
|
193
|
+
const patchParams = useCallback((partial) => setParams(mergeState(paramsRef.current, partial)), [setParams]);
|
|
194
|
+
const patchDraft = useCallback((partial) => setDraft((prev) => mergeState(prev, partial)), [setDraft]);
|
|
195
|
+
/**
|
|
196
|
+
* 즉시 반영 조작(정렬·페이지·그룹) — **적용된 params 위에** 해당 필드만 갱신한다.
|
|
197
|
+
*
|
|
198
|
+
* draft 위에 얹으면 미적용 필터·연산자·전역검색이 정렬 클릭에 딸려 나간다
|
|
199
|
+
* (기존 구현과 koko 모두 있던 결함). draft 는 유지하되 같은 필드는 같은 값으로
|
|
200
|
+
* 맞춰 두어 isDirty 가 정렬·페이지 때문에 튀지 않게 한다.
|
|
201
|
+
*/
|
|
202
|
+
const setImmediate = useCallback((partial) => {
|
|
203
|
+
const nextParams = { ...paramsRef.current, ...partial };
|
|
204
|
+
paramsRef.current = nextParams;
|
|
205
|
+
setParamsState(nextParams);
|
|
206
|
+
const nextDraft = { ...draftRef.current, ...partial };
|
|
207
|
+
draftRef.current = nextDraft;
|
|
208
|
+
setDraftState(nextDraft);
|
|
209
|
+
}, []);
|
|
210
|
+
// onApply 는 렌더마다 새 함수일 수 있다 — 타이머 콜백이 최신 것을 보게 ref 로 읽는다
|
|
211
|
+
const onApplyRef = useRef(onApply);
|
|
212
|
+
onApplyRef.current = onApply;
|
|
213
|
+
/** 즉시 적용 — 타이머와 무관한 본체 */
|
|
214
|
+
const applyNow = useCallback(() => {
|
|
215
|
+
// 검색은 항상 첫 페이지부터 — 3페이지에서 조건을 바꾸면 빈 화면이 나온다
|
|
216
|
+
const next = {
|
|
217
|
+
...draftRef.current,
|
|
218
|
+
pagination: {
|
|
219
|
+
pageIndex: 0,
|
|
220
|
+
pageSize: draftRef.current.pagination?.pageSize ?? DEFAULT_PAGE_SIZE,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
paramsRef.current = next;
|
|
224
|
+
setParamsState(next);
|
|
225
|
+
draftRef.current = next;
|
|
226
|
+
setDraftState(next);
|
|
227
|
+
onApplyRef.current?.(next);
|
|
228
|
+
}, []);
|
|
229
|
+
// ── debounce ──
|
|
230
|
+
// applyOnChange 컬럼·전역검색이 글자마다 조회를 내지 않도록 마지막 입력 뒤 `ms` 를 기다린다.
|
|
231
|
+
// 타이머는 하나뿐이다 — 어느 컬럼이 걸었든 apply 는 draft 전체를 싣기 때문에 합쳐도 결과가 같다.
|
|
232
|
+
const timerRef = useRef(null);
|
|
233
|
+
const [pendingApply, setPendingApply] = useState(false);
|
|
234
|
+
const cancelPending = useCallback(() => {
|
|
235
|
+
if (timerRef.current !== null) {
|
|
236
|
+
clearTimeout(timerRef.current);
|
|
237
|
+
timerRef.current = null;
|
|
238
|
+
}
|
|
239
|
+
setPendingApply(false);
|
|
240
|
+
}, []);
|
|
241
|
+
const scheduleApply = useCallback((ms) => {
|
|
242
|
+
if (!(ms > 0)) {
|
|
243
|
+
cancelPending();
|
|
244
|
+
applyNow();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (timerRef.current !== null)
|
|
248
|
+
clearTimeout(timerRef.current);
|
|
249
|
+
setPendingApply(true);
|
|
250
|
+
timerRef.current = setTimeout(() => {
|
|
251
|
+
timerRef.current = null;
|
|
252
|
+
setPendingApply(false);
|
|
253
|
+
applyNow();
|
|
254
|
+
}, ms);
|
|
255
|
+
}, [cancelPending, applyNow]);
|
|
256
|
+
// 언마운트 시 대기 중인 조회를 버린다 — 사라진 화면의 요청이 나가면 안 된다
|
|
257
|
+
useEffect(() => () => {
|
|
258
|
+
if (timerRef.current !== null)
|
|
259
|
+
clearTimeout(timerRef.current);
|
|
260
|
+
timerRef.current = null;
|
|
261
|
+
}, []);
|
|
262
|
+
/** 명시 apply — 대기 중인 타이머를 취소하고 즉시 */
|
|
263
|
+
const apply = useCallback(() => {
|
|
264
|
+
cancelPending();
|
|
265
|
+
applyNow();
|
|
266
|
+
}, [cancelPending, applyNow]);
|
|
267
|
+
const reset = useCallback(() => {
|
|
268
|
+
cancelPending();
|
|
269
|
+
setParams(initialState(columns, initialRef.current));
|
|
270
|
+
}, [cancelPending, setParams, columns]);
|
|
271
|
+
const setFilter = useCallback((id, value) => {
|
|
272
|
+
const spec = columnsById.get(id)?.filter;
|
|
273
|
+
const immediate = spec?.applyOnChange === true;
|
|
274
|
+
setDraft((prev) => {
|
|
275
|
+
const columnFilters = setEntry(prev.columnFilters, id, value);
|
|
276
|
+
// 상위가 바뀌면 하위 값이 더 이상 고를 수 없는 것이 될 수 있다.
|
|
277
|
+
// effect 로 뒤늦게 지우면 그 사이 잘못된 조건으로 조회가 나가므로
|
|
278
|
+
// **값을 넣는 그 자리에서** 정리한다.
|
|
279
|
+
return {
|
|
280
|
+
...prev,
|
|
281
|
+
columnFilters: pruneCascadeValues(columnFilters, resolveCascadeOptions(columns, columnFilters)),
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
// 컬럼 단위 지연이 옵션 기본값보다 우선한다
|
|
285
|
+
if (immediate)
|
|
286
|
+
scheduleApply(spec?.debounceMs ?? debounceMs);
|
|
287
|
+
}, [columnsById, columns, setDraft, scheduleApply, debounceMs]);
|
|
288
|
+
/**
|
|
289
|
+
* 연산자 변경도 **draft 에 쌓인다.** 즉시 반영하면 검색 버튼을 누르지 않았는데
|
|
290
|
+
* 결과가 바뀐다.
|
|
291
|
+
*
|
|
292
|
+
* 값을 쓰지 않는 연산자(`empty`/`isEmpty`/`hasChild` 등)로 바꿀 때는 필터 항목을
|
|
293
|
+
* 함께 만들어 준다. 서버도 클라이언트도 `columnFilters` 를 훑어 조건을 거니까,
|
|
294
|
+
* 항목이 없으면 연산자만 바뀌고 아무 일도 일어나지 않는다.
|
|
295
|
+
* (값 `true` = 비어 있는 행 / 자식 있는 행, `false` = 반대)
|
|
296
|
+
*/
|
|
297
|
+
const setOperator = useCallback((id, operator) => {
|
|
298
|
+
setDraft((prev) => {
|
|
299
|
+
const previous = (prev.columnFilterFns[id] ?? derived[id] ?? "fuzzy");
|
|
300
|
+
// kind 가 연산자를 알면 kind 의 valueless 가, 모르면 공통 목록이 정한다
|
|
301
|
+
const kind = kindsById.get(id);
|
|
302
|
+
const valueless = (op) => {
|
|
303
|
+
const def = kind?.operators[op];
|
|
304
|
+
return def ? def.valueless === true : isValuelessOperator(op);
|
|
305
|
+
};
|
|
306
|
+
const wasValueless = valueless(previous);
|
|
307
|
+
const nowValueless = valueless(operator);
|
|
308
|
+
let columnFilters = prev.columnFilters;
|
|
309
|
+
if (nowValueless && !wasValueless) {
|
|
310
|
+
columnFilters = setEntry(columnFilters, id, true);
|
|
311
|
+
}
|
|
312
|
+
else if (!nowValueless && wasValueless) {
|
|
313
|
+
// 값 없는 연산자에서 빠져나올 때 남은 true 를 지운다 — 안 지우면
|
|
314
|
+
// 텍스트 입력에 "true" 가 들어 있는 것처럼 동작한다
|
|
315
|
+
columnFilters = setEntry(columnFilters, id, undefined);
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
...prev,
|
|
319
|
+
columnFilters,
|
|
320
|
+
columnFilterFns: { ...prev.columnFilterFns, [id]: operator },
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
}, [setDraft, derived, kindsById]);
|
|
324
|
+
const setGlobalFilter = useCallback((value) => {
|
|
325
|
+
setDraft((prev) => ({ ...prev, globalFilter: value }));
|
|
326
|
+
if (globalFilterApplyOnChange)
|
|
327
|
+
scheduleApply(debounceMs);
|
|
328
|
+
}, [setDraft, globalFilterApplyOnChange, scheduleApply, debounceMs]);
|
|
329
|
+
// 정렬·페이지·그룹은 검색 버튼을 거치지 않는 즉시 반영 조작이다
|
|
330
|
+
const setSorting = useCallback((sorting) => setImmediate({ sorting }), [setImmediate]);
|
|
331
|
+
const setGrouping = useCallback((grouping) => setImmediate({ grouping }), [setImmediate]);
|
|
332
|
+
const setPagination = useCallback((pagination) => setImmediate({ pagination }), [setImmediate]);
|
|
333
|
+
const setPageIndex = useCallback((pageIndex) => setPagination({ ...paramsRef.current.pagination, pageIndex }), [setPagination]);
|
|
334
|
+
const setPageSize = useCallback((pageSize) => setPagination({ pageIndex: 0, pageSize }), [setPagination]);
|
|
335
|
+
const isDirty = useMemo(() => {
|
|
336
|
+
// 정렬·페이지는 즉시 반영이므로 비교에서 뺀다 — 넣으면 항상 "검색 필요"가 된다.
|
|
337
|
+
// 연산자는 **넣는다** — fuzzy 에서 equals 로 바꾸면 값이 그대로여도 결과가 달라진다.
|
|
338
|
+
const key = (state, ops) => JSON.stringify({
|
|
339
|
+
columnFilters: normalizeFilters(state.columnFilters, ops, kindsById),
|
|
340
|
+
globalFilter: state.globalFilter,
|
|
341
|
+
columnFilterFns: state.columnFilterFns,
|
|
342
|
+
});
|
|
343
|
+
return key(draft, draftOperators) !== key(params, operators);
|
|
344
|
+
}, [draft, params, draftOperators, operators, kindsById]);
|
|
345
|
+
// 요청용 필터 — 페이지만 바뀌면 `params.columnFilters` 참조가 유지되므로 재계산되지
|
|
346
|
+
// 않는다. 2층이 필터·정렬 결과를 따로 메모이즈할 수 있는 근거다.
|
|
347
|
+
const requestFilters = useMemo(() => toRequestFilters(normalizeFilters(params.columnFilters, operators, kindsById), operators, kindsById), [params.columnFilters, operators, kindsById]);
|
|
348
|
+
// 요청용 연산자 — kind 가 `wire` 로 서버 이름을 달리 정한 것만 바뀐다 (내장 kind 는 그대로)
|
|
349
|
+
const requestOperators = useMemo(() => toRequestOperators(operators, kindsById), [operators, kindsById]);
|
|
350
|
+
// 통합 컬럼(`meta.sortExpandsTo`) 정렬은 실제 필드로 펼쳐 싣는다. UI 상태는 그대로다.
|
|
351
|
+
const requestSorting = useMemo(() => expandSorting(params.sorting, columns), [params.sorting, columns]);
|
|
352
|
+
const derivedAggregation = useMemo(() => deriveAggregation(columns), [columns]);
|
|
353
|
+
const request = useMemo(() => {
|
|
354
|
+
const grouping = params.grouping && params.grouping.length > 0 ? params.grouping : undefined;
|
|
355
|
+
return {
|
|
356
|
+
columnFilters: requestFilters,
|
|
357
|
+
// 원본과 같이 **전체 맵**을 싣는다. 값 없는 연산자나 서버가 컬럼 기본값을
|
|
358
|
+
// 참조하는 경로가 있어 부분 맵으로는 의미가 달라진다.
|
|
359
|
+
columnFilterFns: requestOperators,
|
|
360
|
+
globalFilter: params.globalFilter || undefined,
|
|
361
|
+
pagination: params.pagination,
|
|
362
|
+
sorting: requestSorting.length > 0 ? requestSorting : undefined,
|
|
363
|
+
grouping: params.grouping,
|
|
364
|
+
virtualColumns: params.virtualColumns,
|
|
365
|
+
// 그룹핑할 때만 집계를 싣는다 — 서버 `aggregation` 은 그룹 헤더/노드의 aggregates 용이다
|
|
366
|
+
aggregation: grouping ? params.aggregation ?? derivedAggregation : undefined,
|
|
367
|
+
};
|
|
368
|
+
}, [params, requestOperators, requestFilters, requestSorting, derivedAggregation]);
|
|
369
|
+
/** 편집 중인 값 기준으로 좁혀진 하위 선택지 */
|
|
370
|
+
const cascadeOptions = useMemo(() => resolveCascadeOptions(columns, draft.columnFilters), [columns, draft.columnFilters]);
|
|
371
|
+
const getFilterFields = useCallback(() => {
|
|
372
|
+
const fields = [];
|
|
373
|
+
for (const column of columns) {
|
|
374
|
+
const id = columnKey(column);
|
|
375
|
+
if (!id)
|
|
376
|
+
continue;
|
|
377
|
+
const spec = column.filter;
|
|
378
|
+
if (spec?.hidden || column.meta?.forceHidden)
|
|
379
|
+
continue;
|
|
380
|
+
const entry = draft.columnFilters.find((f) => f.id === id);
|
|
381
|
+
const narrowed = cascadeOptions.get(id);
|
|
382
|
+
fields.push({
|
|
383
|
+
id,
|
|
384
|
+
header: column.filterLabel ?? column.header,
|
|
385
|
+
variant: resolveVariant(column),
|
|
386
|
+
kind: kindsById.get(id),
|
|
387
|
+
// cascade 컬럼은 상위 선택에 따라 선택지가 줄어든다
|
|
388
|
+
meta: narrowed ? { ...column.meta, selectOptions: narrowed } : column.meta,
|
|
389
|
+
value: entry?.value,
|
|
390
|
+
onChange: (value) => setFilter(id, value),
|
|
391
|
+
// 편집 중인 값을 봐야 한다 — params 를 보면 방금 바꾼 연산자가 반영되지 않는다
|
|
392
|
+
operator: draftOperators[id] ?? "fuzzy",
|
|
393
|
+
onOperatorChange: (operator) => setOperator(id, operator),
|
|
394
|
+
operatorOptions: resolveOperatorOptions(column),
|
|
395
|
+
apply: spec?.applyOnChange ? apply : undefined,
|
|
396
|
+
// kind 별 값 타입으로 좁혀진 render 를 필드 계약(unknown) 으로 넘긴다
|
|
397
|
+
render: spec?.render,
|
|
398
|
+
detail: spec?.detail,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return fields;
|
|
402
|
+
}, [columns, draft.columnFilters, draftOperators, cascadeOptions, kindsById, setFilter, setOperator, apply]);
|
|
403
|
+
return useMemo(() => ({
|
|
404
|
+
columns,
|
|
405
|
+
draft,
|
|
406
|
+
params,
|
|
407
|
+
initialParams: initialRef.current,
|
|
408
|
+
operators,
|
|
409
|
+
draftOperators,
|
|
410
|
+
setFilter,
|
|
411
|
+
setOperator,
|
|
412
|
+
setGlobalFilter,
|
|
413
|
+
setSorting,
|
|
414
|
+
setGrouping,
|
|
415
|
+
setPagination,
|
|
416
|
+
setPageIndex,
|
|
417
|
+
setPageSize,
|
|
418
|
+
setDraft,
|
|
419
|
+
setParams,
|
|
420
|
+
patchParams,
|
|
421
|
+
patchDraft,
|
|
422
|
+
apply,
|
|
423
|
+
pendingApply,
|
|
424
|
+
reset,
|
|
425
|
+
isDirty,
|
|
426
|
+
request,
|
|
427
|
+
getFilterFields,
|
|
428
|
+
}), [
|
|
429
|
+
columns, draft, params, operators, draftOperators, setFilter, setOperator,
|
|
430
|
+
setGlobalFilter, setSorting, setGrouping, setPagination, setPageIndex,
|
|
431
|
+
setPageSize, setDraft, setParams, patchParams, patchDraft, apply, pendingApply, reset, isDirty,
|
|
432
|
+
request, getFilterFields,
|
|
433
|
+
]);
|
|
434
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { SearchState, TableSearch } from "../types/search";
|
|
3
|
+
import type { TableColumnDefs } from "../types/column";
|
|
4
|
+
export declare const DEFAULT_URL_PARAM_KEY = "tp";
|
|
5
|
+
/**
|
|
6
|
+
* 검색 상태 → URL-safe base64 (`tp=` 값).
|
|
7
|
+
*
|
|
8
|
+
* `useUrlSync` 가 URL 에 쓰는 것과 같은 인코딩이다. 공개하는 이유: 공유 링크를
|
|
9
|
+
* 현재 창의 URL 이 아니라 **다른 경로**로 만들어야 하는 호출부(알림 공유, 서버
|
|
10
|
+
* 렌더 링크 등)가 `window.location` 을 거치지 않고 값을 만들 수 있어야 한다.
|
|
11
|
+
* 실패(순환 참조 등)하면 빈 문자열.
|
|
12
|
+
*/
|
|
13
|
+
export declare function encodeUrlSearchState(state: Partial<SearchState>): string;
|
|
14
|
+
/** `encodeUrlSearchState` 의 역 — koko 인코딩만 읽는다. 구 포맷 폴백은 `readUrlSearchState` 가 한다 */
|
|
15
|
+
export declare function decodeUrlSearchState(text: string): Partial<SearchState> | undefined;
|
|
16
|
+
export interface ReadUrlSearchStateOptions {
|
|
17
|
+
/** 기본 `tp` */
|
|
18
|
+
paramKey?: string;
|
|
19
|
+
/**
|
|
20
|
+
* `paramKey` 가 없을 때 차례로 훑는 구 스킴 키 (기본 `["filter"]` —
|
|
21
|
+
* ERP `utils/tableUrlParams.ts` 의 `filter=` + `btoa(encodeURIComponent(JSON))`).
|
|
22
|
+
* 빈 배열이면 구 스킴을 읽지 않는다.
|
|
23
|
+
*/
|
|
24
|
+
legacyParamKeys?: readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* 구 포맷 정규화(ISO 시각 → `YYYY-MM-DD`) 에 쓸 컬럼. 주면 date/datetime 컬럼에만
|
|
27
|
+
* 적용하고, 없으면 ISO 패턴 + 날짜 연산자 항목에만 적용한다.
|
|
28
|
+
*/
|
|
29
|
+
columns?: TableColumnDefs<any>;
|
|
30
|
+
/** 구 포맷으로 읽힌 값의 정규화를 끈다 (기본 false) */
|
|
31
|
+
skipLegacyNormalize?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* URL 에서 검색 조건을 읽는다 — `useTableSearch({ initial })` 에 그대로 넘긴다.
|
|
35
|
+
*
|
|
36
|
+
* 읽기를 훅에서 분리한 이유: 기존 `useUrlTableParams` 는 초기값을 넘겨줄 방법이
|
|
37
|
+
* 없어 `connectParams(params, setParams)` 를 **렌더 도중** 호출했다. 읽기가
|
|
38
|
+
* 순수 함수면 그 우회가 통째로 사라진다.
|
|
39
|
+
*
|
|
40
|
+
* 구 포맷 호환: koko 인코딩(UTF-8 base64) 으로 읽히지 않으면
|
|
41
|
+
* `decodeLegacyTableParams` 로 폴백하고, `paramKey` 가 없으면 `legacyParamKeys`
|
|
42
|
+
* (기본 `filter`) 를 읽는다. 구 포맷으로 읽힌 값은 `normalizeLegacySearchState` 로
|
|
43
|
+
* koko `SearchState` 모양에 맞춘다. 첫 인자는 기존처럼 문자열(paramKey) 도 받는다.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readUrlSearchState(paramKeyOrOptions?: string | ReadUrlSearchStateOptions): Partial<SearchState> | undefined;
|
|
46
|
+
export interface UseUrlSyncOptions {
|
|
47
|
+
enabled?: boolean;
|
|
48
|
+
paramKey?: string;
|
|
49
|
+
/** popstate 복원 시 구 스킴 키도 읽는다 (기본 `["filter"]`). 쓰기는 항상 `paramKey` 로만 */
|
|
50
|
+
legacyParamKeys?: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
export interface UrlSync {
|
|
53
|
+
/** 현재 조건이 담긴 공유용 URL */
|
|
54
|
+
shareUrl: string;
|
|
55
|
+
/** 클립보드 복사 — 실패하면 false */
|
|
56
|
+
copyShareUrl: () => Promise<boolean>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* URL 동기화 애드온 — 1층이 독립했으므로 바깥에서 얹는 훅이 된다.
|
|
60
|
+
*
|
|
61
|
+
* - `params` 가 바뀌면 URL 을 갱신한다 (`replaceState`)
|
|
62
|
+
* - 뒤로/앞으로 가면 URL 을 읽어 `setParams` 한다
|
|
63
|
+
*/
|
|
64
|
+
export declare function useUrlSync<T extends RowData>(search: TableSearch<T>, options?: UseUrlSyncOptions): UrlSync;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { DEFAULT_LEGACY_PARAM_KEYS, decodeLegacyTableParams, normalizeLegacySearchState, } from "../contracts/legacyUrl";
|
|
3
|
+
export const DEFAULT_URL_PARAM_KEY = "tp";
|
|
4
|
+
/**
|
|
5
|
+
* 검색 상태 → URL-safe base64 (`tp=` 값).
|
|
6
|
+
*
|
|
7
|
+
* `useUrlSync` 가 URL 에 쓰는 것과 같은 인코딩이다. 공개하는 이유: 공유 링크를
|
|
8
|
+
* 현재 창의 URL 이 아니라 **다른 경로**로 만들어야 하는 호출부(알림 공유, 서버
|
|
9
|
+
* 렌더 링크 등)가 `window.location` 을 거치지 않고 값을 만들 수 있어야 한다.
|
|
10
|
+
* 실패(순환 참조 등)하면 빈 문자열.
|
|
11
|
+
*/
|
|
12
|
+
export function encodeUrlSearchState(state) {
|
|
13
|
+
return encode(state);
|
|
14
|
+
}
|
|
15
|
+
/** URL-safe base64 */
|
|
16
|
+
function encode(state) {
|
|
17
|
+
try {
|
|
18
|
+
const json = JSON.stringify(state);
|
|
19
|
+
const bytes = new TextEncoder().encode(json);
|
|
20
|
+
let binary = "";
|
|
21
|
+
for (const byte of bytes)
|
|
22
|
+
binary += String.fromCharCode(byte);
|
|
23
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** `encodeUrlSearchState` 의 역 — koko 인코딩만 읽는다. 구 포맷 폴백은 `readUrlSearchState` 가 한다 */
|
|
30
|
+
export function decodeUrlSearchState(text) {
|
|
31
|
+
return decode(text);
|
|
32
|
+
}
|
|
33
|
+
function decode(text) {
|
|
34
|
+
try {
|
|
35
|
+
const base64 = text.replace(/-/g, "+").replace(/_/g, "/");
|
|
36
|
+
const binary = atob(base64);
|
|
37
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
38
|
+
// fatal: 잘못된 UTF-8 바이트열(기존 btoa(JSON) 의 Latin1 문자)은 조용히 U+FFFD 로
|
|
39
|
+
// 바꾸지 말고 실패시켜, readUrlSearchState 가 구 포맷 디코더로 폴백하게 한다
|
|
40
|
+
const json = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
41
|
+
return JSON.parse(json);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* URL 에서 검색 조건을 읽는다 — `useTableSearch({ initial })` 에 그대로 넘긴다.
|
|
49
|
+
*
|
|
50
|
+
* 읽기를 훅에서 분리한 이유: 기존 `useUrlTableParams` 는 초기값을 넘겨줄 방법이
|
|
51
|
+
* 없어 `connectParams(params, setParams)` 를 **렌더 도중** 호출했다. 읽기가
|
|
52
|
+
* 순수 함수면 그 우회가 통째로 사라진다.
|
|
53
|
+
*
|
|
54
|
+
* 구 포맷 호환: koko 인코딩(UTF-8 base64) 으로 읽히지 않으면
|
|
55
|
+
* `decodeLegacyTableParams` 로 폴백하고, `paramKey` 가 없으면 `legacyParamKeys`
|
|
56
|
+
* (기본 `filter`) 를 읽는다. 구 포맷으로 읽힌 값은 `normalizeLegacySearchState` 로
|
|
57
|
+
* koko `SearchState` 모양에 맞춘다. 첫 인자는 기존처럼 문자열(paramKey) 도 받는다.
|
|
58
|
+
*/
|
|
59
|
+
export function readUrlSearchState(paramKeyOrOptions = DEFAULT_URL_PARAM_KEY) {
|
|
60
|
+
if (typeof window === "undefined")
|
|
61
|
+
return undefined;
|
|
62
|
+
const options = typeof paramKeyOrOptions === "string" ? { paramKey: paramKeyOrOptions } : paramKeyOrOptions;
|
|
63
|
+
const { paramKey = DEFAULT_URL_PARAM_KEY, legacyParamKeys = DEFAULT_LEGACY_PARAM_KEYS, columns, skipLegacyNormalize = false, } = options;
|
|
64
|
+
const normalize = (state) => skipLegacyNormalize ? state : normalizeLegacySearchState(state, columns);
|
|
65
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
66
|
+
const encoded = searchParams.get(paramKey);
|
|
67
|
+
if (encoded) {
|
|
68
|
+
const own = decode(encoded);
|
|
69
|
+
if (own)
|
|
70
|
+
return own;
|
|
71
|
+
const legacy = decodeLegacyTableParams(encoded);
|
|
72
|
+
return legacy ? normalize(legacy) : undefined;
|
|
73
|
+
}
|
|
74
|
+
for (const key of legacyParamKeys) {
|
|
75
|
+
if (key === paramKey)
|
|
76
|
+
continue;
|
|
77
|
+
const value = searchParams.get(key);
|
|
78
|
+
if (!value)
|
|
79
|
+
continue;
|
|
80
|
+
const legacy = decodeLegacyTableParams(value);
|
|
81
|
+
if (legacy)
|
|
82
|
+
return normalize(legacy);
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
function writeUrl(state, paramKey, push) {
|
|
87
|
+
const encoded = encode(state);
|
|
88
|
+
if (!encoded)
|
|
89
|
+
return;
|
|
90
|
+
const url = new URL(window.location.href);
|
|
91
|
+
url.searchParams.set(paramKey, encoded);
|
|
92
|
+
const method = push ? "pushState" : "replaceState";
|
|
93
|
+
window.history[method](window.history.state, "", url.toString());
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* URL 동기화 애드온 — 1층이 독립했으므로 바깥에서 얹는 훅이 된다.
|
|
97
|
+
*
|
|
98
|
+
* - `params` 가 바뀌면 URL 을 갱신한다 (`replaceState`)
|
|
99
|
+
* - 뒤로/앞으로 가면 URL 을 읽어 `setParams` 한다
|
|
100
|
+
*/
|
|
101
|
+
export function useUrlSync(search, options = {}) {
|
|
102
|
+
const { enabled = true, paramKey = DEFAULT_URL_PARAM_KEY, legacyParamKeys } = options;
|
|
103
|
+
const [shareUrl, setShareUrl] = useState("");
|
|
104
|
+
// popstate 로 들어온 변경을 다시 URL 에 쓰지 않기 위한 가드
|
|
105
|
+
const fromUrlRef = useRef(false);
|
|
106
|
+
const searchRef = useRef(search);
|
|
107
|
+
searchRef.current = search;
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!enabled || typeof window === "undefined")
|
|
110
|
+
return;
|
|
111
|
+
if (fromUrlRef.current) {
|
|
112
|
+
fromUrlRef.current = false;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
writeUrl(search.params, paramKey, false);
|
|
116
|
+
setShareUrl(window.location.href);
|
|
117
|
+
}, [enabled, paramKey, search.params]);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!enabled || typeof window === "undefined")
|
|
120
|
+
return;
|
|
121
|
+
const onPopState = () => {
|
|
122
|
+
const restored = readUrlSearchState({
|
|
123
|
+
paramKey,
|
|
124
|
+
legacyParamKeys,
|
|
125
|
+
columns: searchRef.current.columns,
|
|
126
|
+
});
|
|
127
|
+
if (!restored)
|
|
128
|
+
return;
|
|
129
|
+
fromUrlRef.current = true;
|
|
130
|
+
searchRef.current.setParams({ ...searchRef.current.params, ...restored });
|
|
131
|
+
};
|
|
132
|
+
window.addEventListener("popstate", onPopState);
|
|
133
|
+
return () => window.removeEventListener("popstate", onPopState);
|
|
134
|
+
}, [enabled, paramKey, legacyParamKeys]);
|
|
135
|
+
const copyShareUrl = useCallback(async () => {
|
|
136
|
+
try {
|
|
137
|
+
await navigator.clipboard.writeText(window.location.href);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}, []);
|
|
144
|
+
return { shareUrl, copyShareUrl };
|
|
145
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export * from "./types";
|
|
2
|
+
export * from "./defaults";
|
|
3
|
+
export * from "./utils/getValue";
|
|
4
|
+
export * from "./utils/compare";
|
|
5
|
+
export * from "./filters/text";
|
|
6
|
+
export * from "./filters/number";
|
|
7
|
+
export * from "./filters/date";
|
|
8
|
+
export * from "./filters/list";
|
|
9
|
+
export * from "./filters/relation";
|
|
10
|
+
export * from "./filters/linear";
|
|
11
|
+
export * from "./filters/schedule";
|
|
12
|
+
export * from "./filters/registry";
|
|
13
|
+
export * from "./filters/kind";
|
|
14
|
+
export * from "./filters/kinds";
|
|
15
|
+
export * from "./filters/describe";
|
|
16
|
+
export * from "./filters/codec";
|
|
17
|
+
export { formatLocalDate, toDateOnly, ISO_DATETIME } from "./utils/dateText";
|
|
18
|
+
export * from "./client/runClientQuery";
|
|
19
|
+
export * from "./client/aggregate";
|
|
20
|
+
export * from "./client/groupRows";
|
|
21
|
+
export * from "./types/grouping";
|
|
22
|
+
export * from "./pivot/transforms";
|
|
23
|
+
export * from "./pivot/runClientPivot";
|
|
24
|
+
export * from "./adapters/sources";
|
|
25
|
+
export * from "./columns/deriveOperators";
|
|
26
|
+
export * from "./columns/labels";
|
|
27
|
+
export * from "./columns/cascade";
|
|
28
|
+
export * from "./columns/ColumnBuilder";
|
|
29
|
+
export * from "./columns/expandSorting";
|
|
30
|
+
export { createColumnHelper, type ColumnHelper, type BaseColumnHelper, type ColumnHelperDeps, type ColumnParams, type ColumnFactory, type CompositeComponent, type CommonCodeLookup, type CommonCodeOption, type UserLookup, } from "./columns/helper";
|
|
31
|
+
export * from "./hooks";
|
|
32
|
+
export * from "./contracts";
|
|
33
|
+
export { createLocalStoragePresetStore, createMemoryPresetStore, loadDefaultPreset, newPresetId, type TablePreset, type PresetInput, type PresetScope, type PresetStore, type MemoryPresetStoreOptions, type LocalStoragePresetStoreOptions, } from "./contracts/presetStore";
|
|
34
|
+
export { usePresets, type UsePresetsOptions, type Presets, type SavePresetOptions, type PresetPatch, } from "./contracts/usePresets";
|
|
35
|
+
export * from "./types/definition";
|
|
36
|
+
export * from "./hooks/useTableFromDefinition";
|
|
37
|
+
export * from "./registry";
|
|
38
|
+
export * from "./contract";
|