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,56 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type SxProps, type Theme } from "@mui/material";
|
|
3
|
+
import type { Presets, PresetScope, TablePreset } from "../../../core/src/index";
|
|
4
|
+
export interface PresetBarMessages {
|
|
5
|
+
empty: string;
|
|
6
|
+
save: string;
|
|
7
|
+
saveTitle: string;
|
|
8
|
+
name: string;
|
|
9
|
+
scope: string;
|
|
10
|
+
scopeUser: string;
|
|
11
|
+
scopeShared: string;
|
|
12
|
+
makeDefault: string;
|
|
13
|
+
clearDefault: string;
|
|
14
|
+
apply: string;
|
|
15
|
+
overwrite: string;
|
|
16
|
+
rename: string;
|
|
17
|
+
renameTitle: string;
|
|
18
|
+
remove: string;
|
|
19
|
+
removeTitle: string;
|
|
20
|
+
removeConfirm: (name: string) => string;
|
|
21
|
+
cancel: string;
|
|
22
|
+
confirm: string;
|
|
23
|
+
defaultBadge: string;
|
|
24
|
+
sharedBadge: string;
|
|
25
|
+
manage: (name: string) => string;
|
|
26
|
+
loadFailed: string;
|
|
27
|
+
}
|
|
28
|
+
export interface PresetBarProps {
|
|
29
|
+
/** `usePresets(...)` 결과 */
|
|
30
|
+
presets: Presets;
|
|
31
|
+
/** 저장 버튼. 끄면 읽기 전용 바가 된다 */
|
|
32
|
+
allowSave?: boolean;
|
|
33
|
+
/** 칩의 관리 메뉴(덮어쓰기·이름 변경·기본 지정·삭제) */
|
|
34
|
+
allowManage?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* 저장 다이얼로그에 보일 범위. 하나만 주면 선택 UI 가 사라진다.
|
|
37
|
+
* 기본 `["user", "shared"]` — 서버 저장소가 공유를 모르면 `["user"]` 로 줄인다.
|
|
38
|
+
*/
|
|
39
|
+
scopes?: PresetScope[];
|
|
40
|
+
/** 공유 프리셋을 수정·삭제할 수 있는가 — 기본 모두 허용. ERP 는 `owner` 비교를 여기에 건다 */
|
|
41
|
+
canModify?: (preset: TablePreset) => boolean;
|
|
42
|
+
size?: "small" | "medium";
|
|
43
|
+
/** 저장 버튼을 아이콘 대신 텍스트 버튼으로 */
|
|
44
|
+
saveVariant?: "icon" | "button";
|
|
45
|
+
messages?: Partial<PresetBarMessages>;
|
|
46
|
+
/** 칩 뒤에 붙는 추가 요소 */
|
|
47
|
+
actions?: ReactNode;
|
|
48
|
+
sx?: SxProps<Theme>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 프리셋 바 — 기존 ERP `PresetChips` + `SavePresetDialog` + `ManagePresetsDialog` 를 하나로.
|
|
52
|
+
*
|
|
53
|
+
* 칩을 누르면 적용, 칩의 점 세 개 메뉴에서 덮어쓰기·이름 변경·기본 지정·삭제.
|
|
54
|
+
* 저장소·표 상태는 전부 `usePresets` 가 들고 있어 이 컴포넌트는 UI 만 안다.
|
|
55
|
+
*/
|
|
56
|
+
export declare function PresetBar({ presets, allowSave, allowManage, scopes, canModify, size, saveVariant, messages: givenMessages, actions, sx, }: PresetBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Button, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, FormLabel, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Radio, RadioGroup, Stack, TextField, Tooltip, Typography, } from "@mui/material";
|
|
4
|
+
import AddIcon from "@mui/icons-material/Add";
|
|
5
|
+
import CheckIcon from "@mui/icons-material/Check";
|
|
6
|
+
import DeleteIcon from "@mui/icons-material/Delete";
|
|
7
|
+
import EditIcon from "@mui/icons-material/Edit";
|
|
8
|
+
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
|
9
|
+
import PeopleIcon from "@mui/icons-material/People";
|
|
10
|
+
import SaveIcon from "@mui/icons-material/Save";
|
|
11
|
+
import StarIcon from "@mui/icons-material/Star";
|
|
12
|
+
import StarBorderIcon from "@mui/icons-material/StarBorder";
|
|
13
|
+
const DEFAULT_MESSAGES = {
|
|
14
|
+
empty: "저장된 프리셋 없음",
|
|
15
|
+
save: "프리셋 저장",
|
|
16
|
+
saveTitle: "현재 상태 저장",
|
|
17
|
+
name: "이름",
|
|
18
|
+
scope: "범위",
|
|
19
|
+
scopeUser: "나만",
|
|
20
|
+
scopeShared: "공유",
|
|
21
|
+
makeDefault: "기본 프리셋으로 지정",
|
|
22
|
+
clearDefault: "기본 지정 해제",
|
|
23
|
+
apply: "적용",
|
|
24
|
+
overwrite: "현재 상태를 덮어쓰기",
|
|
25
|
+
rename: "이름 변경",
|
|
26
|
+
renameTitle: "프리셋 이름 변경",
|
|
27
|
+
remove: "삭제",
|
|
28
|
+
removeTitle: "프리셋 삭제",
|
|
29
|
+
removeConfirm: (name) => `"${name}" 프리셋을 삭제할까요?`,
|
|
30
|
+
cancel: "취소",
|
|
31
|
+
confirm: "확인",
|
|
32
|
+
defaultBadge: "기본",
|
|
33
|
+
sharedBadge: "공유",
|
|
34
|
+
manage: (name) => `${name} 관리`,
|
|
35
|
+
loadFailed: "프리셋을 불러오지 못했습니다",
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* 프리셋 바 — 기존 ERP `PresetChips` + `SavePresetDialog` + `ManagePresetsDialog` 를 하나로.
|
|
39
|
+
*
|
|
40
|
+
* 칩을 누르면 적용, 칩의 점 세 개 메뉴에서 덮어쓰기·이름 변경·기본 지정·삭제.
|
|
41
|
+
* 저장소·표 상태는 전부 `usePresets` 가 들고 있어 이 컴포넌트는 UI 만 안다.
|
|
42
|
+
*/
|
|
43
|
+
export function PresetBar({ presets, allowSave = true, allowManage = true, scopes = ["user", "shared"], canModify, size = "small", saveVariant = "icon", messages: givenMessages, actions, sx, }) {
|
|
44
|
+
const messages = { ...DEFAULT_MESSAGES, ...givenMessages };
|
|
45
|
+
const [saveOpen, setSaveOpen] = useState(false);
|
|
46
|
+
const [menu, setMenu] = useState(null);
|
|
47
|
+
const [renameTarget, setRenameTarget] = useState(null);
|
|
48
|
+
const [removeTarget, setRemoveTarget] = useState(null);
|
|
49
|
+
const closeMenu = () => setMenu(null);
|
|
50
|
+
const modifiable = (preset) => (canModify ? canModify(preset) : true);
|
|
51
|
+
const runMenu = (work) => {
|
|
52
|
+
closeMenu();
|
|
53
|
+
void Promise.resolve(work()).catch(() => {
|
|
54
|
+
// 오류는 presets.error / onError 로 간다 — 여기서 다시 던지면 unhandled rejection 이 된다
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const chipIcon = (preset) => {
|
|
58
|
+
if (preset.isDefault)
|
|
59
|
+
return _jsx(StarIcon, { fontSize: "small", "aria-label": messages.defaultBadge });
|
|
60
|
+
if (preset.scope === "shared")
|
|
61
|
+
return _jsx(PeopleIcon, { fontSize: "small", "aria-label": messages.sharedBadge });
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
return (_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, sx: { minWidth: 0, flexWrap: "wrap", rowGap: 0.5, ...sx }, children: [presets.loading && _jsx(CircularProgress, { size: 16, thickness: 5, "aria-label": "\uD504\uB9AC\uC14B \uBD88\uB7EC\uC624\uB294 \uC911" }), presets.error != null && !presets.loading && (_jsx(Typography, { variant: "caption", color: "error", children: messages.loadFailed })), !presets.loading && presets.error == null && presets.presets.length === 0 && (_jsx(Typography, { variant: "body2", color: "text.disabled", children: messages.empty })), presets.presets.map((preset) => {
|
|
65
|
+
const active = preset.id === presets.activeId;
|
|
66
|
+
return (_jsx(Chip, { label: preset.name, size: size, icon: chipIcon(preset), color: active ? "primary" : "default", variant: active ? "filled" : "outlined", clickable: true, disabled: presets.busy, onClick: () => presets.apply(preset.id), onDelete: allowManage ? (event) => setMenu({ anchor: event.currentTarget, preset }) : undefined, deleteIcon: allowManage ? _jsx(MoreVertIcon, { "aria-label": messages.manage(preset.name) }) : undefined, sx: { fontWeight: active ? 700 : 500, maxWidth: 240 } }, preset.id));
|
|
67
|
+
}), allowSave && (saveVariant === "button" ? (_jsx(Button, { size: size, startIcon: _jsx(SaveIcon, {}), disabled: presets.busy || presets.loading, onClick: () => setSaveOpen(true), children: messages.save })) : (_jsx(Tooltip, { title: messages.save, children: _jsx("span", { children: _jsx(IconButton, { size: "small", "aria-label": messages.save, disabled: presets.busy || presets.loading, onClick: () => setSaveOpen(true), children: _jsx(AddIcon, { fontSize: "small" }) }) }) }))), actions, _jsx(Menu, { anchorEl: menu?.anchor ?? null, open: menu !== null, onClose: closeMenu, children: menu && [
|
|
68
|
+
_jsxs(MenuItem, { onClick: () => runMenu(() => presets.apply(menu.preset.id)), children: [_jsx(ListItemIcon, { children: _jsx(CheckIcon, { fontSize: "small" }) }), _jsx(ListItemText, { children: messages.apply })] }, "apply"),
|
|
69
|
+
_jsxs(MenuItem, { disabled: !modifiable(menu.preset), onClick: () => runMenu(() => presets.overwrite(menu.preset.id)), children: [_jsx(ListItemIcon, { children: _jsx(SaveIcon, { fontSize: "small" }) }), _jsx(ListItemText, { children: messages.overwrite })] }, "overwrite"),
|
|
70
|
+
_jsxs(MenuItem, { disabled: !modifiable(menu.preset), onClick: () => { const target = menu.preset; closeMenu(); setRenameTarget(target); }, children: [_jsx(ListItemIcon, { children: _jsx(EditIcon, { fontSize: "small" }) }), _jsx(ListItemText, { children: messages.rename })] }, "rename"),
|
|
71
|
+
_jsxs(MenuItem, { onClick: () => runMenu(() => presets.setDefault(menu.preset.isDefault ? null : menu.preset.id)), children: [_jsx(ListItemIcon, { children: menu.preset.isDefault ? _jsx(StarIcon, { fontSize: "small" }) : _jsx(StarBorderIcon, { fontSize: "small" }) }), _jsx(ListItemText, { children: menu.preset.isDefault ? messages.clearDefault : messages.makeDefault })] }, "default"),
|
|
72
|
+
_jsxs(MenuItem, { disabled: !modifiable(menu.preset), onClick: () => { const target = menu.preset; closeMenu(); setRemoveTarget(target); }, children: [_jsx(ListItemIcon, { children: _jsx(DeleteIcon, { fontSize: "small", color: "error" }) }), _jsx(ListItemText, { sx: { color: "error.main" }, children: messages.remove })] }, "remove"),
|
|
73
|
+
] }), _jsx(SavePresetDialog, { open: saveOpen, scopes: scopes, messages: messages, onClose: () => setSaveOpen(false), onSave: (name, options) => presets.save(name, options) }), _jsx(NameDialog, { open: renameTarget !== null, title: messages.renameTitle, label: messages.name, initial: renameTarget?.name ?? "", messages: messages, onClose: () => setRenameTarget(null), onSubmit: (name) => (renameTarget ? presets.update(renameTarget.id, { name }) : Promise.resolve()) }), _jsxs(Dialog, { open: removeTarget !== null, onClose: () => setRemoveTarget(null), maxWidth: "xs", fullWidth: true, children: [_jsx(DialogTitle, { children: messages.removeTitle }), _jsx(DialogContent, { children: _jsx(Typography, { variant: "body2", children: removeTarget ? messages.removeConfirm(removeTarget.name) : "" }) }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: () => setRemoveTarget(null), children: messages.cancel }), _jsx(Button, { color: "error", variant: "contained", onClick: () => {
|
|
74
|
+
const target = removeTarget;
|
|
75
|
+
setRemoveTarget(null);
|
|
76
|
+
if (target)
|
|
77
|
+
void presets.remove(target.id).catch(() => { });
|
|
78
|
+
}, children: messages.remove })] })] })] }));
|
|
79
|
+
}
|
|
80
|
+
function SavePresetDialog({ open, scopes, messages, onClose, onSave }) {
|
|
81
|
+
const [name, setName] = useState("");
|
|
82
|
+
const [scope, setScope] = useState(scopes[0] ?? "user");
|
|
83
|
+
const [isDefault, setIsDefault] = useState(false);
|
|
84
|
+
const [saving, setSaving] = useState(false);
|
|
85
|
+
const reset = () => {
|
|
86
|
+
setName("");
|
|
87
|
+
setScope(scopes[0] ?? "user");
|
|
88
|
+
setIsDefault(false);
|
|
89
|
+
};
|
|
90
|
+
const close = () => {
|
|
91
|
+
if (saving)
|
|
92
|
+
return;
|
|
93
|
+
reset();
|
|
94
|
+
onClose();
|
|
95
|
+
};
|
|
96
|
+
const submit = async () => {
|
|
97
|
+
const trimmed = name.trim();
|
|
98
|
+
if (!trimmed)
|
|
99
|
+
return;
|
|
100
|
+
setSaving(true);
|
|
101
|
+
try {
|
|
102
|
+
await onSave(trimmed, { scope, isDefault });
|
|
103
|
+
reset();
|
|
104
|
+
onClose();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// 오류 표시는 presets.error 가 맡는다 — 다이얼로그는 열어 둔다
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
setSaving(false);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
return (_jsxs(Dialog, { open: open, onClose: close, maxWidth: "xs", fullWidth: true, children: [_jsx(DialogTitle, { children: messages.saveTitle }), _jsx(DialogContent, { children: _jsxs(Stack, { spacing: 2, sx: { pt: 1 }, children: [_jsx(TextField, { autoFocus: true, fullWidth: true, size: "small", label: messages.name, value: name, required: true, slotProps: { htmlInput: { maxLength: 50, "aria-label": messages.name } }, onChange: (e) => setName(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
void submit();
|
|
116
|
+
} } }), scopes.length > 1 && (_jsxs(FormControl, { children: [_jsx(FormLabel, { children: messages.scope }), _jsx(RadioGroup, { row: true, value: scope, onChange: (e) => setScope(e.target.value), children: scopes.map((s) => (_jsx(FormControlLabel, { value: s, control: _jsx(Radio, { size: "small" }), label: s === "shared" ? messages.scopeShared : messages.scopeUser }, s))) })] })), _jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: isDefault, onChange: (e) => setIsDefault(e.target.checked) }), label: messages.makeDefault })] }) }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: close, disabled: saving, children: messages.cancel }), _jsx(Button, { variant: "contained", onClick: () => void submit(), disabled: saving || !name.trim(), children: messages.confirm })] })] }));
|
|
117
|
+
}
|
|
118
|
+
function NameDialog({ open, title, label, initial, messages, onClose, onSubmit }) {
|
|
119
|
+
// 열릴 때마다 초기값을 다시 넣는다 — key 로 상태를 갈아끼운다
|
|
120
|
+
return (_jsx(Dialog, { open: open, onClose: onClose, maxWidth: "xs", fullWidth: true, children: open && (_jsx(NameForm, { title: title, label: label, initial: initial, messages: messages, onClose: onClose, onSubmit: onSubmit }, initial)) }));
|
|
121
|
+
}
|
|
122
|
+
function NameForm({ title, label, initial, messages, onClose, onSubmit }) {
|
|
123
|
+
const [name, setName] = useState(initial);
|
|
124
|
+
const [saving, setSaving] = useState(false);
|
|
125
|
+
const submit = async () => {
|
|
126
|
+
const trimmed = name.trim();
|
|
127
|
+
if (!trimmed)
|
|
128
|
+
return;
|
|
129
|
+
setSaving(true);
|
|
130
|
+
try {
|
|
131
|
+
await onSubmit(trimmed);
|
|
132
|
+
onClose();
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// presets.error
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
setSaving(false);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
return (_jsxs(_Fragment, { children: [_jsx(DialogTitle, { children: title }), _jsx(DialogContent, { children: _jsx(Box, { sx: { pt: 1 }, children: _jsx(TextField, { autoFocus: true, fullWidth: true, size: "small", label: label, value: name, slotProps: { htmlInput: { maxLength: 50, "aria-label": label } }, onChange: (e) => setName(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
void submit();
|
|
144
|
+
} } }) }) }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: onClose, disabled: saving, children: messages.cancel }), _jsx(Button, { variant: "contained", onClick: () => void submit(), disabled: saving || !name.trim(), children: messages.confirm })] })] }));
|
|
145
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { UrlSync } from "../../../core/src/index";
|
|
3
|
+
export interface ShareAction {
|
|
4
|
+
key: string;
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
icon?: ReactNode;
|
|
7
|
+
/** 현재 공유 URL 을 받는다 — 알림 공유 API 등 소비 앱 몫 */
|
|
8
|
+
onClick: (shareUrl: string) => void | Promise<void>;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ShareDropdownMessages {
|
|
12
|
+
copied: string;
|
|
13
|
+
copyFailedTitle: string;
|
|
14
|
+
copyFailedHint: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ShareDropdownProps {
|
|
17
|
+
/** `useUrlSync(search)` 결과 — `shareUrl` 과 `copyShareUrl` 만 쓴다 */
|
|
18
|
+
urlSync: Pick<UrlSync, "shareUrl" | "copyShareUrl">;
|
|
19
|
+
/** "알림으로 공유" 같은 앱 액션. URL 복사 아래에 순서대로 놓인다 */
|
|
20
|
+
extraActions?: ShareAction[];
|
|
21
|
+
size?: "small" | "medium" | "large";
|
|
22
|
+
tooltip?: string;
|
|
23
|
+
copyLabel?: ReactNode;
|
|
24
|
+
messages?: Partial<ShareDropdownMessages>;
|
|
25
|
+
/** 복사 성공/실패 뒤 호출 — 호출부가 자체 토스트를 쓰면 `showSnackbar=false` 와 함께 */
|
|
26
|
+
onCopy?: (ok: boolean, shareUrl: string) => void;
|
|
27
|
+
showSnackbar?: boolean;
|
|
28
|
+
/** 아이콘 대신 텍스트 버튼으로 그린다 */
|
|
29
|
+
variant?: "icon" | "button";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 공유 드롭다운 — 기존 ERP `ShareDropdown` 대응.
|
|
33
|
+
* URL 복사는 `copyShareUrl()` 로, 실패하면 URL 을 보여 주는 다이얼로그로 폴백한다.
|
|
34
|
+
*/
|
|
35
|
+
export declare function ShareDropdown({ urlSync, extraActions, size, tooltip, copyLabel, messages: givenMessages, onCopy, showSnackbar, variant, }: ShareDropdownProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Snackbar, TextField, Tooltip, Typography, } from "@mui/material";
|
|
4
|
+
import LinkIcon from "@mui/icons-material/Link";
|
|
5
|
+
import ShareIcon from "@mui/icons-material/Share";
|
|
6
|
+
const DEFAULT_MESSAGES = {
|
|
7
|
+
copied: "URL 을 복사했습니다",
|
|
8
|
+
copyFailedTitle: "URL 복사 실패",
|
|
9
|
+
copyFailedHint: "클립보드 권한이 없습니다. 아래 URL 을 직접 복사하세요.",
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* 공유 드롭다운 — 기존 ERP `ShareDropdown` 대응.
|
|
13
|
+
* URL 복사는 `copyShareUrl()` 로, 실패하면 URL 을 보여 주는 다이얼로그로 폴백한다.
|
|
14
|
+
*/
|
|
15
|
+
export function ShareDropdown({ urlSync, extraActions = [], size = "small", tooltip = "공유", copyLabel = "URL 복사", messages: givenMessages, onCopy, showSnackbar = true, variant = "icon", }) {
|
|
16
|
+
const messages = { ...DEFAULT_MESSAGES, ...givenMessages };
|
|
17
|
+
const [anchorEl, setAnchorEl] = useState(null);
|
|
18
|
+
const [toast, setToast] = useState(null);
|
|
19
|
+
const [fallbackUrl, setFallbackUrl] = useState(null);
|
|
20
|
+
const close = () => setAnchorEl(null);
|
|
21
|
+
const copy = async () => {
|
|
22
|
+
close();
|
|
23
|
+
const url = urlSync.shareUrl || (typeof window !== "undefined" ? window.location.href : "");
|
|
24
|
+
const ok = await urlSync.copyShareUrl();
|
|
25
|
+
onCopy?.(ok, url);
|
|
26
|
+
if (ok) {
|
|
27
|
+
if (showSnackbar)
|
|
28
|
+
setToast(messages.copied);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
setFallbackUrl(url);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const run = async (action) => {
|
|
35
|
+
close();
|
|
36
|
+
const url = urlSync.shareUrl || (typeof window !== "undefined" ? window.location.href : "");
|
|
37
|
+
await action.onClick(url);
|
|
38
|
+
};
|
|
39
|
+
const trigger = variant === "button" ? (_jsx(Button, { size: size === "large" ? "medium" : size, variant: "outlined", color: "info", startIcon: _jsx(ShareIcon, {}), onClick: (event) => setAnchorEl(event.currentTarget), "aria-label": tooltip, "aria-haspopup": "menu", "aria-expanded": !!anchorEl, children: tooltip })) : (_jsx(IconButton, { size: size, onClick: (event) => setAnchorEl(event.currentTarget), "aria-label": tooltip, "aria-haspopup": "menu", "aria-expanded": !!anchorEl, sx: { border: 1, borderColor: "info.main", color: "info.main" }, children: _jsx(ShareIcon, { fontSize: "small" }) }));
|
|
40
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tooltip, { title: tooltip, children: trigger }), _jsxs(Menu, { anchorEl: anchorEl, open: !!anchorEl, onClose: close, anchorOrigin: { vertical: "bottom", horizontal: "right" }, transformOrigin: { vertical: "top", horizontal: "right" }, children: [_jsxs(MenuItem, { onClick: () => { void copy(); }, children: [_jsx(ListItemIcon, { children: _jsx(LinkIcon, { fontSize: "small" }) }), _jsx(ListItemText, { children: copyLabel })] }), extraActions.map((action) => (_jsxs(MenuItem, { disabled: action.disabled, onClick: () => { void run(action); }, children: [action.icon && _jsx(ListItemIcon, { children: action.icon }), _jsx(ListItemText, { children: action.label })] }, action.key)))] }), _jsx(Snackbar, { open: !!toast, autoHideDuration: 2500, onClose: () => setToast(null), message: toast }), _jsxs(Dialog, { open: fallbackUrl !== null, onClose: () => setFallbackUrl(null), maxWidth: "sm", fullWidth: true, children: [_jsx(DialogTitle, { children: messages.copyFailedTitle }), _jsxs(DialogContent, { children: [_jsx(Typography, { variant: "body2", sx: { mb: 1.5 }, children: messages.copyFailedHint }), _jsx(TextField, { value: fallbackUrl ?? "", fullWidth: true, size: "small", multiline: true, maxRows: 4, slotProps: { input: { readOnly: true }, htmlInput: { "aria-label": "공유 URL" } }, onFocus: (event) => event.target.select() })] }), _jsx(DialogActions, { children: _jsx(Button, { onClick: () => setFallbackUrl(null), children: "\uB2EB\uAE30" }) })] })] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { RowData } from "@tanstack/react-table";
|
|
2
|
+
import { type SearchState, type TableSearch } from "../../../core/src/index";
|
|
3
|
+
export interface SearchStoreSnapshot {
|
|
4
|
+
params: SearchState;
|
|
5
|
+
draft: SearchState;
|
|
6
|
+
isDirty: boolean;
|
|
7
|
+
/** 브리지가 붙어 있는가 — false 면 아직 표 화면이 마운트되지 않았다 */
|
|
8
|
+
bound: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** 브리지가 스토어에 연결하는 search 메서드 — `SearchState` 만 오간다 */
|
|
11
|
+
export interface SearchStoreActions {
|
|
12
|
+
patchParams: (partial: Partial<SearchState>) => void;
|
|
13
|
+
patchDraft: (partial: Partial<SearchState>) => void;
|
|
14
|
+
setParams: (params: SearchState) => void;
|
|
15
|
+
setDraft: (draft: SearchState) => void;
|
|
16
|
+
apply: () => void;
|
|
17
|
+
reset: () => void;
|
|
18
|
+
}
|
|
19
|
+
export interface SearchStore extends SearchStoreActions {
|
|
20
|
+
subscribe: (listener: () => void) => () => void;
|
|
21
|
+
getSnapshot: () => SearchStoreSnapshot;
|
|
22
|
+
/** `getSnapshot` 의 별칭 */
|
|
23
|
+
getState: () => SearchStoreSnapshot;
|
|
24
|
+
/** @internal 브리지 전용 */
|
|
25
|
+
_bind: (actions: SearchStoreActions | null) => void;
|
|
26
|
+
/** @internal 브리지 전용 */
|
|
27
|
+
_publish: (next: Pick<SearchStoreSnapshot, "params" | "draft" | "isDirty">) => void;
|
|
28
|
+
}
|
|
29
|
+
export interface CreateSearchStoreOptions {
|
|
30
|
+
/** 브리지가 붙기 전 조작을 받았을 때 (기본 무시). 경고 로그 등에 쓴다 */
|
|
31
|
+
onUnboundAction?: (action: keyof SearchStoreActions) => void;
|
|
32
|
+
}
|
|
33
|
+
/** 스토어 생성 — 모듈 스코프나 `useMemo` 안에서 만든다 */
|
|
34
|
+
export declare function createSearchStore(options?: CreateSearchStoreOptions): SearchStore;
|
|
35
|
+
/**
|
|
36
|
+
* 표 화면 안에서 `search` 를 스토어에 연결한다.
|
|
37
|
+
* 언마운트하면 연결을 끊는다 — 스냅샷은 마지막 값으로 남아 있고 `bound` 만 false 가 된다.
|
|
38
|
+
*/
|
|
39
|
+
export declare function useSearchStoreBridge<T extends RowData>(store: SearchStore, search: TableSearch<T>): void;
|
|
40
|
+
/**
|
|
41
|
+
* 스토어 구독 — `selector` 결과가 `equalityFn`(기본 `Object.is`) 으로 같으면 리렌더하지 않는다.
|
|
42
|
+
* 배열·객체를 새로 만드는 selector 는 `equalityFn` 을 주거나 원시값으로 좁힌다.
|
|
43
|
+
*/
|
|
44
|
+
export declare function useSearchStore<S = SearchStoreSnapshot>(store: SearchStore, selector?: (snapshot: SearchStoreSnapshot) => S, equalityFn?: (a: S, b: S) => boolean): S;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 검색 상태 스토어 — 기존 ERP `createTableFilterStore`(zustand) 의 무의존 대체.
|
|
3
|
+
*
|
|
4
|
+
* 표 화면의 브리지(`useSearchStoreBridge`) 가 `search.params/draft/isDirty` 를 밀어넣고,
|
|
5
|
+
* 표 트리 밖의 컴포넌트가 `useSearchStore(store, selector)` 로 구독한다.
|
|
6
|
+
* 원격 조작(`patchParams` 등) 은 브리지가 연결한 search 메서드로 되돌아간다.
|
|
7
|
+
* 타입은 `SearchState` 만 쓰므로 행 타입 `T` 가 없다 — 스토어를 모듈 스코프에 두어도 된다.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
10
|
+
import { DEFAULT_PAGINATION } from "../../../core/src/index";
|
|
11
|
+
function emptyState() {
|
|
12
|
+
return {
|
|
13
|
+
columnFilters: [],
|
|
14
|
+
columnFilterFns: {},
|
|
15
|
+
globalFilter: "",
|
|
16
|
+
pagination: { ...DEFAULT_PAGINATION },
|
|
17
|
+
sorting: [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** 스토어 생성 — 모듈 스코프나 `useMemo` 안에서 만든다 */
|
|
21
|
+
export function createSearchStore(options = {}) {
|
|
22
|
+
const initial = emptyState();
|
|
23
|
+
let snapshot = { params: initial, draft: initial, isDirty: false, bound: false };
|
|
24
|
+
let actions = null;
|
|
25
|
+
const listeners = new Set();
|
|
26
|
+
const emit = () => { for (const listener of listeners)
|
|
27
|
+
listener(); };
|
|
28
|
+
const call = (name, ...args) => {
|
|
29
|
+
if (!actions) {
|
|
30
|
+
options.onUnboundAction?.(name);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
actions[name](...args);
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
subscribe: (listener) => {
|
|
37
|
+
listeners.add(listener);
|
|
38
|
+
return () => { listeners.delete(listener); };
|
|
39
|
+
},
|
|
40
|
+
getSnapshot: () => snapshot,
|
|
41
|
+
getState: () => snapshot,
|
|
42
|
+
patchParams: (partial) => call("patchParams", partial),
|
|
43
|
+
patchDraft: (partial) => call("patchDraft", partial),
|
|
44
|
+
setParams: (params) => call("setParams", params),
|
|
45
|
+
setDraft: (draft) => call("setDraft", draft),
|
|
46
|
+
apply: () => call("apply"),
|
|
47
|
+
reset: () => call("reset"),
|
|
48
|
+
_bind: (next) => {
|
|
49
|
+
actions = next;
|
|
50
|
+
const bound = next !== null;
|
|
51
|
+
if (snapshot.bound !== bound) {
|
|
52
|
+
snapshot = { ...snapshot, bound };
|
|
53
|
+
emit();
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
_publish: (next) => {
|
|
57
|
+
if (snapshot.params === next.params && snapshot.draft === next.draft && snapshot.isDirty === next.isDirty)
|
|
58
|
+
return;
|
|
59
|
+
snapshot = { ...snapshot, params: next.params, draft: next.draft, isDirty: next.isDirty };
|
|
60
|
+
emit();
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 표 화면 안에서 `search` 를 스토어에 연결한다.
|
|
66
|
+
* 언마운트하면 연결을 끊는다 — 스냅샷은 마지막 값으로 남아 있고 `bound` 만 false 가 된다.
|
|
67
|
+
*/
|
|
68
|
+
export function useSearchStoreBridge(store, search) {
|
|
69
|
+
const searchRef = useRef(search);
|
|
70
|
+
searchRef.current = search;
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
// ref 를 거쳐 부르므로 search 객체가 매 렌더 바뀌어도 다시 바인딩하지 않는다
|
|
73
|
+
store._bind({
|
|
74
|
+
patchParams: (partial) => searchRef.current.patchParams(partial),
|
|
75
|
+
patchDraft: (partial) => searchRef.current.patchDraft(partial),
|
|
76
|
+
setParams: (params) => searchRef.current.setParams(params),
|
|
77
|
+
setDraft: (draft) => searchRef.current.setDraft(draft),
|
|
78
|
+
apply: () => searchRef.current.apply(),
|
|
79
|
+
reset: () => searchRef.current.reset(),
|
|
80
|
+
});
|
|
81
|
+
return () => store._bind(null);
|
|
82
|
+
}, [store]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
store._publish({ params: search.params, draft: search.draft, isDirty: search.isDirty });
|
|
85
|
+
}, [store, search.params, search.draft, search.isDirty]);
|
|
86
|
+
}
|
|
87
|
+
const identity = (snapshot) => snapshot;
|
|
88
|
+
/**
|
|
89
|
+
* 스토어 구독 — `selector` 결과가 `equalityFn`(기본 `Object.is`) 으로 같으면 리렌더하지 않는다.
|
|
90
|
+
* 배열·객체를 새로 만드는 selector 는 `equalityFn` 을 주거나 원시값으로 좁힌다.
|
|
91
|
+
*/
|
|
92
|
+
export function useSearchStore(store, selector = (identity), equalityFn = Object.is) {
|
|
93
|
+
const cache = useRef(null);
|
|
94
|
+
const selectorRef = useRef(selector);
|
|
95
|
+
selectorRef.current = selector;
|
|
96
|
+
const equalityRef = useRef(equalityFn);
|
|
97
|
+
equalityRef.current = equalityFn;
|
|
98
|
+
const getSelected = () => {
|
|
99
|
+
const snapshot = store.getSnapshot();
|
|
100
|
+
const previous = cache.current;
|
|
101
|
+
if (previous && previous.snapshot === snapshot)
|
|
102
|
+
return previous.selected;
|
|
103
|
+
const selected = selectorRef.current(snapshot);
|
|
104
|
+
if (previous && equalityRef.current(previous.selected, selected)) {
|
|
105
|
+
cache.current = { snapshot, selected: previous.selected };
|
|
106
|
+
return previous.selected;
|
|
107
|
+
}
|
|
108
|
+
cache.current = { snapshot, selected };
|
|
109
|
+
return selected;
|
|
110
|
+
};
|
|
111
|
+
return useSyncExternalStore(store.subscribe, getSelected, getSelected);
|
|
112
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 검색 상태 요약 — `FilterStatusContent` 가 그리는 표의 **순수 데이터**.
|
|
3
|
+
*
|
|
4
|
+
* 기존 ERP `FilterStatusContent.formatFilterValue` 의 값 표현 규칙을 따른다
|
|
5
|
+
* (빈 값 "(없음)", 날짜는 `YYYY-MM-DD`, 배열은 쉼표, 관계 필터는 컬럼별 값 묶음 또는
|
|
6
|
+
* "N개 그룹"). 값 문구 자체는 core 로 옮겼다 — 컬럼에 필터 kind 가 있으면
|
|
7
|
+
* `kind.operators[op].describe`, 없으면 `describeFilterValue` 의 일반 규칙(값 모양 추정).
|
|
8
|
+
* JSX 가 없으므로 모달 밖(툴팁·로그) 에서도 쓴다.
|
|
9
|
+
*/
|
|
10
|
+
import type { RowData } from "@tanstack/react-table";
|
|
11
|
+
import { type AnyFilterKind, type ColumnDescription, type FilterDescribeLabels, type FilterOperator, type SearchState, type TableColumnDefs } from "../../../core/src/index";
|
|
12
|
+
export declare const EMPTY_VALUE_TEXT: string;
|
|
13
|
+
/** 값을 쓰지 않는 연산자의 값 칸 */
|
|
14
|
+
export declare const VALUELESS_TEXT: string;
|
|
15
|
+
export type ConditionStatus =
|
|
16
|
+
/** params 와 draft 가 같다 */
|
|
17
|
+
"applied"
|
|
18
|
+
/** 적용됐지만 draft 에서 값·연산자가 바뀌었다 */
|
|
19
|
+
| "changed"
|
|
20
|
+
/** draft 에만 있다 (아직 적용 안 됨) */
|
|
21
|
+
| "added"
|
|
22
|
+
/** params 에는 있는데 draft 에서 지워졌다 */
|
|
23
|
+
| "removed";
|
|
24
|
+
export interface ConditionSummary {
|
|
25
|
+
id: string;
|
|
26
|
+
header: string;
|
|
27
|
+
/** 적용된 연산자 (draft 에만 있는 항목은 draft 연산자) */
|
|
28
|
+
operator: FilterOperator;
|
|
29
|
+
operatorLabel: string;
|
|
30
|
+
/** 적용된 값의 표시 문자열 */
|
|
31
|
+
value: string;
|
|
32
|
+
status: ConditionStatus;
|
|
33
|
+
/** status 가 applied 가 아닐 때 draft 쪽 연산자·값 */
|
|
34
|
+
draftOperator?: FilterOperator;
|
|
35
|
+
draftOperatorLabel?: string;
|
|
36
|
+
draftValue?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface SortSummary {
|
|
39
|
+
id: string;
|
|
40
|
+
header: string;
|
|
41
|
+
desc: boolean;
|
|
42
|
+
/** "거래처 오름차순" */
|
|
43
|
+
label: string;
|
|
44
|
+
}
|
|
45
|
+
export interface SearchSummary {
|
|
46
|
+
conditions: ConditionSummary[];
|
|
47
|
+
globalFilter: {
|
|
48
|
+
value: string;
|
|
49
|
+
draftValue: string;
|
|
50
|
+
pending: boolean;
|
|
51
|
+
};
|
|
52
|
+
sorting: SortSummary[];
|
|
53
|
+
/** 그룹핑 컬럼 헤더 */
|
|
54
|
+
grouping: string[];
|
|
55
|
+
pagination: {
|
|
56
|
+
pageIndex: number;
|
|
57
|
+
pageSize: number;
|
|
58
|
+
};
|
|
59
|
+
/** 적용 중인(값이 있는) 조건 수 — 전역검색 포함 */
|
|
60
|
+
appliedCount: number;
|
|
61
|
+
/** draft 와 다른 항목 수 (전역검색 포함) */
|
|
62
|
+
pendingCount: number;
|
|
63
|
+
}
|
|
64
|
+
export interface FormatConditionValueOptions {
|
|
65
|
+
operator?: FilterOperator;
|
|
66
|
+
column?: Pick<ColumnDescription, "variant" | "selectOptions" | "relationColumns">;
|
|
67
|
+
/** 컬럼의 필터 kind — 있으면 `operators[op].describe` 가 문구를 정한다 */
|
|
68
|
+
kind?: AnyFilterKind;
|
|
69
|
+
/** 배열 값이 이보다 길면 앞부분만 보이고 "외 N개" (기본 5) */
|
|
70
|
+
maxListItems?: number;
|
|
71
|
+
/** 문구 묶음 (기본 한국어 `DEFAULT_DESCRIBE_LABELS`) */
|
|
72
|
+
labels?: FilterDescribeLabels;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 필터 값 하나를 사람이 읽는 문자열로.
|
|
76
|
+
*
|
|
77
|
+
* kind 가 있으면 kind 의 `describe`(값 없는 연산자·빈 값은 공통 처리), 없으면 값 모양으로
|
|
78
|
+
* 추정하는 core `describeFilterValue` 의 일반 규칙:
|
|
79
|
+
* - 값 없는 연산자(`empty`/`hasChild` …) → `-`
|
|
80
|
+
* - 선택지가 있으면 값 대신 라벨
|
|
81
|
+
* - 두 원소 범위(숫자·날짜) → `a ~ b`, 그 외 배열은 쉼표
|
|
82
|
+
* - 관계(`groups`)/선형(`terms`)/일정(`date`|`start`) 은 요약 문구
|
|
83
|
+
*/
|
|
84
|
+
export declare function formatConditionValue(value: unknown, options?: FormatConditionValueOptions): string;
|
|
85
|
+
/** 조건이 실제로 걸려 있는가 — 값이 있거나 값 없는 연산자다 */
|
|
86
|
+
export declare function isActiveCondition(value: unknown, operator?: FilterOperator): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* 적용 중인 조건 수 — 값이 있는 컬럼 필터 + 전역검색.
|
|
89
|
+
* 트리거 버튼 배지와 접힌 패널 요약이 같은 수를 보여야 하므로 한 곳에 둔다.
|
|
90
|
+
*/
|
|
91
|
+
export declare function countActiveConditions(state: Pick<SearchState, "columnFilters" | "globalFilter" | "columnFilterFns">, operators?: Readonly<Record<string, FilterOperator>>): number;
|
|
92
|
+
export interface SummarizeSearchOptions<T extends RowData> {
|
|
93
|
+
columns: TableColumnDefs<T>;
|
|
94
|
+
params: SearchState;
|
|
95
|
+
/** 주면 params 와 비교해 미적용 항목을 표시한다 */
|
|
96
|
+
draft?: SearchState;
|
|
97
|
+
/** 컬럼 기본값이 덧대진 연산자 맵 (`search.operators`). 없으면 `params.columnFilterFns` */
|
|
98
|
+
operators?: Readonly<Record<string, FilterOperator>>;
|
|
99
|
+
draftOperators?: Readonly<Record<string, FilterOperator>>;
|
|
100
|
+
/** 값이 비어 있는 항목도 표에 남긴다 (기본 false) */
|
|
101
|
+
includeInactive?: boolean;
|
|
102
|
+
/** 값 문구 묶음 — Provider 라벨을 넘길 때 (기본 한국어) */
|
|
103
|
+
labels?: FilterDescribeLabels;
|
|
104
|
+
}
|
|
105
|
+
/** `search.params`(+`draft`) 를 표 데이터로 요약한다 */
|
|
106
|
+
export declare function summarizeSearch<T extends RowData>(options: SummarizeSearchOptions<T>): SearchSummary;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { DEFAULT_DESCRIBE_LABELS, describeColumns, describeFilter, filterKindMap, isUnsetFilterValue, isValuelessOperator, } from "../../../core/src/index";
|
|
2
|
+
import { operatorLabel } from "../filter/inputs/OperatorMenu";
|
|
3
|
+
export const EMPTY_VALUE_TEXT = DEFAULT_DESCRIBE_LABELS.empty;
|
|
4
|
+
/** 값을 쓰지 않는 연산자의 값 칸 */
|
|
5
|
+
export const VALUELESS_TEXT = DEFAULT_DESCRIBE_LABELS.valueless;
|
|
6
|
+
/**
|
|
7
|
+
* 필터 값 하나를 사람이 읽는 문자열로.
|
|
8
|
+
*
|
|
9
|
+
* kind 가 있으면 kind 의 `describe`(값 없는 연산자·빈 값은 공통 처리), 없으면 값 모양으로
|
|
10
|
+
* 추정하는 core `describeFilterValue` 의 일반 규칙:
|
|
11
|
+
* - 값 없는 연산자(`empty`/`hasChild` …) → `-`
|
|
12
|
+
* - 선택지가 있으면 값 대신 라벨
|
|
13
|
+
* - 두 원소 범위(숫자·날짜) → `a ~ b`, 그 외 배열은 쉼표
|
|
14
|
+
* - 관계(`groups`)/선형(`terms`)/일정(`date`|`start`) 은 요약 문구
|
|
15
|
+
*/
|
|
16
|
+
export function formatConditionValue(value, options = {}) {
|
|
17
|
+
const { operator, column, kind, maxListItems = 5, labels } = options;
|
|
18
|
+
return describeFilter(kind, operator, value, {
|
|
19
|
+
operator,
|
|
20
|
+
variant: column?.variant,
|
|
21
|
+
options: column?.selectOptions,
|
|
22
|
+
relationColumns: column?.relationColumns,
|
|
23
|
+
maxListItems,
|
|
24
|
+
labels,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** 조건이 실제로 걸려 있는가 — 값이 있거나 값 없는 연산자다 */
|
|
28
|
+
export function isActiveCondition(value, operator) {
|
|
29
|
+
if (operator && isValuelessOperator(operator))
|
|
30
|
+
return true;
|
|
31
|
+
return !isUnsetFilterValue(value);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 적용 중인 조건 수 — 값이 있는 컬럼 필터 + 전역검색.
|
|
35
|
+
* 트리거 버튼 배지와 접힌 패널 요약이 같은 수를 보여야 하므로 한 곳에 둔다.
|
|
36
|
+
*/
|
|
37
|
+
export function countActiveConditions(state, operators) {
|
|
38
|
+
const ops = operators ?? state.columnFilterFns;
|
|
39
|
+
const filters = state.columnFilters.filter((f) => f && isActiveCondition(f.value, ops[f.id])).length;
|
|
40
|
+
return filters + (state.globalFilter ? 1 : 0);
|
|
41
|
+
}
|
|
42
|
+
const sameJson = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
43
|
+
/** `search.params`(+`draft`) 를 표 데이터로 요약한다 */
|
|
44
|
+
export function summarizeSearch(options) {
|
|
45
|
+
const { columns, params, draft, includeInactive = false, labels } = options;
|
|
46
|
+
const operators = options.operators ?? params.columnFilterFns;
|
|
47
|
+
const draftOperators = options.draftOperators ?? draft?.columnFilterFns ?? operators;
|
|
48
|
+
const described = new Map(describeColumns(columns).map((c) => [c.id, c]));
|
|
49
|
+
const kinds = filterKindMap(columns);
|
|
50
|
+
const headerOf = (id) => described.get(id)?.header ?? id;
|
|
51
|
+
const operatorOf = (id, map) => map[id] ?? described.get(id)?.defaultOperator ?? "fuzzy";
|
|
52
|
+
const appliedById = new Map(params.columnFilters.filter(Boolean).map((f) => [f.id, f]));
|
|
53
|
+
const draftById = new Map((draft?.columnFilters ?? []).filter(Boolean).map((f) => [f.id, f]));
|
|
54
|
+
const ids = Array.from(new Set([...appliedById.keys(), ...draftById.keys()]));
|
|
55
|
+
const conditions = [];
|
|
56
|
+
for (const id of ids) {
|
|
57
|
+
const column = described.get(id);
|
|
58
|
+
const applied = appliedById.get(id);
|
|
59
|
+
const inDraft = draftById.get(id);
|
|
60
|
+
const appliedOperator = operatorOf(id, operators);
|
|
61
|
+
const draftOperator = operatorOf(id, draftOperators);
|
|
62
|
+
const appliedActive = applied ? isActiveCondition(applied.value, appliedOperator) : false;
|
|
63
|
+
const draftActive = draft ? (inDraft ? isActiveCondition(inDraft.value, draftOperator) : false) : appliedActive;
|
|
64
|
+
if (!appliedActive && !draftActive && !includeInactive)
|
|
65
|
+
continue;
|
|
66
|
+
let status = "applied";
|
|
67
|
+
if (draft) {
|
|
68
|
+
if (appliedActive && !draftActive)
|
|
69
|
+
status = "removed";
|
|
70
|
+
else if (!appliedActive && draftActive)
|
|
71
|
+
status = "added";
|
|
72
|
+
else if (!sameJson(applied?.value, inDraft?.value) || appliedOperator !== draftOperator)
|
|
73
|
+
status = "changed";
|
|
74
|
+
}
|
|
75
|
+
const format = (value, operator) => formatConditionValue(value, { operator, column, kind: kinds.get(id), labels });
|
|
76
|
+
const summary = {
|
|
77
|
+
id,
|
|
78
|
+
header: headerOf(id),
|
|
79
|
+
operator: status === "added" ? draftOperator : appliedOperator,
|
|
80
|
+
operatorLabel: operatorLabel(status === "added" ? draftOperator : appliedOperator, labels?.operators),
|
|
81
|
+
value: status === "added" ? EMPTY_VALUE_TEXT : format(applied?.value, appliedOperator),
|
|
82
|
+
status,
|
|
83
|
+
};
|
|
84
|
+
if (status !== "applied") {
|
|
85
|
+
summary.draftOperator = draftOperator;
|
|
86
|
+
summary.draftOperatorLabel = operatorLabel(draftOperator, labels?.operators);
|
|
87
|
+
summary.draftValue = status === "removed" ? EMPTY_VALUE_TEXT : format(inDraft?.value, draftOperator);
|
|
88
|
+
}
|
|
89
|
+
conditions.push(summary);
|
|
90
|
+
}
|
|
91
|
+
const globalValue = params.globalFilter ?? "";
|
|
92
|
+
const globalDraft = draft ? draft.globalFilter ?? "" : globalValue;
|
|
93
|
+
const globalPending = globalDraft !== globalValue;
|
|
94
|
+
const sorting = params.sorting.map((s) => ({
|
|
95
|
+
id: s.id,
|
|
96
|
+
header: headerOf(s.id),
|
|
97
|
+
desc: !!s.desc,
|
|
98
|
+
label: `${headerOf(s.id)} ${s.desc ? "내림차순" : "오름차순"}`,
|
|
99
|
+
}));
|
|
100
|
+
return {
|
|
101
|
+
conditions,
|
|
102
|
+
globalFilter: { value: globalValue, draftValue: globalDraft, pending: globalPending },
|
|
103
|
+
sorting,
|
|
104
|
+
grouping: (params.grouping ?? []).map(headerOf),
|
|
105
|
+
pagination: { pageIndex: params.pagination.pageIndex, pageSize: params.pagination.pageSize },
|
|
106
|
+
appliedCount: countActiveConditions(params, operators),
|
|
107
|
+
pendingCount: conditions.filter((c) => c.status !== "applied").length + (globalPending ? 1 : 0),
|
|
108
|
+
};
|
|
109
|
+
}
|