namirasoft-site-react 1.5.16 → 1.5.18

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.
Files changed (362) hide show
  1. package/.dockerignore +86 -86
  2. package/.env.template +10 -10
  3. package/Dockerfile +18 -18
  4. package/SKILL.md +307 -307
  5. package/config-overrides.js +72 -72
  6. package/dist/App.css +33 -33
  7. package/dist/components/NSBanner.module.css +47 -47
  8. package/dist/components/NSBarAction.module.css +91 -91
  9. package/dist/components/NSBarAlert.module.css +79 -79
  10. package/dist/components/NSBarNotification.js +3 -3
  11. package/dist/components/NSBarNotification.js.map +1 -1
  12. package/dist/components/NSBarNotification.module.css +34 -34
  13. package/dist/components/NSBarTitle.module.css +9 -9
  14. package/dist/components/NSBox.module.css +87 -87
  15. package/dist/components/NSBoxBaseCombo.module.css +6 -6
  16. package/dist/components/NSBoxBoolean.module.css +73 -73
  17. package/dist/components/NSBoxDate.module.css +4 -4
  18. package/dist/components/NSBoxDateRangeBase.module.css +73 -73
  19. package/dist/components/NSBoxDateTime.module.css +4 -4
  20. package/dist/components/NSBoxDuration.module.css +4 -4
  21. package/dist/components/NSBoxFile.module.css +10 -10
  22. package/dist/components/NSBoxInteger.module.css +16 -16
  23. package/dist/components/NSBoxOptions.module.css +53 -53
  24. package/dist/components/NSBoxPhone.module.css +35 -35
  25. package/dist/components/NSBoxRadio.module.css +33 -33
  26. package/dist/components/NSBoxSchemaBase.module.css +17 -17
  27. package/dist/components/NSBoxSchemaVariable.module.css +16 -16
  28. package/dist/components/NSBoxTextArea.module.css +12 -12
  29. package/dist/components/NSBoxTime.module.css +4 -4
  30. package/dist/components/NSButton.module.css +57 -57
  31. package/dist/components/NSButtonBlue.module.css +4 -4
  32. package/dist/components/NSButtonGreen.module.css +4 -4
  33. package/dist/components/NSButtonRed.module.css +4 -4
  34. package/dist/components/NSCard.module.css +113 -113
  35. package/dist/components/NSCardScreenshot.module.css +41 -41
  36. package/dist/components/NSColumn.module.css +16 -16
  37. package/dist/components/NSCopyBox.module.css +39 -39
  38. package/dist/components/NSDialog.module.css +105 -105
  39. package/dist/components/NSDialogPageSelection.module.css +103 -103
  40. package/dist/components/NSDownTimer.module.css +55 -55
  41. package/dist/components/NSDownload.module.css +46 -46
  42. package/dist/components/NSElectronicCard.module.css +60 -60
  43. package/dist/components/NSEntityCardBackground.module.css +27 -27
  44. package/dist/components/NSFilterBox.module.css +27 -27
  45. package/dist/components/NSFilterBoxItems.module.css +15 -15
  46. package/dist/components/NSFilterItem.module.css +24 -24
  47. package/dist/components/NSFooter.module.css +140 -140
  48. package/dist/components/NSGauge.module.css +67 -67
  49. package/dist/components/NSGaugeDate.module.css +57 -57
  50. package/dist/components/NSGaugeNumber.module.css +66 -66
  51. package/dist/components/NSHeader.module.css +316 -316
  52. package/dist/components/NSHeaderScreenshot.module.css +35 -35
  53. package/dist/components/NSID.js +1 -1
  54. package/dist/components/NSID.js.map +1 -1
  55. package/dist/components/NSLabel.module.css +25 -25
  56. package/dist/components/NSLabelErrorNotifier.module.css +2 -2
  57. package/dist/components/NSLabelSensitive.module.css +9 -9
  58. package/dist/components/NSLayout.module.css +22 -22
  59. package/dist/components/NSLine.module.css +15 -15
  60. package/dist/components/NSLineText.module.css +38 -38
  61. package/dist/components/NSLink.module.css +36 -36
  62. package/dist/components/NSListGrouped.module.css +69 -69
  63. package/dist/components/NSLoading.module.css +10 -10
  64. package/dist/components/NSMegaMenu.module.css +133 -133
  65. package/dist/components/NSMenuAction.module.css +88 -88
  66. package/dist/components/NSMenuButton.module.css +44 -44
  67. package/dist/components/NSNoData.module.css +11 -11
  68. package/dist/components/NSPagination.module.css +87 -87
  69. package/dist/components/NSPanel.module.css +28 -28
  70. package/dist/components/NSPanelAccordion.module.css +4 -4
  71. package/dist/components/NSProductSearch.module.css +73 -73
  72. package/dist/components/NSProgressBar.module.css +38 -38
  73. package/dist/components/NSRange.module.css +66 -66
  74. package/dist/components/NSReorderDialog.module.css +49 -49
  75. package/dist/components/NSRow.module.css +17 -17
  76. package/dist/components/NSSection.module.css +4 -4
  77. package/dist/components/NSSectionCards.module.css +38 -38
  78. package/dist/components/NSSectionTiles.module.css +10 -10
  79. package/dist/components/NSSplitter.module.css +59 -59
  80. package/dist/components/NSTabPage.module.css +59 -59
  81. package/dist/components/NSTable.js +23 -23
  82. package/dist/components/NSTable.module.css +372 -372
  83. package/dist/components/NSTableChart.module.css +119 -119
  84. package/dist/components/NSTile.module.css +76 -76
  85. package/dist/components/NSTimelineMonthly.module.css +71 -71
  86. package/dist/components/NSTitle.module.css +15 -15
  87. package/dist/formatter/StringFormatter.d.ts +2 -1
  88. package/dist/formatter/StringFormatter.js +5 -3
  89. package/dist/formatter/StringFormatter.js.map +1 -1
  90. package/dist/index.css +5 -5
  91. package/dist/pages/NSImplementing.module.css +35 -35
  92. package/dist/pages/NSNotFoundPage.module.css +18 -18
  93. package/dist/pages/NSUpdating.module.css +35 -35
  94. package/package.json +86 -86
  95. package/public/index.html +38 -38
  96. package/src/App.css +33 -33
  97. package/src/App.tsx +167 -167
  98. package/src/BrowserDetector.ts +109 -109
  99. package/src/Color.tsx +10 -10
  100. package/src/CopyToClipboard.ts +6 -6
  101. package/src/EncryptionOperation.ts +98 -98
  102. package/src/KeyOperation.ts +109 -109
  103. package/src/LinkOperation.ts +101 -101
  104. package/src/NSBoxBuilder.tsx +922 -922
  105. package/src/ProductCacheService.ts +15 -15
  106. package/src/Validator.ts +199 -199
  107. package/src/components/INSBox.tsx +7 -7
  108. package/src/components/NSBanner.module.css +47 -47
  109. package/src/components/NSBanner.tsx +80 -80
  110. package/src/components/NSBarAction.module.css +91 -91
  111. package/src/components/NSBarAction.tsx +95 -95
  112. package/src/components/NSBarAlert.module.css +79 -79
  113. package/src/components/NSBarAlert.tsx +35 -35
  114. package/src/components/NSBarNotification.module.css +34 -34
  115. package/src/components/NSBarNotification.tsx +89 -89
  116. package/src/components/NSBarTitle.module.css +9 -9
  117. package/src/components/NSBarTitle.tsx +23 -23
  118. package/src/components/NSBox.module.css +87 -87
  119. package/src/components/NSBox.tsx +19 -19
  120. package/src/components/NSBoxBaseCombo.module.css +6 -6
  121. package/src/components/NSBoxBaseCombo.tsx +488 -488
  122. package/src/components/NSBoxBaseLayout.tsx +163 -163
  123. package/src/components/NSBoxBaseLayoutRecursive.tsx +59 -59
  124. package/src/components/NSBoxBoolean.module.css +73 -73
  125. package/src/components/NSBoxBoolean.tsx +152 -152
  126. package/src/components/NSBoxBooleans.tsx +181 -181
  127. package/src/components/NSBoxColor.tsx +107 -107
  128. package/src/components/NSBoxCombo.tsx +95 -95
  129. package/src/components/NSBoxDate.module.css +4 -4
  130. package/src/components/NSBoxDate.tsx +144 -144
  131. package/src/components/NSBoxDateRange.tsx +88 -88
  132. package/src/components/NSBoxDateRangeBase.module.css +73 -73
  133. package/src/components/NSBoxDateRangeBase.tsx +236 -236
  134. package/src/components/NSBoxDateTime.module.css +4 -4
  135. package/src/components/NSBoxDateTime.tsx +136 -136
  136. package/src/components/NSBoxDateTimeRange.tsx +66 -66
  137. package/src/components/NSBoxDouble.tsx +110 -110
  138. package/src/components/NSBoxDuration.module.css +4 -4
  139. package/src/components/NSBoxDuration.tsx +107 -107
  140. package/src/components/NSBoxDynamic.tsx +73 -73
  141. package/src/components/NSBoxDynamics.tsx +68 -68
  142. package/src/components/NSBoxEmail.tsx +117 -117
  143. package/src/components/NSBoxEntity.tsx +181 -181
  144. package/src/components/NSBoxEnum.tsx +113 -113
  145. package/src/components/NSBoxFile.module.css +10 -10
  146. package/src/components/NSBoxFile.tsx +106 -106
  147. package/src/components/NSBoxFilePath.tsx +106 -106
  148. package/src/components/NSBoxFont.tsx +107 -107
  149. package/src/components/NSBoxIPV4.tsx +107 -107
  150. package/src/components/NSBoxIPV4Range.tsx +107 -107
  151. package/src/components/NSBoxIPV6.tsx +104 -104
  152. package/src/components/NSBoxIPV6Range.tsx +107 -107
  153. package/src/components/NSBoxInteger.module.css +16 -16
  154. package/src/components/NSBoxInteger.tsx +126 -126
  155. package/src/components/NSBoxMoney.tsx +115 -115
  156. package/src/components/NSBoxOptions.module.css +53 -53
  157. package/src/components/NSBoxOptions.tsx +182 -182
  158. package/src/components/NSBoxPhone.module.css +35 -35
  159. package/src/components/NSBoxPhone.tsx +120 -120
  160. package/src/components/NSBoxRadio.module.css +33 -33
  161. package/src/components/NSBoxRadio.tsx +104 -104
  162. package/src/components/NSBoxSchemaBase.module.css +17 -17
  163. package/src/components/NSBoxSchemaBase.tsx +562 -562
  164. package/src/components/NSBoxSchemaVariable.module.css +16 -16
  165. package/src/components/NSBoxSchemaVariable.tsx +263 -263
  166. package/src/components/NSBoxSearch.tsx +112 -112
  167. package/src/components/NSBoxSensitive.tsx +133 -133
  168. package/src/components/NSBoxString.tsx +106 -106
  169. package/src/components/NSBoxTextArea.module.css +12 -12
  170. package/src/components/NSBoxTextArea.tsx +136 -136
  171. package/src/components/NSBoxTime.module.css +4 -4
  172. package/src/components/NSBoxTime.tsx +106 -106
  173. package/src/components/NSBoxTimeRange.tsx +220 -220
  174. package/src/components/NSBoxTimeZone.tsx +122 -122
  175. package/src/components/NSBoxURL.tsx +117 -117
  176. package/src/components/NSBoxVersion.tsx +107 -107
  177. package/src/components/NSButton.module.css +57 -57
  178. package/src/components/NSButton.tsx +74 -74
  179. package/src/components/NSButtonBlue.module.css +4 -4
  180. package/src/components/NSButtonBlue.tsx +29 -29
  181. package/src/components/NSButtonGreen.module.css +4 -4
  182. package/src/components/NSButtonGreen.tsx +29 -29
  183. package/src/components/NSButtonRed.module.css +4 -4
  184. package/src/components/NSButtonRed.tsx +29 -29
  185. package/src/components/NSCard.module.css +113 -113
  186. package/src/components/NSCard.tsx +67 -67
  187. package/src/components/NSCardScreenshot.module.css +41 -41
  188. package/src/components/NSCardScreenshot.tsx +31 -31
  189. package/src/components/NSChartBar.tsx +134 -134
  190. package/src/components/NSChartDoughnut.tsx +114 -114
  191. package/src/components/NSChartLine.tsx +110 -110
  192. package/src/components/NSChartPie.tsx +105 -105
  193. package/src/components/NSChartRange.tsx +14 -14
  194. package/src/components/NSColumn.module.css +16 -16
  195. package/src/components/NSColumn.tsx +24 -24
  196. package/src/components/NSCopy.tsx +95 -95
  197. package/src/components/NSCopyBox.module.css +39 -39
  198. package/src/components/NSCopyBox.tsx +53 -53
  199. package/src/components/NSDialog.module.css +105 -105
  200. package/src/components/NSDialog.tsx +72 -72
  201. package/src/components/NSDialogBox.tsx +38 -38
  202. package/src/components/NSDialogBoxDate.tsx +28 -28
  203. package/src/components/NSDialogBoxDateTime.tsx +28 -28
  204. package/src/components/NSDialogBoxPhone.tsx +28 -28
  205. package/src/components/NSDialogBoxString.tsx +28 -28
  206. package/src/components/NSDialogDelete.tsx +36 -36
  207. package/src/components/NSDialogInfo.tsx +49 -49
  208. package/src/components/NSDialogPageSelection.module.css +103 -103
  209. package/src/components/NSDialogPageSelection.tsx +233 -233
  210. package/src/components/NSDownTimer.module.css +55 -55
  211. package/src/components/NSDownTimer.tsx +91 -91
  212. package/src/components/NSDownload.module.css +46 -46
  213. package/src/components/NSDownload.tsx +69 -69
  214. package/src/components/NSElectronicCard.module.css +60 -60
  215. package/src/components/NSElectronicCard.tsx +46 -46
  216. package/src/components/NSEntityCardBackground.module.css +27 -27
  217. package/src/components/NSEntityCardBackground.tsx +36 -36
  218. package/src/components/NSFilterBox.module.css +27 -27
  219. package/src/components/NSFilterBox.tsx +477 -477
  220. package/src/components/NSFilterBoxItems.module.css +15 -15
  221. package/src/components/NSFilterBoxItems.tsx +52 -52
  222. package/src/components/NSFilterItem.module.css +24 -24
  223. package/src/components/NSFilterItem.tsx +27 -27
  224. package/src/components/NSFooter.module.css +140 -140
  225. package/src/components/NSFooter.tsx +321 -321
  226. package/src/components/NSGauge.module.css +67 -67
  227. package/src/components/NSGauge.tsx +52 -52
  228. package/src/components/NSGaugeDate.module.css +57 -57
  229. package/src/components/NSGaugeDate.tsx +125 -125
  230. package/src/components/NSGaugeNumber.module.css +66 -66
  231. package/src/components/NSGaugeNumber.tsx +93 -93
  232. package/src/components/NSHeader.module.css +316 -316
  233. package/src/components/NSHeader.tsx +337 -337
  234. package/src/components/NSHeaderScreenshot.module.css +35 -35
  235. package/src/components/NSHeaderScreenshot.tsx +39 -39
  236. package/src/components/NSID.tsx +190 -188
  237. package/src/components/NSInfo.tsx +50 -50
  238. package/src/components/NSLabel.module.css +25 -25
  239. package/src/components/NSLabel.tsx +47 -47
  240. package/src/components/NSLabelErrorNotifier.module.css +2 -2
  241. package/src/components/NSLabelErrorNotifier.tsx +35 -35
  242. package/src/components/NSLabelSensitive.module.css +9 -9
  243. package/src/components/NSLabelSensitive.tsx +101 -101
  244. package/src/components/NSLayout.module.css +22 -22
  245. package/src/components/NSLayout.tsx +143 -143
  246. package/src/components/NSLine.module.css +15 -15
  247. package/src/components/NSLine.tsx +13 -13
  248. package/src/components/NSLineText.module.css +38 -38
  249. package/src/components/NSLineText.tsx +17 -17
  250. package/src/components/NSLink.module.css +36 -36
  251. package/src/components/NSLink.tsx +26 -26
  252. package/src/components/NSLinkBlue.tsx +22 -22
  253. package/src/components/NSLinkGreen.tsx +13 -13
  254. package/src/components/NSLinkRed.tsx +13 -13
  255. package/src/components/NSListGrouped.module.css +69 -69
  256. package/src/components/NSListGrouped.tsx +106 -106
  257. package/src/components/NSListProduct.tsx +78 -78
  258. package/src/components/NSLoading.module.css +10 -10
  259. package/src/components/NSLoading.tsx +43 -43
  260. package/src/components/NSMegaMenu.module.css +133 -133
  261. package/src/components/NSMegaMenu.tsx +188 -188
  262. package/src/components/NSMenuAction.module.css +88 -88
  263. package/src/components/NSMenuAction.tsx +106 -106
  264. package/src/components/NSMenuButton.module.css +44 -44
  265. package/src/components/NSMenuButton.tsx +198 -198
  266. package/src/components/NSNoData.module.css +11 -11
  267. package/src/components/NSNoData.tsx +24 -24
  268. package/src/components/NSPagination.module.css +87 -87
  269. package/src/components/NSPagination.tsx +183 -183
  270. package/src/components/NSPanel.module.css +28 -28
  271. package/src/components/NSPanel.tsx +26 -26
  272. package/src/components/NSPanelAccordion.module.css +4 -4
  273. package/src/components/NSPanelAccordion.tsx +51 -51
  274. package/src/components/NSProductSearch.module.css +73 -73
  275. package/src/components/NSProductSearch.tsx +193 -193
  276. package/src/components/NSProgressBar.module.css +38 -38
  277. package/src/components/NSProgressBar.tsx +29 -29
  278. package/src/components/NSRange.module.css +66 -66
  279. package/src/components/NSRange.tsx +83 -83
  280. package/src/components/NSReorderDialog.module.css +49 -49
  281. package/src/components/NSReorderDialog.tsx +184 -184
  282. package/src/components/NSRepeater.tsx +328 -328
  283. package/src/components/NSRepeaterNSBoxSchemaVariable.tsx +94 -94
  284. package/src/components/NSRepeaterNSTag.tsx +86 -86
  285. package/src/components/NSRow.module.css +17 -17
  286. package/src/components/NSRow.tsx +24 -24
  287. package/src/components/NSSection.module.css +4 -4
  288. package/src/components/NSSection.tsx +26 -26
  289. package/src/components/NSSectionCards.module.css +38 -38
  290. package/src/components/NSSectionCards.tsx +51 -51
  291. package/src/components/NSSectionTiles.module.css +10 -10
  292. package/src/components/NSSectionTiles.tsx +25 -25
  293. package/src/components/NSSectionTitle.tsx +19 -19
  294. package/src/components/NSSortDialog.tsx +124 -124
  295. package/src/components/NSSpace.tsx +28 -28
  296. package/src/components/NSSplitter.module.css +59 -59
  297. package/src/components/NSSplitter.tsx +211 -211
  298. package/src/components/NSTabPage.module.css +59 -59
  299. package/src/components/NSTabPage.tsx +122 -122
  300. package/src/components/NSTable.module.css +372 -372
  301. package/src/components/NSTable.tsx +1443 -1443
  302. package/src/components/NSTableChart.module.css +119 -119
  303. package/src/components/NSTableChart.tsx +732 -732
  304. package/src/components/NSTag.tsx +74 -74
  305. package/src/components/NSTile.module.css +76 -76
  306. package/src/components/NSTile.tsx +27 -27
  307. package/src/components/NSTimelineMonthly.module.css +71 -71
  308. package/src/components/NSTimelineMonthly.tsx +44 -44
  309. package/src/components/NSTitle.module.css +15 -15
  310. package/src/components/NSTitle.tsx +19 -19
  311. package/src/css.d.ts +13 -13
  312. package/src/formatter/BackColorFormatter.tsx +23 -23
  313. package/src/formatter/BaseColumnFormatter.ts +86 -86
  314. package/src/formatter/BaseURLImageFormatter.tsx +37 -37
  315. package/src/formatter/BooleanFormatter.ts +26 -26
  316. package/src/formatter/BytesFormatter.tsx +19 -19
  317. package/src/formatter/DateFormatter.ts +26 -26
  318. package/src/formatter/DateTimeFormatter.ts +26 -26
  319. package/src/formatter/DurationFormatter.ts +17 -17
  320. package/src/formatter/EmailFormatter.tsx +25 -25
  321. package/src/formatter/EnumFormatter.ts +17 -17
  322. package/src/formatter/FloatFormatter.ts +27 -27
  323. package/src/formatter/ForeColorFormatter.tsx +24 -24
  324. package/src/formatter/IDFormatter.tsx +57 -57
  325. package/src/formatter/IPFormatter.ts +39 -39
  326. package/src/formatter/IntegerFormatter.ts +27 -27
  327. package/src/formatter/JsonFormatter.tsx +77 -77
  328. package/src/formatter/MoneyFormatter.ts +39 -39
  329. package/src/formatter/PercentFormatter.tsx +43 -43
  330. package/src/formatter/PhoneFormatter.tsx +25 -25
  331. package/src/formatter/SensitiveFormatter.tsx +40 -40
  332. package/src/formatter/StringFormatter.tsx +49 -47
  333. package/src/formatter/TimeFormatter.ts +25 -25
  334. package/src/formatter/URLFormatter.tsx +28 -28
  335. package/src/formatter/UnknowFormatter.ts +22 -22
  336. package/src/index.css +5 -5
  337. package/src/index.tsx +7 -7
  338. package/src/main.ts +263 -263
  339. package/src/pages/NSImplementing.module.css +35 -35
  340. package/src/pages/NSImplementing.tsx +32 -32
  341. package/src/pages/NSNotFoundPage.module.css +18 -18
  342. package/src/pages/NSNotFoundPage.tsx +14 -14
  343. package/src/pages/NSUpdating.module.css +35 -35
  344. package/src/pages/NSUpdating.tsx +32 -32
  345. package/src/props/IBackgroundProps.ts +5 -5
  346. package/src/props/IBaseComponentProps.ts +8 -8
  347. package/src/props/IHeaderIconProps.ts +10 -10
  348. package/src/props/IHeaderLeftProps.ts +6 -6
  349. package/src/props/IHeaderRightProps.ts +8 -8
  350. package/src/props/IImageProps.ts +4 -4
  351. package/src/props/ILinkProps.ts +4 -4
  352. package/src/props/IValidationNumberProps.ts +4 -4
  353. package/src/props/IValidationPrecisionProps.ts +3 -3
  354. package/src/props/IValidationProps.ts +9 -9
  355. package/src/props/IValidationRegexProps.ts +4 -4
  356. package/src/props/IValidationStringProps.ts +4 -4
  357. package/src/routing/NSNotifier.ts +114 -114
  358. package/src/routing/NSRouterMaker.tsx +28 -28
  359. package/tsconfig.json +43 -43
  360. package/dist/components/NSListMenu.d.ts +0 -9
  361. package/dist/components/NSListMenu.js +0 -135
  362. package/dist/components/NSListMenu.js.map +0 -1
@@ -1,1444 +1,1444 @@
1
- "use client";
2
- import { BaseMetaColumn, BaseMetaTable, IStorageLocal, SortItem } from "namirasoft-core";
3
- import { Component, createRef, ReactNode } from "react";
4
- import { utils, writeFile } from 'xlsx';
5
- import { BaseColumnFormatter } from '../formatter/BaseColumnFormatter';
6
- import { IBaseComponentProps } from "../props/IBaseComponentProps";
7
- import { NSButton, NSButtonProps } from './NSButton';
8
- import { NSDialogInfo } from './NSDialogInfo';
9
- import { NSDialogPageSelection } from './NSDialogPageSelection';
10
- import { NSLoading } from './NSLoading';
11
- import { NSNoData } from './NSNoData';
12
- import { NSPagination } from './NSPagination';
13
- import { NSReorderDialog } from './NSReorderDialog';
14
- import { NSSortDialog } from './NSSortDialog';
15
- import { NSTableChart } from './NSTableChart';
16
- import Styles from './NSTable.module.css';
17
-
18
- export interface TableInfo
19
- {
20
- name: string;
21
- text: string;
22
- }
23
-
24
- export interface TableColumnInfo
25
- {
26
- table: TableInfo;
27
- index: number;
28
- type: string;
29
- name: string;
30
- text: string;
31
- formatter: BaseColumnFormatter;
32
- group?: string;
33
- defaults?: {
34
- hidden?: boolean;
35
- };
36
- }
37
-
38
- export interface TableRowInfo<RowType>
39
- {
40
- index: number;
41
- value: RowType;
42
- }
43
-
44
- export interface TableCellInfo<RowType>
45
- {
46
- column: TableColumnInfo;
47
- row: TableRowInfo<RowType>;
48
- value: any;
49
- formatted: any;
50
- }
51
-
52
- interface PrintableRow<RowType>
53
- {
54
- value: RowType;
55
- index: number;
56
- depth: number;
57
- ancestorHasNext: boolean[];
58
- isLastChild: boolean;
59
- }
60
-
61
- export interface NSTableProps<RowType> extends IBaseComponentProps
62
- {
63
- name: string;
64
- checkbox: boolean;
65
- columns: TableColumnInfo[];
66
- getRows: (page: number, size: number, sorts: SortItem[]) => Promise<{ rows: RowType[], count: number }>;
67
- getRowKey: (row: TableRowInfo<RowType>) => string;
68
- getRowAttributes?: (row_value: RowType, row_index: number) => { [key: string]: any };
69
- getColumnAttributes?: (column: TableColumnInfo) => { [key: string]: any };
70
- getCellFormattedValue?: (value: any, column: TableColumnInfo, row: TableRowInfo<RowType>, printable: boolean) => any;
71
- onColumnClick?: (e: React.MouseEvent<HTMLTableCellElement, MouseEvent>, column: TableColumnInfo) => void;
72
- onRowClick?: (e: React.MouseEvent<HTMLTableRowElement, MouseEvent>, row: TableRowInfo<RowType>) => void;
73
- onCellClick?: (e: React.MouseEvent<HTMLTableCellElement, MouseEvent>, cell: TableCellInfo<RowType>) => boolean | undefined;
74
- getRowCheckboxDisabled?: (row: TableRowInfo<RowType>) => boolean;
75
- buttons?: NSButtonProps[];
76
- children?: ReactNode;
77
- headerChildren?: ReactNode;
78
- hierarchy?: { id: string, parent: string, group?: string[] };
79
- defaultExpanded?: boolean;
80
- onSelectedIDsChanged?: () => void;
81
- onSortChanged?: (items: SortItem[] | null) => void;
82
- onVisibleColumnsChanged?: (visible_columns: { table: string, column: string }[]) => void;
83
- sort?: NSTableSortConfig;
84
- ui?: {
85
- pagination?: boolean;
86
- button_panel?: boolean | NSTableButtonPanelConfig;
87
- };
88
- }
89
-
90
- export interface NSTableSortConfig
91
- {
92
- enabled?: boolean;
93
- onSort?: (items: SortItem[] | null) => void;
94
- }
95
-
96
- export type NSTableButtonKey = "reorder" | "sort" | "fullscreen" | "chart" | "export" | "print" | "refresh" | "expand";
97
- export type NSTableButtonPanelConfig = { [key in NSTableButtonKey]?: boolean };
98
-
99
- interface NSTableState<RowType>
100
- {
101
- columns: TableColumnInfo[];
102
- visible_columns: { table: string, column: string }[];
103
- totalItems: number;
104
- rows: RowType[] | null;
105
- title?: string;
106
- model: {
107
- show: boolean;
108
- description?: string;
109
- };
110
- selectedIDs: string[];
111
- selectedRows: RowType[];
112
- expandedIDs: string[];
113
- pageSelectionMode: PageSelectionMode;
114
- sortItems: SortItem[] | null;
115
- columnWidths: { [key: string]: number };
116
- fullscreen: boolean;
117
- showChart: boolean;
118
- showSortDialog: boolean;
119
- showReorderDialog: boolean;
120
- }
121
-
122
- enum PageSelectionMode
123
- {
124
- Hidden = "Hidden",
125
- Print = "Print",
126
- CSV = "CSV"
127
- }
128
-
129
- export class NSTable<RowType> extends Component<NSTableProps<RowType>, NSTableState<RowType>>
130
- {
131
- private Table_Ref = createRef<HTMLTableElement>();
132
- NSPagination = createRef<NSPagination>();
133
-
134
- private LastSelectedRowID: number | null = null;
135
- private resizeMoveHandler: ((e: MouseEvent) => void) | null = null;
136
- private resizeUpHandler: (() => void) | null = null;
137
-
138
- constructor(props: NSTableProps<RowType>)
139
- {
140
- super(props);
141
- this.state = {
142
- columns: props.columns,
143
- visible_columns: this.getVisibleColumns(),
144
- rows: null,
145
- totalItems: 0,
146
- model: { show: false },
147
- selectedIDs: [],
148
- selectedRows: [],
149
- expandedIDs: [],
150
- pageSelectionMode: PageSelectionMode.Hidden,
151
- sortItems: this.getSortItems(),
152
- columnWidths: this.loadColumnWidths(),
153
- fullscreen: false,
154
- showChart: false,
155
- showSortDialog: false,
156
- showReorderDialog: false,
157
- };
158
- this.getSortItemsKey = this.getSortItemsKey.bind(this);
159
- this.getSortItems = this.getSortItems.bind(this);
160
- this.setSortItems = this.setSortItems.bind(this);
161
- this.getVisibleColumnsKey = this.getVisibleColumnsKey.bind(this);
162
- this.getVisibleColumns = this.getVisibleColumns.bind(this);
163
- this.setVisibleColumns = this.setVisibleColumns.bind(this);
164
- this.setColumns = this.setColumns.bind(this);
165
- this.setColumns = this.setColumns.bind(this);
166
- this.setRows = this.setRows.bind(this);
167
- this.foreachColumn = this.foreachColumn.bind(this);
168
- this.getColumns = this.getColumns.bind(this);
169
- this.showModal = this.showModal.bind(this);
170
- this.hideModal = this.hideModal.bind(this);
171
- this.onPageChange = this.onPageChange.bind(this);
172
- this.isSomeSelected = this.isSomeSelected.bind(this);
173
- this.toggleAllSelections = this.toggleAllSelections.bind(this);
174
- this.print = this.print.bind(this);
175
- this.exportCSV = this.exportCSV.bind(this);
176
- this.isAllSelected = this.isAllSelected.bind(this);
177
- this.reload = this.reload.bind(this);
178
- this.toggleFullscreen = this.toggleFullscreen.bind(this);
179
- this.toggleExpandAll = this.toggleExpandAll.bind(this);
180
- }
181
- private toggleFullscreen()
182
- {
183
- this.setState({ fullscreen: !this.state.fullscreen });
184
- }
185
- private getSortItemsKey(): string
186
- {
187
- return "ns_table_" + this.props.name + "_sort_items";
188
- }
189
- getSortItems(): SortItem[]
190
- {
191
- let storage = new IStorageLocal();
192
- let item = storage.get(this.getSortItemsKey(), "");
193
- try
194
- {
195
- return SortItem.parse(item);
196
- } catch (error)
197
- {
198
- }
199
- return [];
200
- }
201
- isSortEnabled(): boolean
202
- {
203
- return this.props.sort?.enabled ?? true;
204
- }
205
- private hasCustomSort(): boolean
206
- {
207
- return !!this.props.sort?.onSort;
208
- }
209
- setSortItems(items: SortItem[] | null)
210
- {
211
- if (!this.isSortEnabled())
212
- return;
213
- let storage = new IStorageLocal();
214
- storage.set(this.getSortItemsKey(), SortItem.stringify(items));
215
- this.setState({ sortItems: items }, () =>
216
- {
217
- this.props.onSortChanged?.(items);
218
- this.props.sort?.onSort?.(items);
219
- });
220
- }
221
- private compareRowsBySortItems(a: RowType, b: RowType, items: SortItem[]): number
222
- {
223
- for (let item of items)
224
- {
225
- let column = this.state.columns.find(c => c.table.name === item.table.name && c.name === item.column.name);
226
- let va = (a as any)[item.column.name];
227
- let vb = (b as any)[item.column.name];
228
- let cmp = column ? column.formatter.compare(va, vb) : BaseColumnFormatter.smartCompare(va, vb);
229
- if (cmp !== 0)
230
- return item.ascending ? cmp : -cmp;
231
- }
232
- return 0;
233
- }
234
- private sortRowsClient(rows: RowType[]): RowType[]
235
- {
236
- let items = this.state.sortItems;
237
- if (!items || items.length === 0)
238
- return rows;
239
- let decorated = rows.map((row, index) => ({ row, index }));
240
- decorated.sort((x, y) =>
241
- {
242
- let cmp = this.compareRowsBySortItems(x.row, y.row, items!);
243
- return cmp !== 0 ? cmp : x.index - y.index;
244
- });
245
- return decorated.map(d => d.row);
246
- }
247
- private getDisplayRows(): RowType[] | null
248
- {
249
- let rows = this.state.rows;
250
- if (!rows || !this.isSortEnabled() || this.hasCustomSort())
251
- return rows;
252
- return this.sortRowsClient(rows);
253
- }
254
- private getVisibleColumnsKey(): string
255
- {
256
- return "ns_table_" + this.props.name + "_visible_columns";
257
- }
258
- getVisibleColumns(): { table: string, column: string }[]
259
- {
260
- let storage = new IStorageLocal();
261
- let item = storage.get(this.getVisibleColumnsKey(), "");
262
- try
263
- {
264
- return JSON.parse(item);
265
- } catch (error)
266
- {
267
- }
268
- return [];
269
- }
270
- setVisibleColumns(visible_columns: { table: string, column: string }[])
271
- {
272
- let storage = new IStorageLocal();
273
- storage.set(this.getVisibleColumnsKey(), JSON.stringify(visible_columns));
274
- this.setState({ visible_columns }, () =>
275
- {
276
- this.props.onVisibleColumnsChanged?.(visible_columns);
277
- });
278
- }
279
- setColumns(columns: TableColumnInfo[])
280
- {
281
- this.setState({ columns });
282
- }
283
- setRows(rows: RowType[] | null, totalItems: number)
284
- {
285
- this.setState({ rows, totalItems });
286
- }
287
- foreachColumn(visible: boolean | null, handler: (column: TableColumnInfo) => void)
288
- {
289
- let vs = this.getVisibleColumns();
290
- let shouldBeVisible = (c: TableColumnInfo) =>
291
- {
292
- if (vs.length === 0)
293
- return !c.defaults?.hidden;
294
- return vs.filter(v => v.table === c.table.name && v.column === c.name).length > 0;
295
- };
296
- this.state.columns.forEach(c =>
297
- {
298
- if (visible === null || shouldBeVisible(c) === visible)
299
- handler(c);
300
- })
301
- }
302
- getColumns(visible: boolean | null): TableColumnInfo[]
303
- {
304
- let columns: TableColumnInfo[] = [];
305
- this.foreachColumn(visible, c => columns.push(c));
306
- if (visible === true)
307
- {
308
- let vs = this.getVisibleColumns();
309
- if (vs.length > 0)
310
- columns.sort((a, b) =>
311
- {
312
- let ai = vs.findIndex(v => v.table === a.table.name && v.column === a.name);
313
- let bi = vs.findIndex(v => v.table === b.table.name && v.column === b.name);
314
- return ai - bi;
315
- });
316
- }
317
- return columns;
318
- }
319
- private showModal(description: string, title?: string)
320
- {
321
- this.setState({ model: { show: true, description }, title });
322
- }
323
- private hideModal()
324
- {
325
- this.setState({ model: { show: false } });
326
- }
327
- private onPageChange(page: number, size: number)
328
- {
329
- this.reload(page, size);
330
- }
331
- public async reload(page: number | null, size: number | null)
332
- {
333
- this.setRows(null, 0);
334
- if (page === null)
335
- page = this.NSPagination.current?.getCurrentPage() ?? 0;
336
- if (size === null)
337
- size = this.NSPagination.current?.getPageSize() ?? 0;
338
- try
339
- {
340
- let sorts = this.getSortItems();
341
- let res = await this.props.getRows(page, size, sorts);
342
- this.setRows(res.rows, res.count);
343
- } catch (error)
344
- {
345
- }
346
- }
347
- private toggleAllSelections()
348
- {
349
- let selectedIDs: string[] = [];
350
- let selectedRows: RowType[] = [];
351
- this.LastSelectedRowID = null;
352
-
353
- if (this.getSelectedIDs().length === 0)
354
- {
355
- this.state.rows?.forEach((row, rowIndex) =>
356
- {
357
- let rowInfo = { index: rowIndex, value: row };
358
- if (this.props.getRowCheckboxDisabled?.(rowInfo))
359
- return;
360
- let id = this.props.getRowKey(rowInfo);
361
- selectedIDs.push(id);
362
- selectedRows.push(row);
363
- })
364
- }
365
-
366
- this.setSelectedIDs(selectedIDs, selectedRows);
367
- }
368
- private escapeHtml(value: any): string
369
- {
370
- if (value === null || value === undefined)
371
- return "";
372
- let text = typeof value === "object" ? JSON.stringify(value) : String(value);
373
- return text
374
- .replace(/&/g, "&amp;")
375
- .replace(/</g, "&lt;")
376
- .replace(/>/g, "&gt;")
377
- .replace(/"/g, "&quot;")
378
- .replace(/'/g, "&#39;");
379
- }
380
- private getPrintableRows(rows: RowType[], expandAll: boolean): PrintableRow<RowType>[]
381
- {
382
- let result: PrintableRow<RowType>[] = [];
383
- if (!this.props.hierarchy)
384
- {
385
- rows.forEach((value, index) => result.push({ value, index, depth: 0, ancestorHasNext: [], isLastChild: true }));
386
- return result;
387
- }
388
- let childrenMap = this.buildChildrenMap(rows);
389
- let indexOfRow = new Map<RowType, number>();
390
- rows.forEach((row, index) => indexOfRow.set(row, index));
391
- let parentCol = this.props.hierarchy.parent;
392
- let ids = new Set<string>();
393
- rows.forEach(row => ids.add(this.getRowIdValue(row) + "" + this.getGroupKey(row)));
394
- let topLevel = rows.filter(row =>
395
- {
396
- let pid = (row as any)[parentCol];
397
- let pidKey = pid === null || pid === undefined ? "" : String(pid);
398
- if (pidKey === "")
399
- return true;
400
- return !ids.has(pidKey + "" + this.getGroupKey(row));
401
- });
402
- let walk = (value: RowType, depth: number, ancestorHasNext: boolean[], isLastChild: boolean) =>
403
- {
404
- let index = indexOfRow.get(value) ?? 0;
405
- result.push({ value, index, depth, ancestorHasNext, isLastChild });
406
- let rowKey = this.props.getRowKey({ index, value });
407
- if (!expandAll && !this.isExpanded(rowKey))
408
- return;
409
- let children = childrenMap?.get(this.getRowChildLookupKey(value)) ?? [];
410
- let childAncestors = [...ancestorHasNext, !isLastChild];
411
- children.forEach((child, childIndex) => walk(child, depth + 1, childAncestors, childIndex === children.length - 1));
412
- };
413
- topLevel.forEach((row, index) => walk(row, 0, [], index === topLevel.length - 1));
414
- return result;
415
- }
416
- private buildTreeGuides(entry: PrintableRow<RowType>): string
417
- {
418
- if (entry.depth === 0)
419
- return "";
420
- let indent = 16;
421
- let color = "#000";
422
- let overhang = 8;
423
- let guides = "";
424
- for (let i = 0; i < entry.depth; i++)
425
- {
426
- let center = indent / 2;
427
- let lines = "";
428
- if (i < entry.depth - 1)
429
- {
430
- if (entry.ancestorHasNext[i])
431
- lines += `<span style="position: absolute; inset-inline-start: ${center}px; top: 0; height: 100%; width: 1px; background: ${color};"></span>`;
432
- }
433
- else
434
- {
435
- lines += `<span style="position: absolute; inset-inline-start: ${center}px; top: -${overhang}px; height: calc(50% + ${overhang}px); width: 1px; background: ${color};"></span>`;
436
- if (!entry.isLastChild)
437
- lines += `<span style="position: absolute; inset-inline-start: ${center}px; top: 50%; height: 50%; width: 1px; background: ${color};"></span>`;
438
- lines += `<span style="position: absolute; inset-inline-start: ${center}px; top: 50%; width: ${center - 2}px; height: 1px; background: ${color};"></span>`;
439
- }
440
- guides += `<span style="position: relative; display: inline-block; width: ${indent}px; align-self: stretch; flex: 0 0 auto;">${lines}</span>`;
441
- }
442
- return guides;
443
- }
444
- private buildPrintDocument(rows: RowType[], expandAll: boolean): string
445
- {
446
- let columns = this.getColumns(true);
447
- let totalWidth = columns.reduce((sum, column) => sum + this.getColumnWidth(column), 0) || 1;
448
- let colgroup = columns
449
- .map(column => `<col style="width: ${(this.getColumnWidth(column) / totalWidth * 100).toFixed(4)}%" />`)
450
- .join("");
451
- let head = columns
452
- .map(column => `<th>${this.escapeHtml(column.text)}</th>`)
453
- .join("");
454
- let printable = this.getPrintableRows(rows, expandAll);
455
- let body = printable
456
- .map((entry, entryIndex) =>
457
- {
458
- let row = { index: entry.index, value: entry.value };
459
- let next = printable[entryIndex + 1];
460
- let previous = printable[entryIndex - 1];
461
- let removeTop = entry.depth > 0 && !!previous;
462
- let removeBottom = !!next && next.depth > 0;
463
- let edgeStyle = `${removeTop ? "border-top: none;" : ""}${removeBottom ? "border-bottom: none;" : ""}`;
464
- let cells = columns
465
- .map((column, columnIndex) =>
466
- {
467
- let text = this.escapeHtml(this.getCellTableInfo(column, row, true).formatted);
468
- if (columnIndex === 0 && this.props.hierarchy && entry.depth > 0)
469
- {
470
- let guides = this.buildTreeGuides(entry);
471
- let overlay = `<span style="position: absolute; top: 0; bottom: 0; inset-inline-start: 8px; display: flex; align-items: stretch;">${guides}</span>`;
472
- return `<td style="position: relative; padding-inline-start: ${8 + entry.depth * 16}px;${edgeStyle}">${overlay}${text}</td>`;
473
- }
474
- if (columnIndex === 0 && edgeStyle)
475
- return `<td style="${edgeStyle}">${text}</td>`;
476
- return `<td>${text}</td>`;
477
- })
478
- .join("");
479
- return `<tr>${cells}</tr>`;
480
- })
481
- .join("");
482
- let direction = document.documentElement.dir || "ltr";
483
- return `
484
- <!DOCTYPE html>
485
- <html dir="${direction}">
486
- <head>
487
- <meta charset="utf-8" />
488
- <title>${this.escapeHtml(this.props.name)}</title>
489
- <style>
490
- * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
491
- body { font-family: sans-serif; margin: 16px; }
492
- table { width: 100%; border-collapse: collapse; table-layout: fixed; }
493
- th, td { border: 1px solid #000; padding: 8px; text-align: start; overflow-wrap: break-word; word-break: break-word; }
494
- thead { display: table-header-group; }
495
- tr { page-break-inside: avoid; }
496
- </style>
497
- </head>
498
- <body>
499
- <table>
500
- <colgroup>${colgroup}</colgroup>
501
- <thead><tr>${head}</tr></thead>
502
- <tbody>${body}</tbody>
503
- </table>
504
- </body>
505
- </html>
506
- `;
507
- }
508
- print(rows: RowType[], expandAll: boolean = false)
509
- {
510
- let blob = new Blob([this.buildPrintDocument(rows, expandAll)], { type: "text/html" });
511
- let url = URL.createObjectURL(blob);
512
- let printWindow = window.open(url, "_blank", "height=800,width=800");
513
- if (!printWindow)
514
- {
515
- URL.revokeObjectURL(url);
516
- this.showModal("Please allow pop-ups to print this table.", this.props.name);
517
- return;
518
- }
519
- printWindow.onload = () =>
520
- {
521
- printWindow!.focus();
522
- printWindow!.onafterprint = () =>
523
- {
524
- printWindow!.close();
525
- URL.revokeObjectURL(url);
526
- };
527
- printWindow!.print();
528
- };
529
- }
530
- exportCSV(rows: RowType[])
531
- {
532
- let columns = this.getColumns(true);
533
- let header = columns.map(column => column.text);
534
- let data = (rows || []).map((rowValue, rowIndex) =>
535
- {
536
- let row = { index: rowIndex, value: rowValue };
537
- return columns.map(column =>
538
- {
539
- let formatted = this.getCellTableInfo(column, row, true).formatted;
540
- if (formatted === null || formatted === undefined)
541
- return "";
542
- return typeof formatted === "object" ? JSON.stringify(formatted) : formatted;
543
- });
544
- });
545
- let worksheet = utils.aoa_to_sheet([header, ...data]);
546
- worksheet["!cols"] = columns.map(column => ({ wpx: this.getColumnWidth(column) }));
547
- let workbook = utils.book_new();
548
- utils.book_append_sheet(workbook, worksheet, this.props.name);
549
- writeFile(workbook, this.props.name + ".xlsx");
550
- }
551
-
552
- handleSelectRow(row: TableRowInfo<RowType>, selected: boolean, ctrlKey: boolean, shiftKey: boolean)
553
- {
554
- let selectedIDs = this.state.selectedIDs;
555
- let selectedRows = this.state.selectedRows;
556
- let rowID = this.props.getRowKey(row);
557
-
558
- if (shiftKey)
559
- {
560
- selectedRows = this.getRowsInRange(this.LastSelectedRowID ?? 0, row.index);
561
- selectedIDs = selectedRows.map(row => this.props.getRowKey({ index: -1, value: row }));
562
- }
563
- else if (selected)
564
- {
565
- if (this.isMultipleSelected() && !ctrlKey)
566
- {
567
- selectedIDs = [rowID];
568
- selectedRows = [row.value];
569
- this.LastSelectedRowID = row.index;
570
- }
571
- else
572
- {
573
- selectedIDs = selectedIDs.filter(x => x !== rowID);
574
- selectedRows = selectedRows.filter(x => this.props.getRowKey({ index: -1, value: x }) !== rowID);
575
- this.LastSelectedRowID = ctrlKey ? row.index : null;
576
- }
577
- }
578
- else
579
- {
580
- if (ctrlKey)
581
- {
582
- selectedIDs.push(rowID);
583
- selectedRows.push(row.value);
584
- }
585
- else
586
- {
587
- selectedIDs = [rowID];
588
- selectedRows = [row.value];
589
- }
590
- this.LastSelectedRowID = row.index;
591
- }
592
-
593
- this.setSelectedIDs(selectedIDs, selectedRows);
594
- }
595
- getRowsInRange(fromIndex: number, toIndex: number)
596
- {
597
- let rows = this.getDisplayRows();
598
- if (!rows)
599
- return [];
600
-
601
- let start = Math.min(fromIndex, toIndex);
602
- let end = Math.max(fromIndex, toIndex);
603
-
604
- return rows.slice(start, end + 1);
605
- }
606
- getSelectedIDs(): string[]
607
- {
608
- return this.state.selectedIDs;
609
- }
610
- getSelectedRows(): RowType[]
611
- {
612
- return this.state.selectedRows;
613
- }
614
- setSelectedIDs(ids: string[], rows: RowType[], callback?: () => void)
615
- {
616
- this.setState({ selectedIDs: ids, selectedRows: rows }, () =>
617
- {
618
- if (this.props.onSelectedIDsChanged)
619
- this.props.onSelectedIDsChanged();
620
- if (callback)
621
- callback();
622
- });
623
- }
624
- private getGroupKey(row: RowType): string
625
- {
626
- let h = this.props.hierarchy;
627
- if (!h || !h.group || h.group.length === 0)
628
- return "";
629
- return h.group.map(g =>
630
- {
631
- let v = (row as any)[g];
632
- return v === null || v === undefined ? "" : String(v);
633
- }).join("");
634
- }
635
- private buildChildrenMap(rows: RowType[] | null): Map<string, RowType[]> | null
636
- {
637
- let h = this.props.hierarchy;
638
- if (!h || !rows)
639
- return null;
640
- let map = new Map<string, RowType[]>();
641
- for (let r of rows)
642
- {
643
- let pid = (r as any)[h.parent];
644
- let pidKey = pid === null || pid === undefined ? "" : String(pid);
645
- let key = pidKey + "" + this.getGroupKey(r);
646
- let arr = map.get(key);
647
- if (!arr) { arr = []; map.set(key, arr); }
648
- arr.push(r);
649
- }
650
- return map;
651
- }
652
- private getRowIdValue(row: RowType): string
653
- {
654
- let h = this.props.hierarchy;
655
- if (!h) return "";
656
- let v = (row as any)[h.id];
657
- return v === null || v === undefined ? "" : String(v);
658
- }
659
- private getRowChildLookupKey(row: RowType): string
660
- {
661
- return this.getRowIdValue(row) + "" + this.getGroupKey(row);
662
- }
663
- private isExpanded(rowKey: string): boolean
664
- {
665
- let inList = this.state.expandedIDs.includes(rowKey);
666
- return this.props.defaultExpanded ? !inList : inList;
667
- }
668
- private toggleExpanded(rowKey: string)
669
- {
670
- let ids = this.state.expandedIDs;
671
- if (ids.includes(rowKey))
672
- ids = ids.filter(x => x !== rowKey);
673
- else
674
- ids = [...ids, rowKey];
675
- this.setState({ expandedIDs: ids });
676
- }
677
- private getExpandableRowKeys(): string[]
678
- {
679
- let rows = this.state.rows;
680
- if (!this.props.hierarchy || !rows)
681
- return [];
682
- let childrenMap = this.buildChildrenMap(rows);
683
- if (!childrenMap)
684
- return [];
685
- let keys: string[] = [];
686
- rows.forEach((value, index) =>
687
- {
688
- let children = childrenMap.get(this.getRowChildLookupKey(value)) ?? [];
689
- if (children.length > 0)
690
- keys.push(this.props.getRowKey({ index, value }));
691
- });
692
- return keys;
693
- }
694
- private isAnyExpanded(): boolean
695
- {
696
- return this.getExpandableRowKeys().some(key => this.isExpanded(key));
697
- }
698
- private toggleExpandAll()
699
- {
700
- let keys = this.getExpandableRowKeys();
701
- let expand = !this.isAnyExpanded();
702
- let inList = this.props.defaultExpanded ? !expand : expand;
703
- this.setState({ expandedIDs: inList ? keys : [] });
704
- }
705
- private isAllSelected(): boolean
706
- {
707
- if (!this.state.rows || this.state.rows.length === 0)
708
- return false;
709
- const selectableCount = this.state.rows.filter((row, i) =>
710
- !this.props.getRowCheckboxDisabled?.({ index: i, value: row })
711
- ).length;
712
- return selectableCount > 0 && this.state.selectedIDs.length === selectableCount;
713
- }
714
- private isSomeSelected(): boolean
715
- {
716
- return this.state.selectedIDs.length > 0 && !this.isAllSelected();
717
- }
718
- private isMultipleSelected(): boolean
719
- {
720
- return this.state.selectedIDs.length > 1;
721
- }
722
- private getCellTableInfo(column: TableColumnInfo, row: TableRowInfo<RowType>, printable: boolean)
723
- {
724
- let value = (row.value as any)[column.name];
725
- let formatted = this.getCellFormattedValue(value, column, row, printable);
726
- return { column, row, value, formatted };
727
- }
728
- private getCellFormattedValue(value: any, column: TableColumnInfo, row: TableRowInfo<RowType>, printable: boolean)
729
- {
730
- if (this.props.getCellFormattedValue)
731
- return this.props.getCellFormattedValue(value, column, row, printable);
732
- return column.formatter.format(value, column, row, printable);
733
- }
734
- private getColumnWidthsKey(): string
735
- {
736
- return "ns_table_" + this.props.name + "_column_widths";
737
- }
738
- private loadColumnWidths(): { [key: string]: number }
739
- {
740
- let storage = new IStorageLocal();
741
- let item = storage.get(this.getColumnWidthsKey(), "");
742
- try
743
- {
744
- return JSON.parse(item);
745
- }
746
- catch
747
- {
748
- return {};
749
- }
750
- }
751
- private saveColumnWidths(widths: { [key: string]: number })
752
- {
753
- let storage = new IStorageLocal();
754
- storage.set(this.getColumnWidthsKey(), JSON.stringify(widths));
755
- }
756
- private getColumnKey(column: TableColumnInfo): string
757
- {
758
- return column.table.name + "_" + column.name;
759
- }
760
- private getColumnWidth(column: TableColumnInfo): number
761
- {
762
- let key = this.getColumnKey(column);
763
- if (this.state.columnWidths[key] !== undefined)
764
- return this.state.columnWidths[key];
765
- let w = column.formatter.width;
766
- if (typeof w === 'number') return w;
767
- if (typeof w === 'string')
768
- {
769
- let parsed = parseInt(w, 10);
770
- if (!isNaN(parsed)) return parsed;
771
- }
772
- return 120;
773
- }
774
- private getGridTemplateColumns(): string
775
- {
776
- let parts: string[] = [];
777
- if (this.props.checkbox)
778
- parts.push("40px");
779
- this.getColumns(true).forEach(column =>
780
- {
781
- parts.push(this.getColumnWidth(column) + "px");
782
- });
783
- return parts.join(" ");
784
- }
785
- private startResize(e: React.MouseEvent, column: TableColumnInfo)
786
- {
787
- e.preventDefault();
788
- e.stopPropagation();
789
- let key = this.getColumnKey(column);
790
- let startX = e.clientX;
791
- let startWidth = this.getColumnWidth(column);
792
- let currentWidths = { ...this.state.columnWidths };
793
- document.body.style.cursor = 'col-resize';
794
- document.body.style.userSelect = 'none';
795
- const onMouseMove = (ev: MouseEvent) =>
796
- {
797
- let delta = ev.clientX - startX;
798
- let newWidth = Math.max(80, startWidth + delta);
799
- currentWidths = { ...currentWidths, [key]: newWidth };
800
- this.setState({ columnWidths: currentWidths });
801
- };
802
- const onMouseUp = () =>
803
- {
804
- document.body.style.cursor = '';
805
- document.body.style.userSelect = '';
806
- document.removeEventListener('mousemove', onMouseMove);
807
- document.removeEventListener('mouseup', onMouseUp);
808
- this.resizeMoveHandler = null;
809
- this.resizeUpHandler = null;
810
- this.saveColumnWidths(currentWidths);
811
- };
812
- this.resizeMoveHandler = onMouseMove;
813
- this.resizeUpHandler = onMouseUp;
814
- document.addEventListener('mousemove', onMouseMove);
815
- document.addEventListener('mouseup', onMouseUp);
816
- }
817
- override componentDidMount(): void
818
- {
819
- if (!this.NSPagination.current)
820
- this.reload(null, null);
821
- }
822
- override componentWillUnmount(): void
823
- {
824
- if (this.resizeMoveHandler)
825
- document.removeEventListener('mousemove', this.resizeMoveHandler);
826
- if (this.resizeUpHandler)
827
- document.removeEventListener('mouseup', this.resizeUpHandler);
828
- }
829
- override render()
830
- {
831
-
832
- let show_pagination = this.props.ui?.pagination ?? true;
833
- let button_panel_config = this.props.ui?.button_panel;
834
- let show_button_panel = button_panel_config !== false;
835
- let isButtonEnabled = (key: NSTableButtonKey): boolean =>
836
- {
837
- if (button_panel_config === undefined || typeof button_panel_config === "boolean")
838
- return true;
839
- return button_panel_config[key] ?? true;
840
- };
841
- let getRowAttributes = (row_value: RowType, row_index: number) =>
842
- {
843
- if (this.props.getRowAttributes)
844
- return this.props.getRowAttributes(row_value, row_index);
845
- return {};
846
- }
847
- let getColumnAttributes = (column: TableColumnInfo) =>
848
- {
849
- if (this.props.getColumnAttributes)
850
- return this.props.getColumnAttributes(column);
851
- return {};
852
- }
853
- let onColumnClick = (e: React.MouseEvent<HTMLTableCellElement, MouseEvent>, column: TableColumnInfo) =>
854
- {
855
- if ((e.target as HTMLElement).tagName !== "SPAN")
856
- return;
857
-
858
- if (this.props.onColumnClick)
859
- this.props.onColumnClick(e, column);
860
- }
861
- let onRowClick = (e: React.MouseEvent<HTMLTableRowElement, MouseEvent>, row: TableRowInfo<RowType>, selected: boolean) =>
862
- {
863
- const target = e.target as HTMLElement;
864
-
865
- if (target.tagName === "TR" || target.tagName === "TD" || target.closest("td")?.dataset.cellType === "checkbox")
866
- this.handleSelectRow(row, selected, e.ctrlKey, e.shiftKey);
867
-
868
- if (this.props.onRowClick)
869
- this.props.onRowClick(e, row);
870
- };
871
- let onCellClick = (e: React.MouseEvent<HTMLTableCellElement, MouseEvent>, cell: TableCellInfo<RowType>) =>
872
- {
873
- if ((e.target as HTMLElement).tagName !== "SPAN")
874
- return;
875
-
876
- let overided = cell.column.formatter.onclick_overrided;
877
- if (this.props.onCellClick)
878
- overided = this.props.onCellClick(e, cell) ?? true;
879
- if (!overided)
880
- this.showModal(cell.formatted, cell.column.text);
881
- };
882
- let gridTemplateColumns = this.getGridTemplateColumns();
883
- let content;
884
- if (this.state.rows === null)
885
- {
886
- content = (
887
- <tr className="d-flex justify-content-center align-items-center p-0">
888
- <td className="w-100 h-100 m-0 mh-100">
889
- <NSLoading
890
- title="Loading table rows ..."
891
- />
892
- </td>
893
- </tr>
894
- );
895
- }
896
- else if (this.state.rows.length === 0)
897
- {
898
- content = (
899
- <tr className="d-flex justify-content-center align-items-center p-0">
900
- <td className="w-100 h-100 m-0 mh-100">
901
- <NSNoData lable='No Data' classList={["p-3"]} />
902
- </td>
903
- </tr>
904
- );
905
- }
906
- else
907
- {
908
- let displayRows = this.getDisplayRows() ?? [];
909
- let hierarchical = !!this.props.hierarchy;
910
- let childrenMap = this.buildChildrenMap(displayRows);
911
- let indexOfRow = new Map<RowType, number>();
912
- displayRows.forEach((r, i) => indexOfRow.set(r, i));
913
- let INDENT = 16;
914
- let lineColor = "rgba(20, 27, 92, 0.35)";
915
- let renderRow = (rowValue: RowType, rowIndex: number, depth: number, parentKey: string, ancestorHasNext: boolean[], isLastChild: boolean): React.ReactNode[] =>
916
- {
917
- let row = { index: rowIndex, value: rowValue };
918
- let rowKey = this.props.getRowKey(row);
919
- let reactKey = parentKey ? `${parentKey}>${rowKey}` : rowKey;
920
- let isSelected = this.state.selectedIDs.includes(rowKey);
921
- let isLastSelected = this.LastSelectedRowID === row.index;
922
- let isCheckboxDisabled = this.props.getRowCheckboxDisabled?.(row) ?? false;
923
- let children: RowType[] = [];
924
- if (hierarchical && childrenMap)
925
- {
926
- let lookupKey = this.getRowChildLookupKey(rowValue);
927
- children = childrenMap.get(lookupKey) ?? [];
928
- }
929
- let hasChildren = children.length > 0;
930
- let expanded = this.isExpanded(rowKey);
931
- let rowAttrs = getRowAttributes(rowValue, rowIndex);
932
- let nodes: React.ReactNode[] = [];
933
- nodes.push(
934
- <tr
935
- key={reactKey}
936
- {...rowAttrs}
937
- style={{ ...(rowAttrs.style || {}), gridTemplateColumns }}
938
- className={`${isSelected ? Styles.ns_selected : ""} ${isLastSelected ? Styles.ns_last_selected : ""}`.trim()}
939
- onClick={(e) => !isCheckboxDisabled && onRowClick(e, row, isSelected)}
940
- >
941
- {
942
- this.props.checkbox &&
943
- <td data-cell-type="checkbox">
944
- <label
945
- htmlFor={`checkbox_device_${rowKey}`}
946
- className={Styles.ns_check_box_label}
947
- >
948
- <input
949
- id={`checkbox_device_${rowKey}`}
950
- type="checkbox"
951
- checked={isSelected}
952
- disabled={isCheckboxDisabled}
953
- className={Styles.ns_check_box}
954
- onChange={() => { }}
955
- />
956
- </label>
957
- </td>
958
- }
959
- {
960
- this.getColumns(true).map((column, index) =>
961
- {
962
- column.index = index + (this.props.checkbox ? 1 : 0);
963
- let cell = this.getCellTableInfo(column, row, false);
964
- let isFirstDataColumn = index === 0;
965
- return (
966
- <td
967
- key={column.index}
968
- {...getColumnAttributes(column)}
969
- data-label={`${column.text}: `}
970
- width={column.formatter.width}
971
- className=''
972
- style={isFirstDataColumn && hierarchical ? { overflow: "visible" } : undefined}
973
- onClick={(e) =>
974
- {
975
- onColumnClick(e, column);
976
- onCellClick(e, cell);
977
- }}
978
- >
979
- {isFirstDataColumn && hierarchical ? (
980
- <div
981
- style={{
982
- display: "flex",
983
- alignItems: "center",
984
- }}
985
- >
986
- {Array.from({ length: depth }).map((_, i) =>
987
- {
988
- let isLastLevel = i === depth - 1;
989
- let drawVerticalTop = i < depth - 1 ? ancestorHasNext[i] : true;
990
- let drawVerticalBottom = i < depth - 1 ? ancestorHasNext[i] : !isLastChild;
991
- let drawHorizontal = isLastLevel;
992
- return (
993
- <span
994
- key={i}
995
- style={{
996
- position: "relative",
997
- display: "inline-block",
998
- width: INDENT,
999
- alignSelf: "stretch",
1000
- flex: "0 0 auto",
1001
- }}
1002
- >
1003
- {drawVerticalTop && (
1004
- <span style={{
1005
- position: "absolute",
1006
- left: INDENT / 2,
1007
- top: -24,
1008
- height: "calc(50% + 24px)",
1009
- width: 1,
1010
- background: lineColor,
1011
- }} />
1012
- )}
1013
- {drawVerticalBottom && (
1014
- <span style={{
1015
- position: "absolute",
1016
- left: INDENT / 2,
1017
- top: "50%",
1018
- height: "calc(50% + 24px)",
1019
- width: 1,
1020
- background: lineColor,
1021
- }} />
1022
- )}
1023
- {drawHorizontal && (
1024
- <span style={{
1025
- position: "absolute",
1026
- left: INDENT / 2,
1027
- top: "50%",
1028
- width: INDENT / 2,
1029
- height: 1,
1030
- background: lineColor,
1031
- }} />
1032
- )}
1033
- </span>
1034
- );
1035
- })}
1036
- <span
1037
- data-cell-type="tree-toggle"
1038
- style={{
1039
- display: "inline-flex",
1040
- alignItems: "center",
1041
- justifyContent: "center",
1042
- marginRight: 6,
1043
- }}
1044
- onClick={(e) => e.stopPropagation()}
1045
- >
1046
- {hasChildren ? (
1047
- <button
1048
- type="button"
1049
- onClick={(e) =>
1050
- {
1051
- e.stopPropagation();
1052
- this.toggleExpanded(rowKey);
1053
- }}
1054
- style={{
1055
- width: 18,
1056
- height: 18,
1057
- lineHeight: "16px",
1058
- padding: 0,
1059
- border: "1px solid rgba(20, 27, 92, 0.4)",
1060
- borderRadius: 3,
1061
- background: "#fff",
1062
- cursor: "pointer",
1063
- fontSize: 14,
1064
- fontWeight: "bold",
1065
- color: "rgba(20, 27, 92, 1)",
1066
- }}
1067
- aria-label={expanded ? "Collapse" : "Expand"}
1068
- >
1069
- {expanded ? "−" : "+"}
1070
- </button>
1071
- ) : (
1072
- <span style={{ display: "inline-block", width: 18, height: 18 }} />
1073
- )}
1074
- </span>
1075
- {cell.formatted ?? ""}
1076
- </div>
1077
- ) : (
1078
- <>
1079
- {cell.formatted ?? ""}
1080
- </>
1081
- )}
1082
- </td>
1083
- );
1084
- })
1085
- }
1086
- </tr>
1087
- );
1088
- if (hierarchical && hasChildren && expanded)
1089
- {
1090
- let childAncestors = [...ancestorHasNext, !isLastChild];
1091
- children.forEach((child, ci2) =>
1092
- {
1093
- let ci = indexOfRow.get(child) ?? -1;
1094
- let lastChild = ci2 === children.length - 1;
1095
- renderRow(child, ci, depth + 1, reactKey, childAncestors, lastChild).forEach(n => nodes.push(n));
1096
- });
1097
- }
1098
- return nodes;
1099
- };
1100
- let topLevel: RowType[];
1101
- if (hierarchical && this.props.hierarchy)
1102
- {
1103
- let parentCol = this.props.hierarchy.parent;
1104
- let ids = new Set<string>();
1105
- displayRows.forEach(r => ids.add(this.getRowIdValue(r) + "" + this.getGroupKey(r)));
1106
- topLevel = displayRows.filter(r =>
1107
- {
1108
- let pid = (r as any)[parentCol];
1109
- let pidKey = pid === null || pid === undefined ? "" : String(pid);
1110
- if (pidKey === "")
1111
- return true;
1112
- return !ids.has(pidKey + "" + this.getGroupKey(r));
1113
- });
1114
- }
1115
- else
1116
- {
1117
- topLevel = displayRows;
1118
- }
1119
- content = topLevel.flatMap((rowValue, i) => renderRow(rowValue, indexOfRow.get(rowValue) ?? 0, 0, "", [], i === topLevel.length - 1));
1120
- }
1121
-
1122
- let getSortSign = (column: TableColumnInfo) =>
1123
- {
1124
- if (this.state.sortItems)
1125
- {
1126
- let c = this.state.sortItems.filter(c => c.table.name === column.table.name && c.column.name === column.name)[0]
1127
- if (c)
1128
- {
1129
- if (c.ascending)
1130
- return "↑"
1131
- return "↓";
1132
- }
1133
- }
1134
- return "";
1135
- };
1136
-
1137
- let buttonPanel = show_button_panel && (
1138
- <div className={Styles.ns_button_panel}>
1139
- {
1140
- this.state.pageSelectionMode !== PageSelectionMode.Hidden &&
1141
- <NSDialogPageSelection<RowType>
1142
- icon={
1143
- this.state.pageSelectionMode === PageSelectionMode.Print ? "https://static.namirasoft.com/image/concept/print/blue.svg" :
1144
- this.state.pageSelectionMode === PageSelectionMode.CSV ? "https://static.namirasoft.com/image/concept/export/blue.svg" : ""
1145
- }
1146
- current_page={this.NSPagination.current?.getCurrentPage() ?? 1}
1147
- current_size={this.NSPagination.current?.getPageSize() ?? 0}
1148
- max_page={this.state.totalItems}
1149
- getItems={async (page, size, selected) =>
1150
- {
1151
- if (selected)
1152
- {
1153
- let rows: RowType[] = [];
1154
- if (this.state.rows)
1155
- for (let i = 0; i < this.state.rows.length; i++)
1156
- {
1157
- const row = { index: i, value: this.state.rows[i] };
1158
- let id = this.props.getRowKey(row);
1159
- if (this.state.selectedIDs.includes(id))
1160
- rows.push(row.value);
1161
- }
1162
- return rows;
1163
- }
1164
- let res = await this.props.getRows(page, size, this.getSortItems());
1165
- return res.rows;
1166
- }}
1167
- onFinish={async (items: RowType[], expandAll: boolean) =>
1168
- {
1169
- if (this.state.pageSelectionMode === PageSelectionMode.Print)
1170
- this.print(items, expandAll);
1171
- else if (this.state.pageSelectionMode === PageSelectionMode.CSV)
1172
- this.exportCSV(items);
1173
- }}
1174
- showExpandAll={this.state.pageSelectionMode === PageSelectionMode.Print && !!this.props.hierarchy}
1175
- onClose={() => this.setState({ pageSelectionMode: PageSelectionMode.Hidden })}
1176
- />
1177
- }
1178
- {
1179
- this.props.hierarchy && isButtonEnabled("expand") &&
1180
- <NSButton
1181
- attributes={{ title: this.isAnyExpanded() ? 'Collapse All' : 'Expand All' }}
1182
- icon={{ src: "https://static.namirasoft.com/image/concept/hierarchy/blue.svg", alt: "expand" }}
1183
- onClick={{
1184
- action: this.toggleExpandAll,
1185
- showLoading: false
1186
- }}
1187
- classList={[Styles.ns_table_button]}
1188
- />
1189
- }
1190
- {
1191
- isButtonEnabled("reorder") &&
1192
- <NSButton
1193
- attributes={{ title: 'Reorder' }}
1194
- icon={{ src: "https://static.namirasoft.com/image/concept/grip-vertical/blue.svg", alt: "reorder" }}
1195
- onClick={{
1196
- action: () => this.setState({ showReorderDialog: true }),
1197
- showLoading: false
1198
- }}
1199
- classList={[Styles.ns_table_button]}
1200
- />
1201
- }
1202
- {
1203
- isButtonEnabled("sort") && this.isSortEnabled() &&
1204
- <NSButton
1205
- attributes={{ title: 'Sort' }}
1206
- icon={{ src: "https://static.namirasoft.com/image/concept/arrow-down-short-wide/blue.svg", alt: "sort" }}
1207
- onClick={{
1208
- action: () => this.setState({ showSortDialog: true }),
1209
- showLoading: false
1210
- }}
1211
- classList={[Styles.ns_table_button]}
1212
- />
1213
- }
1214
- {
1215
- this.props.buttons?.map((button, index) => (
1216
- <NSButton
1217
- key={index}
1218
- {...button}
1219
- classList={[Styles.ns_table_button, ...(button.classList ?? [])]}
1220
- />
1221
- ))
1222
- }
1223
- {
1224
- isButtonEnabled("fullscreen") &&
1225
- <NSButton
1226
- icon={{ src: `https://static.namirasoft.com/image/concept/full-screen/blue.svg` }}
1227
- classList={[Styles.ns_table_button, Styles.ns_fullscreen_button]}
1228
- onClick={{
1229
- action: this.toggleFullscreen,
1230
- showLoading: false
1231
- }}
1232
- attributes={{ title: this.state.fullscreen ? 'Minimize' : 'Maximize' }}
1233
- />
1234
- }
1235
- {
1236
- isButtonEnabled("chart") &&
1237
- <NSButton
1238
- attributes={{ title: 'Chart' }}
1239
- icon={{ src: "https://static.namirasoft.com/image/concept/chart/blue.svg" }}
1240
- onClick={{
1241
- action: () =>
1242
- {
1243
- if (!this.state.rows || this.state.rows.length === 0)
1244
- this.showModal(" ", "There is no data to display as a chart.");
1245
- else
1246
- this.setState({ showChart: true });
1247
- },
1248
- showLoading: false
1249
- }}
1250
- classList={[Styles.ns_table_button]}
1251
- />
1252
- }
1253
- {
1254
- isButtonEnabled("export") &&
1255
- <NSButton
1256
- icon={{ src: "https://static.namirasoft.com/image/concept/export/blue.svg" }}
1257
- onClick={{
1258
- action: () => { this.setState({ pageSelectionMode: PageSelectionMode.CSV }); },
1259
- showLoading: false
1260
- }}
1261
- classList={[Styles.ns_table_button]}
1262
- attributes={{ title: 'Export' }}
1263
- />
1264
- }
1265
- {
1266
- isButtonEnabled("print") &&
1267
- <NSButton
1268
- attributes={{ title: 'Print' }}
1269
- icon={{ src: "https://static.namirasoft.com/image/concept/print/blue.svg" }}
1270
- onClick={{
1271
- action: () => { this.setState({ pageSelectionMode: PageSelectionMode.Print }); },
1272
- showLoading: false
1273
- }}
1274
- classList={[Styles.ns_table_button]}
1275
- />
1276
- }
1277
- {
1278
- isButtonEnabled("refresh") &&
1279
- <NSButton
1280
- attributes={{ title: 'Refresh' }}
1281
- icon={{ src: "https://static.namirasoft.com/image/concept/refresh/blue.svg" }}
1282
- onClick={{
1283
- action: async (onFinished) =>
1284
- {
1285
- this.reload(null, null).then(onFinished).catch(onFinished);
1286
- }
1287
- }}
1288
- classList={[Styles.ns_table_button]}
1289
- style={{ border: '1px solid rgb(3, 119, 255)' }}
1290
- />
1291
- }
1292
- </div>
1293
- );
1294
-
1295
- return (
1296
- <div
1297
- id={this.props.id}
1298
- className={[...this.props.classList ?? [], Styles.ns_table_wrapper, this.state.fullscreen ? Styles.ns_fullscreen : ""].filter(Boolean).join(" ")}
1299
- style={{ ...this.props.style }}
1300
- >
1301
- {
1302
- ((!this.state.fullscreen && this.props.headerChildren) || show_pagination || buttonPanel) &&
1303
- <div className={Styles.ns_table_topbar}>
1304
- {
1305
- !this.state.fullscreen &&
1306
- this.props.headerChildren
1307
- }
1308
- {
1309
- show_pagination &&
1310
- <NSPagination ref={this.NSPagination} totalItems={this.state.totalItems} onPageChange={this.onPageChange} />
1311
- }
1312
- {buttonPanel}
1313
- </div>
1314
- }
1315
- {
1316
- !this.state.fullscreen && this.props.children &&
1317
- <div className={Styles.ns_table_children}>
1318
- {this.props.children}
1319
- </div>
1320
- }
1321
- <table
1322
- ref={this.Table_Ref}
1323
- className={Styles.ns_table}
1324
- >
1325
- <thead>
1326
- <tr style={{ gridTemplateColumns }}>
1327
- {
1328
- this.props.checkbox &&
1329
- <th data-cell-type="checkbox" className="d-flex">
1330
- <label
1331
- htmlFor='checkbox'
1332
- className={Styles.ns_checkbox_label}
1333
- >
1334
- <input
1335
- id="checkbox"
1336
- type="checkbox"
1337
- className={`${Styles.ns_check_box} ${this.isSomeSelected() ? Styles.ns_indeterminate : ""}`}
1338
- checked={this.getSelectedIDs().length > 0}
1339
- onChange={this.toggleAllSelections}
1340
- ></input>
1341
- </label>
1342
- </th>
1343
- }
1344
- {
1345
- this.getColumns(true).map(column =>
1346
- <th
1347
- key={column.name}
1348
- scope="col"
1349
- onMouseDown={(e) =>
1350
- {
1351
- if ((e.target as HTMLElement).closest('[data-resize-handle]'))
1352
- return;
1353
- if (!this.isSortEnabled())
1354
- return;
1355
- let t = new BaseMetaTable(null, column.table.name, column.table.text);
1356
- let c = new BaseMetaColumn(t, column.name, column.text, "", false);
1357
- let items = this.getSortItems();
1358
- if (!items || items.length === 0 || items[0].table.name !== t.name || items[0].column.name !== c.name)
1359
- this.setSortItems([new SortItem(t, c, true)]);
1360
- else
1361
- {
1362
- if (items[0].ascending)
1363
- this.setSortItems([new SortItem(t, c, false)]);
1364
- else
1365
- this.setSortItems(null);
1366
- }
1367
- }}
1368
- style={{ width: this.getColumnWidth(column) }}
1369
- >
1370
- <span className={Styles.ns_th_title}>
1371
- {column.text + " " + getSortSign(column)}
1372
- </span>
1373
- <div
1374
- data-resize-handle="true"
1375
- className={Styles.ns_resize_handle}
1376
- onMouseDown={(e) => this.startResize(e, column)}
1377
- onClick={(e) => { e.stopPropagation() }}
1378
- />
1379
- </th>
1380
- )
1381
- }
1382
- </tr>
1383
- </thead>
1384
-
1385
- <tbody className={Styles.ns_tbody}>
1386
- {content}
1387
- </tbody>
1388
- </table>
1389
-
1390
- {
1391
- this.state.model.show &&
1392
- <NSDialogInfo
1393
- description={this.state.model.description}
1394
- onClose={() => { this.hideModal(); }}
1395
- title={this.state.title}
1396
- show_as_code={true}
1397
- >
1398
- <></>
1399
- </NSDialogInfo>
1400
- }
1401
- {
1402
- this.state.showChart &&
1403
- <NSTableChart<RowType>
1404
- name={this.props.name}
1405
- columns={this.getColumns(true)}
1406
- rows={this.state.rows ?? []}
1407
- selectedRows={this.state.selectedRows}
1408
- onClose={() => this.setState({ showChart: false })}
1409
- />
1410
- }
1411
- {
1412
- this.state.showSortDialog && this.isSortEnabled() &&
1413
- <NSSortDialog
1414
- sorts={this.getSortItems()}
1415
- getColumns={() => this.getColumns(null)}
1416
- onCancel={() => this.setState({ showSortDialog: false })}
1417
- onApply={sorts =>
1418
- {
1419
- this.setSortItems(sorts);
1420
- this.setState({ showSortDialog: false });
1421
- }}
1422
- />
1423
- }
1424
- {
1425
- this.state.showReorderDialog &&
1426
- <NSReorderDialog
1427
- visibleColumnKeys={this.getVisibleColumns().map(c => c.table + "." + c.column)}
1428
- getColumns={() => this.getColumns(null)}
1429
- onCancel={() => this.setState({ showReorderDialog: false })}
1430
- onApply={orderedKeys =>
1431
- {
1432
- const all = this.getColumns(null);
1433
- const cs = orderedKeys
1434
- .map(k => all.find(c => c.table.name + "." + c.name === k))
1435
- .filter((c): c is TableColumnInfo => !!c);
1436
- this.setVisibleColumns(cs.map(c => ({ table: c.table.name, column: c.name })));
1437
- this.setState({ showReorderDialog: false });
1438
- }}
1439
- />
1440
- }
1441
- </div>
1442
- );
1443
- }
1
+ "use client";
2
+ import { BaseMetaColumn, BaseMetaTable, IStorageLocal, SortItem } from "namirasoft-core";
3
+ import { Component, createRef, ReactNode } from "react";
4
+ import { utils, writeFile } from 'xlsx';
5
+ import { BaseColumnFormatter } from '../formatter/BaseColumnFormatter';
6
+ import { IBaseComponentProps } from "../props/IBaseComponentProps";
7
+ import { NSButton, NSButtonProps } from './NSButton';
8
+ import { NSDialogInfo } from './NSDialogInfo';
9
+ import { NSDialogPageSelection } from './NSDialogPageSelection';
10
+ import { NSLoading } from './NSLoading';
11
+ import { NSNoData } from './NSNoData';
12
+ import { NSPagination } from './NSPagination';
13
+ import { NSReorderDialog } from './NSReorderDialog';
14
+ import { NSSortDialog } from './NSSortDialog';
15
+ import { NSTableChart } from './NSTableChart';
16
+ import Styles from './NSTable.module.css';
17
+
18
+ export interface TableInfo
19
+ {
20
+ name: string;
21
+ text: string;
22
+ }
23
+
24
+ export interface TableColumnInfo
25
+ {
26
+ table: TableInfo;
27
+ index: number;
28
+ type: string;
29
+ name: string;
30
+ text: string;
31
+ formatter: BaseColumnFormatter;
32
+ group?: string;
33
+ defaults?: {
34
+ hidden?: boolean;
35
+ };
36
+ }
37
+
38
+ export interface TableRowInfo<RowType>
39
+ {
40
+ index: number;
41
+ value: RowType;
42
+ }
43
+
44
+ export interface TableCellInfo<RowType>
45
+ {
46
+ column: TableColumnInfo;
47
+ row: TableRowInfo<RowType>;
48
+ value: any;
49
+ formatted: any;
50
+ }
51
+
52
+ interface PrintableRow<RowType>
53
+ {
54
+ value: RowType;
55
+ index: number;
56
+ depth: number;
57
+ ancestorHasNext: boolean[];
58
+ isLastChild: boolean;
59
+ }
60
+
61
+ export interface NSTableProps<RowType> extends IBaseComponentProps
62
+ {
63
+ name: string;
64
+ checkbox: boolean;
65
+ columns: TableColumnInfo[];
66
+ getRows: (page: number, size: number, sorts: SortItem[]) => Promise<{ rows: RowType[], count: number }>;
67
+ getRowKey: (row: TableRowInfo<RowType>) => string;
68
+ getRowAttributes?: (row_value: RowType, row_index: number) => { [key: string]: any };
69
+ getColumnAttributes?: (column: TableColumnInfo) => { [key: string]: any };
70
+ getCellFormattedValue?: (value: any, column: TableColumnInfo, row: TableRowInfo<RowType>, printable: boolean) => any;
71
+ onColumnClick?: (e: React.MouseEvent<HTMLTableCellElement, MouseEvent>, column: TableColumnInfo) => void;
72
+ onRowClick?: (e: React.MouseEvent<HTMLTableRowElement, MouseEvent>, row: TableRowInfo<RowType>) => void;
73
+ onCellClick?: (e: React.MouseEvent<HTMLTableCellElement, MouseEvent>, cell: TableCellInfo<RowType>) => boolean | undefined;
74
+ getRowCheckboxDisabled?: (row: TableRowInfo<RowType>) => boolean;
75
+ buttons?: NSButtonProps[];
76
+ children?: ReactNode;
77
+ headerChildren?: ReactNode;
78
+ hierarchy?: { id: string, parent: string, group?: string[] };
79
+ defaultExpanded?: boolean;
80
+ onSelectedIDsChanged?: () => void;
81
+ onSortChanged?: (items: SortItem[] | null) => void;
82
+ onVisibleColumnsChanged?: (visible_columns: { table: string, column: string }[]) => void;
83
+ sort?: NSTableSortConfig;
84
+ ui?: {
85
+ pagination?: boolean;
86
+ button_panel?: boolean | NSTableButtonPanelConfig;
87
+ };
88
+ }
89
+
90
+ export interface NSTableSortConfig
91
+ {
92
+ enabled?: boolean;
93
+ onSort?: (items: SortItem[] | null) => void;
94
+ }
95
+
96
+ export type NSTableButtonKey = "reorder" | "sort" | "fullscreen" | "chart" | "export" | "print" | "refresh" | "expand";
97
+ export type NSTableButtonPanelConfig = { [key in NSTableButtonKey]?: boolean };
98
+
99
+ interface NSTableState<RowType>
100
+ {
101
+ columns: TableColumnInfo[];
102
+ visible_columns: { table: string, column: string }[];
103
+ totalItems: number;
104
+ rows: RowType[] | null;
105
+ title?: string;
106
+ model: {
107
+ show: boolean;
108
+ description?: string;
109
+ };
110
+ selectedIDs: string[];
111
+ selectedRows: RowType[];
112
+ expandedIDs: string[];
113
+ pageSelectionMode: PageSelectionMode;
114
+ sortItems: SortItem[] | null;
115
+ columnWidths: { [key: string]: number };
116
+ fullscreen: boolean;
117
+ showChart: boolean;
118
+ showSortDialog: boolean;
119
+ showReorderDialog: boolean;
120
+ }
121
+
122
+ enum PageSelectionMode
123
+ {
124
+ Hidden = "Hidden",
125
+ Print = "Print",
126
+ CSV = "CSV"
127
+ }
128
+
129
+ export class NSTable<RowType> extends Component<NSTableProps<RowType>, NSTableState<RowType>>
130
+ {
131
+ private Table_Ref = createRef<HTMLTableElement>();
132
+ NSPagination = createRef<NSPagination>();
133
+
134
+ private LastSelectedRowID: number | null = null;
135
+ private resizeMoveHandler: ((e: MouseEvent) => void) | null = null;
136
+ private resizeUpHandler: (() => void) | null = null;
137
+
138
+ constructor(props: NSTableProps<RowType>)
139
+ {
140
+ super(props);
141
+ this.state = {
142
+ columns: props.columns,
143
+ visible_columns: this.getVisibleColumns(),
144
+ rows: null,
145
+ totalItems: 0,
146
+ model: { show: false },
147
+ selectedIDs: [],
148
+ selectedRows: [],
149
+ expandedIDs: [],
150
+ pageSelectionMode: PageSelectionMode.Hidden,
151
+ sortItems: this.getSortItems(),
152
+ columnWidths: this.loadColumnWidths(),
153
+ fullscreen: false,
154
+ showChart: false,
155
+ showSortDialog: false,
156
+ showReorderDialog: false,
157
+ };
158
+ this.getSortItemsKey = this.getSortItemsKey.bind(this);
159
+ this.getSortItems = this.getSortItems.bind(this);
160
+ this.setSortItems = this.setSortItems.bind(this);
161
+ this.getVisibleColumnsKey = this.getVisibleColumnsKey.bind(this);
162
+ this.getVisibleColumns = this.getVisibleColumns.bind(this);
163
+ this.setVisibleColumns = this.setVisibleColumns.bind(this);
164
+ this.setColumns = this.setColumns.bind(this);
165
+ this.setColumns = this.setColumns.bind(this);
166
+ this.setRows = this.setRows.bind(this);
167
+ this.foreachColumn = this.foreachColumn.bind(this);
168
+ this.getColumns = this.getColumns.bind(this);
169
+ this.showModal = this.showModal.bind(this);
170
+ this.hideModal = this.hideModal.bind(this);
171
+ this.onPageChange = this.onPageChange.bind(this);
172
+ this.isSomeSelected = this.isSomeSelected.bind(this);
173
+ this.toggleAllSelections = this.toggleAllSelections.bind(this);
174
+ this.print = this.print.bind(this);
175
+ this.exportCSV = this.exportCSV.bind(this);
176
+ this.isAllSelected = this.isAllSelected.bind(this);
177
+ this.reload = this.reload.bind(this);
178
+ this.toggleFullscreen = this.toggleFullscreen.bind(this);
179
+ this.toggleExpandAll = this.toggleExpandAll.bind(this);
180
+ }
181
+ private toggleFullscreen()
182
+ {
183
+ this.setState({ fullscreen: !this.state.fullscreen });
184
+ }
185
+ private getSortItemsKey(): string
186
+ {
187
+ return "ns_table_" + this.props.name + "_sort_items";
188
+ }
189
+ getSortItems(): SortItem[]
190
+ {
191
+ let storage = new IStorageLocal();
192
+ let item = storage.get(this.getSortItemsKey(), "");
193
+ try
194
+ {
195
+ return SortItem.parse(item);
196
+ } catch (error)
197
+ {
198
+ }
199
+ return [];
200
+ }
201
+ isSortEnabled(): boolean
202
+ {
203
+ return this.props.sort?.enabled ?? true;
204
+ }
205
+ private hasCustomSort(): boolean
206
+ {
207
+ return !!this.props.sort?.onSort;
208
+ }
209
+ setSortItems(items: SortItem[] | null)
210
+ {
211
+ if (!this.isSortEnabled())
212
+ return;
213
+ let storage = new IStorageLocal();
214
+ storage.set(this.getSortItemsKey(), SortItem.stringify(items));
215
+ this.setState({ sortItems: items }, () =>
216
+ {
217
+ this.props.onSortChanged?.(items);
218
+ this.props.sort?.onSort?.(items);
219
+ });
220
+ }
221
+ private compareRowsBySortItems(a: RowType, b: RowType, items: SortItem[]): number
222
+ {
223
+ for (let item of items)
224
+ {
225
+ let column = this.state.columns.find(c => c.table.name === item.table.name && c.name === item.column.name);
226
+ let va = (a as any)[item.column.name];
227
+ let vb = (b as any)[item.column.name];
228
+ let cmp = column ? column.formatter.compare(va, vb) : BaseColumnFormatter.smartCompare(va, vb);
229
+ if (cmp !== 0)
230
+ return item.ascending ? cmp : -cmp;
231
+ }
232
+ return 0;
233
+ }
234
+ private sortRowsClient(rows: RowType[]): RowType[]
235
+ {
236
+ let items = this.state.sortItems;
237
+ if (!items || items.length === 0)
238
+ return rows;
239
+ let decorated = rows.map((row, index) => ({ row, index }));
240
+ decorated.sort((x, y) =>
241
+ {
242
+ let cmp = this.compareRowsBySortItems(x.row, y.row, items!);
243
+ return cmp !== 0 ? cmp : x.index - y.index;
244
+ });
245
+ return decorated.map(d => d.row);
246
+ }
247
+ private getDisplayRows(): RowType[] | null
248
+ {
249
+ let rows = this.state.rows;
250
+ if (!rows || !this.isSortEnabled() || this.hasCustomSort())
251
+ return rows;
252
+ return this.sortRowsClient(rows);
253
+ }
254
+ private getVisibleColumnsKey(): string
255
+ {
256
+ return "ns_table_" + this.props.name + "_visible_columns";
257
+ }
258
+ getVisibleColumns(): { table: string, column: string }[]
259
+ {
260
+ let storage = new IStorageLocal();
261
+ let item = storage.get(this.getVisibleColumnsKey(), "");
262
+ try
263
+ {
264
+ return JSON.parse(item);
265
+ } catch (error)
266
+ {
267
+ }
268
+ return [];
269
+ }
270
+ setVisibleColumns(visible_columns: { table: string, column: string }[])
271
+ {
272
+ let storage = new IStorageLocal();
273
+ storage.set(this.getVisibleColumnsKey(), JSON.stringify(visible_columns));
274
+ this.setState({ visible_columns }, () =>
275
+ {
276
+ this.props.onVisibleColumnsChanged?.(visible_columns);
277
+ });
278
+ }
279
+ setColumns(columns: TableColumnInfo[])
280
+ {
281
+ this.setState({ columns });
282
+ }
283
+ setRows(rows: RowType[] | null, totalItems: number)
284
+ {
285
+ this.setState({ rows, totalItems });
286
+ }
287
+ foreachColumn(visible: boolean | null, handler: (column: TableColumnInfo) => void)
288
+ {
289
+ let vs = this.getVisibleColumns();
290
+ let shouldBeVisible = (c: TableColumnInfo) =>
291
+ {
292
+ if (vs.length === 0)
293
+ return !c.defaults?.hidden;
294
+ return vs.filter(v => v.table === c.table.name && v.column === c.name).length > 0;
295
+ };
296
+ this.state.columns.forEach(c =>
297
+ {
298
+ if (visible === null || shouldBeVisible(c) === visible)
299
+ handler(c);
300
+ })
301
+ }
302
+ getColumns(visible: boolean | null): TableColumnInfo[]
303
+ {
304
+ let columns: TableColumnInfo[] = [];
305
+ this.foreachColumn(visible, c => columns.push(c));
306
+ if (visible === true)
307
+ {
308
+ let vs = this.getVisibleColumns();
309
+ if (vs.length > 0)
310
+ columns.sort((a, b) =>
311
+ {
312
+ let ai = vs.findIndex(v => v.table === a.table.name && v.column === a.name);
313
+ let bi = vs.findIndex(v => v.table === b.table.name && v.column === b.name);
314
+ return ai - bi;
315
+ });
316
+ }
317
+ return columns;
318
+ }
319
+ private showModal(description: string, title?: string)
320
+ {
321
+ this.setState({ model: { show: true, description }, title });
322
+ }
323
+ private hideModal()
324
+ {
325
+ this.setState({ model: { show: false } });
326
+ }
327
+ private onPageChange(page: number, size: number)
328
+ {
329
+ this.reload(page, size);
330
+ }
331
+ public async reload(page: number | null, size: number | null)
332
+ {
333
+ this.setRows(null, 0);
334
+ if (page === null)
335
+ page = this.NSPagination.current?.getCurrentPage() ?? 0;
336
+ if (size === null)
337
+ size = this.NSPagination.current?.getPageSize() ?? 0;
338
+ try
339
+ {
340
+ let sorts = this.getSortItems();
341
+ let res = await this.props.getRows(page, size, sorts);
342
+ this.setRows(res.rows, res.count);
343
+ } catch (error)
344
+ {
345
+ }
346
+ }
347
+ private toggleAllSelections()
348
+ {
349
+ let selectedIDs: string[] = [];
350
+ let selectedRows: RowType[] = [];
351
+ this.LastSelectedRowID = null;
352
+
353
+ if (this.getSelectedIDs().length === 0)
354
+ {
355
+ this.state.rows?.forEach((row, rowIndex) =>
356
+ {
357
+ let rowInfo = { index: rowIndex, value: row };
358
+ if (this.props.getRowCheckboxDisabled?.(rowInfo))
359
+ return;
360
+ let id = this.props.getRowKey(rowInfo);
361
+ selectedIDs.push(id);
362
+ selectedRows.push(row);
363
+ })
364
+ }
365
+
366
+ this.setSelectedIDs(selectedIDs, selectedRows);
367
+ }
368
+ private escapeHtml(value: any): string
369
+ {
370
+ if (value === null || value === undefined)
371
+ return "";
372
+ let text = typeof value === "object" ? JSON.stringify(value) : String(value);
373
+ return text
374
+ .replace(/&/g, "&amp;")
375
+ .replace(/</g, "&lt;")
376
+ .replace(/>/g, "&gt;")
377
+ .replace(/"/g, "&quot;")
378
+ .replace(/'/g, "&#39;");
379
+ }
380
+ private getPrintableRows(rows: RowType[], expandAll: boolean): PrintableRow<RowType>[]
381
+ {
382
+ let result: PrintableRow<RowType>[] = [];
383
+ if (!this.props.hierarchy)
384
+ {
385
+ rows.forEach((value, index) => result.push({ value, index, depth: 0, ancestorHasNext: [], isLastChild: true }));
386
+ return result;
387
+ }
388
+ let childrenMap = this.buildChildrenMap(rows);
389
+ let indexOfRow = new Map<RowType, number>();
390
+ rows.forEach((row, index) => indexOfRow.set(row, index));
391
+ let parentCol = this.props.hierarchy.parent;
392
+ let ids = new Set<string>();
393
+ rows.forEach(row => ids.add(this.getRowIdValue(row) + "" + this.getGroupKey(row)));
394
+ let topLevel = rows.filter(row =>
395
+ {
396
+ let pid = (row as any)[parentCol];
397
+ let pidKey = pid === null || pid === undefined ? "" : String(pid);
398
+ if (pidKey === "")
399
+ return true;
400
+ return !ids.has(pidKey + "" + this.getGroupKey(row));
401
+ });
402
+ let walk = (value: RowType, depth: number, ancestorHasNext: boolean[], isLastChild: boolean) =>
403
+ {
404
+ let index = indexOfRow.get(value) ?? 0;
405
+ result.push({ value, index, depth, ancestorHasNext, isLastChild });
406
+ let rowKey = this.props.getRowKey({ index, value });
407
+ if (!expandAll && !this.isExpanded(rowKey))
408
+ return;
409
+ let children = childrenMap?.get(this.getRowChildLookupKey(value)) ?? [];
410
+ let childAncestors = [...ancestorHasNext, !isLastChild];
411
+ children.forEach((child, childIndex) => walk(child, depth + 1, childAncestors, childIndex === children.length - 1));
412
+ };
413
+ topLevel.forEach((row, index) => walk(row, 0, [], index === topLevel.length - 1));
414
+ return result;
415
+ }
416
+ private buildTreeGuides(entry: PrintableRow<RowType>): string
417
+ {
418
+ if (entry.depth === 0)
419
+ return "";
420
+ let indent = 16;
421
+ let color = "#000";
422
+ let overhang = 8;
423
+ let guides = "";
424
+ for (let i = 0; i < entry.depth; i++)
425
+ {
426
+ let center = indent / 2;
427
+ let lines = "";
428
+ if (i < entry.depth - 1)
429
+ {
430
+ if (entry.ancestorHasNext[i])
431
+ lines += `<span style="position: absolute; inset-inline-start: ${center}px; top: 0; height: 100%; width: 1px; background: ${color};"></span>`;
432
+ }
433
+ else
434
+ {
435
+ lines += `<span style="position: absolute; inset-inline-start: ${center}px; top: -${overhang}px; height: calc(50% + ${overhang}px); width: 1px; background: ${color};"></span>`;
436
+ if (!entry.isLastChild)
437
+ lines += `<span style="position: absolute; inset-inline-start: ${center}px; top: 50%; height: 50%; width: 1px; background: ${color};"></span>`;
438
+ lines += `<span style="position: absolute; inset-inline-start: ${center}px; top: 50%; width: ${center - 2}px; height: 1px; background: ${color};"></span>`;
439
+ }
440
+ guides += `<span style="position: relative; display: inline-block; width: ${indent}px; align-self: stretch; flex: 0 0 auto;">${lines}</span>`;
441
+ }
442
+ return guides;
443
+ }
444
+ private buildPrintDocument(rows: RowType[], expandAll: boolean): string
445
+ {
446
+ let columns = this.getColumns(true);
447
+ let totalWidth = columns.reduce((sum, column) => sum + this.getColumnWidth(column), 0) || 1;
448
+ let colgroup = columns
449
+ .map(column => `<col style="width: ${(this.getColumnWidth(column) / totalWidth * 100).toFixed(4)}%" />`)
450
+ .join("");
451
+ let head = columns
452
+ .map(column => `<th>${this.escapeHtml(column.text)}</th>`)
453
+ .join("");
454
+ let printable = this.getPrintableRows(rows, expandAll);
455
+ let body = printable
456
+ .map((entry, entryIndex) =>
457
+ {
458
+ let row = { index: entry.index, value: entry.value };
459
+ let next = printable[entryIndex + 1];
460
+ let previous = printable[entryIndex - 1];
461
+ let removeTop = entry.depth > 0 && !!previous;
462
+ let removeBottom = !!next && next.depth > 0;
463
+ let edgeStyle = `${removeTop ? "border-top: none;" : ""}${removeBottom ? "border-bottom: none;" : ""}`;
464
+ let cells = columns
465
+ .map((column, columnIndex) =>
466
+ {
467
+ let text = this.escapeHtml(this.getCellTableInfo(column, row, true).formatted);
468
+ if (columnIndex === 0 && this.props.hierarchy && entry.depth > 0)
469
+ {
470
+ let guides = this.buildTreeGuides(entry);
471
+ let overlay = `<span style="position: absolute; top: 0; bottom: 0; inset-inline-start: 8px; display: flex; align-items: stretch;">${guides}</span>`;
472
+ return `<td style="position: relative; padding-inline-start: ${8 + entry.depth * 16}px;${edgeStyle}">${overlay}${text}</td>`;
473
+ }
474
+ if (columnIndex === 0 && edgeStyle)
475
+ return `<td style="${edgeStyle}">${text}</td>`;
476
+ return `<td>${text}</td>`;
477
+ })
478
+ .join("");
479
+ return `<tr>${cells}</tr>`;
480
+ })
481
+ .join("");
482
+ let direction = document.documentElement.dir || "ltr";
483
+ return `
484
+ <!DOCTYPE html>
485
+ <html dir="${direction}">
486
+ <head>
487
+ <meta charset="utf-8" />
488
+ <title>${this.escapeHtml(this.props.name)}</title>
489
+ <style>
490
+ * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
491
+ body { font-family: sans-serif; margin: 16px; }
492
+ table { width: 100%; border-collapse: collapse; table-layout: fixed; }
493
+ th, td { border: 1px solid #000; padding: 8px; text-align: start; overflow-wrap: break-word; word-break: break-word; }
494
+ thead { display: table-header-group; }
495
+ tr { page-break-inside: avoid; }
496
+ </style>
497
+ </head>
498
+ <body>
499
+ <table>
500
+ <colgroup>${colgroup}</colgroup>
501
+ <thead><tr>${head}</tr></thead>
502
+ <tbody>${body}</tbody>
503
+ </table>
504
+ </body>
505
+ </html>
506
+ `;
507
+ }
508
+ print(rows: RowType[], expandAll: boolean = false)
509
+ {
510
+ let blob = new Blob([this.buildPrintDocument(rows, expandAll)], { type: "text/html" });
511
+ let url = URL.createObjectURL(blob);
512
+ let printWindow = window.open(url, "_blank", "height=800,width=800");
513
+ if (!printWindow)
514
+ {
515
+ URL.revokeObjectURL(url);
516
+ this.showModal("Please allow pop-ups to print this table.", this.props.name);
517
+ return;
518
+ }
519
+ printWindow.onload = () =>
520
+ {
521
+ printWindow!.focus();
522
+ printWindow!.onafterprint = () =>
523
+ {
524
+ printWindow!.close();
525
+ URL.revokeObjectURL(url);
526
+ };
527
+ printWindow!.print();
528
+ };
529
+ }
530
+ exportCSV(rows: RowType[])
531
+ {
532
+ let columns = this.getColumns(true);
533
+ let header = columns.map(column => column.text);
534
+ let data = (rows || []).map((rowValue, rowIndex) =>
535
+ {
536
+ let row = { index: rowIndex, value: rowValue };
537
+ return columns.map(column =>
538
+ {
539
+ let formatted = this.getCellTableInfo(column, row, true).formatted;
540
+ if (formatted === null || formatted === undefined)
541
+ return "";
542
+ return typeof formatted === "object" ? JSON.stringify(formatted) : formatted;
543
+ });
544
+ });
545
+ let worksheet = utils.aoa_to_sheet([header, ...data]);
546
+ worksheet["!cols"] = columns.map(column => ({ wpx: this.getColumnWidth(column) }));
547
+ let workbook = utils.book_new();
548
+ utils.book_append_sheet(workbook, worksheet, this.props.name);
549
+ writeFile(workbook, this.props.name + ".xlsx");
550
+ }
551
+
552
+ handleSelectRow(row: TableRowInfo<RowType>, selected: boolean, ctrlKey: boolean, shiftKey: boolean)
553
+ {
554
+ let selectedIDs = this.state.selectedIDs;
555
+ let selectedRows = this.state.selectedRows;
556
+ let rowID = this.props.getRowKey(row);
557
+
558
+ if (shiftKey)
559
+ {
560
+ selectedRows = this.getRowsInRange(this.LastSelectedRowID ?? 0, row.index);
561
+ selectedIDs = selectedRows.map(row => this.props.getRowKey({ index: -1, value: row }));
562
+ }
563
+ else if (selected)
564
+ {
565
+ if (this.isMultipleSelected() && !ctrlKey)
566
+ {
567
+ selectedIDs = [rowID];
568
+ selectedRows = [row.value];
569
+ this.LastSelectedRowID = row.index;
570
+ }
571
+ else
572
+ {
573
+ selectedIDs = selectedIDs.filter(x => x !== rowID);
574
+ selectedRows = selectedRows.filter(x => this.props.getRowKey({ index: -1, value: x }) !== rowID);
575
+ this.LastSelectedRowID = ctrlKey ? row.index : null;
576
+ }
577
+ }
578
+ else
579
+ {
580
+ if (ctrlKey)
581
+ {
582
+ selectedIDs.push(rowID);
583
+ selectedRows.push(row.value);
584
+ }
585
+ else
586
+ {
587
+ selectedIDs = [rowID];
588
+ selectedRows = [row.value];
589
+ }
590
+ this.LastSelectedRowID = row.index;
591
+ }
592
+
593
+ this.setSelectedIDs(selectedIDs, selectedRows);
594
+ }
595
+ getRowsInRange(fromIndex: number, toIndex: number)
596
+ {
597
+ let rows = this.getDisplayRows();
598
+ if (!rows)
599
+ return [];
600
+
601
+ let start = Math.min(fromIndex, toIndex);
602
+ let end = Math.max(fromIndex, toIndex);
603
+
604
+ return rows.slice(start, end + 1);
605
+ }
606
+ getSelectedIDs(): string[]
607
+ {
608
+ return this.state.selectedIDs;
609
+ }
610
+ getSelectedRows(): RowType[]
611
+ {
612
+ return this.state.selectedRows;
613
+ }
614
+ setSelectedIDs(ids: string[], rows: RowType[], callback?: () => void)
615
+ {
616
+ this.setState({ selectedIDs: ids, selectedRows: rows }, () =>
617
+ {
618
+ if (this.props.onSelectedIDsChanged)
619
+ this.props.onSelectedIDsChanged();
620
+ if (callback)
621
+ callback();
622
+ });
623
+ }
624
+ private getGroupKey(row: RowType): string
625
+ {
626
+ let h = this.props.hierarchy;
627
+ if (!h || !h.group || h.group.length === 0)
628
+ return "";
629
+ return h.group.map(g =>
630
+ {
631
+ let v = (row as any)[g];
632
+ return v === null || v === undefined ? "" : String(v);
633
+ }).join("");
634
+ }
635
+ private buildChildrenMap(rows: RowType[] | null): Map<string, RowType[]> | null
636
+ {
637
+ let h = this.props.hierarchy;
638
+ if (!h || !rows)
639
+ return null;
640
+ let map = new Map<string, RowType[]>();
641
+ for (let r of rows)
642
+ {
643
+ let pid = (r as any)[h.parent];
644
+ let pidKey = pid === null || pid === undefined ? "" : String(pid);
645
+ let key = pidKey + "" + this.getGroupKey(r);
646
+ let arr = map.get(key);
647
+ if (!arr) { arr = []; map.set(key, arr); }
648
+ arr.push(r);
649
+ }
650
+ return map;
651
+ }
652
+ private getRowIdValue(row: RowType): string
653
+ {
654
+ let h = this.props.hierarchy;
655
+ if (!h) return "";
656
+ let v = (row as any)[h.id];
657
+ return v === null || v === undefined ? "" : String(v);
658
+ }
659
+ private getRowChildLookupKey(row: RowType): string
660
+ {
661
+ return this.getRowIdValue(row) + "" + this.getGroupKey(row);
662
+ }
663
+ private isExpanded(rowKey: string): boolean
664
+ {
665
+ let inList = this.state.expandedIDs.includes(rowKey);
666
+ return this.props.defaultExpanded ? !inList : inList;
667
+ }
668
+ private toggleExpanded(rowKey: string)
669
+ {
670
+ let ids = this.state.expandedIDs;
671
+ if (ids.includes(rowKey))
672
+ ids = ids.filter(x => x !== rowKey);
673
+ else
674
+ ids = [...ids, rowKey];
675
+ this.setState({ expandedIDs: ids });
676
+ }
677
+ private getExpandableRowKeys(): string[]
678
+ {
679
+ let rows = this.state.rows;
680
+ if (!this.props.hierarchy || !rows)
681
+ return [];
682
+ let childrenMap = this.buildChildrenMap(rows);
683
+ if (!childrenMap)
684
+ return [];
685
+ let keys: string[] = [];
686
+ rows.forEach((value, index) =>
687
+ {
688
+ let children = childrenMap.get(this.getRowChildLookupKey(value)) ?? [];
689
+ if (children.length > 0)
690
+ keys.push(this.props.getRowKey({ index, value }));
691
+ });
692
+ return keys;
693
+ }
694
+ private isAnyExpanded(): boolean
695
+ {
696
+ return this.getExpandableRowKeys().some(key => this.isExpanded(key));
697
+ }
698
+ private toggleExpandAll()
699
+ {
700
+ let keys = this.getExpandableRowKeys();
701
+ let expand = !this.isAnyExpanded();
702
+ let inList = this.props.defaultExpanded ? !expand : expand;
703
+ this.setState({ expandedIDs: inList ? keys : [] });
704
+ }
705
+ private isAllSelected(): boolean
706
+ {
707
+ if (!this.state.rows || this.state.rows.length === 0)
708
+ return false;
709
+ const selectableCount = this.state.rows.filter((row, i) =>
710
+ !this.props.getRowCheckboxDisabled?.({ index: i, value: row })
711
+ ).length;
712
+ return selectableCount > 0 && this.state.selectedIDs.length === selectableCount;
713
+ }
714
+ private isSomeSelected(): boolean
715
+ {
716
+ return this.state.selectedIDs.length > 0 && !this.isAllSelected();
717
+ }
718
+ private isMultipleSelected(): boolean
719
+ {
720
+ return this.state.selectedIDs.length > 1;
721
+ }
722
+ private getCellTableInfo(column: TableColumnInfo, row: TableRowInfo<RowType>, printable: boolean)
723
+ {
724
+ let value = (row.value as any)[column.name];
725
+ let formatted = this.getCellFormattedValue(value, column, row, printable);
726
+ return { column, row, value, formatted };
727
+ }
728
+ private getCellFormattedValue(value: any, column: TableColumnInfo, row: TableRowInfo<RowType>, printable: boolean)
729
+ {
730
+ if (this.props.getCellFormattedValue)
731
+ return this.props.getCellFormattedValue(value, column, row, printable);
732
+ return column.formatter.format(value, column, row, printable);
733
+ }
734
+ private getColumnWidthsKey(): string
735
+ {
736
+ return "ns_table_" + this.props.name + "_column_widths";
737
+ }
738
+ private loadColumnWidths(): { [key: string]: number }
739
+ {
740
+ let storage = new IStorageLocal();
741
+ let item = storage.get(this.getColumnWidthsKey(), "");
742
+ try
743
+ {
744
+ return JSON.parse(item);
745
+ }
746
+ catch
747
+ {
748
+ return {};
749
+ }
750
+ }
751
+ private saveColumnWidths(widths: { [key: string]: number })
752
+ {
753
+ let storage = new IStorageLocal();
754
+ storage.set(this.getColumnWidthsKey(), JSON.stringify(widths));
755
+ }
756
+ private getColumnKey(column: TableColumnInfo): string
757
+ {
758
+ return column.table.name + "_" + column.name;
759
+ }
760
+ private getColumnWidth(column: TableColumnInfo): number
761
+ {
762
+ let key = this.getColumnKey(column);
763
+ if (this.state.columnWidths[key] !== undefined)
764
+ return this.state.columnWidths[key];
765
+ let w = column.formatter.width;
766
+ if (typeof w === 'number') return w;
767
+ if (typeof w === 'string')
768
+ {
769
+ let parsed = parseInt(w, 10);
770
+ if (!isNaN(parsed)) return parsed;
771
+ }
772
+ return 120;
773
+ }
774
+ private getGridTemplateColumns(): string
775
+ {
776
+ let parts: string[] = [];
777
+ if (this.props.checkbox)
778
+ parts.push("40px");
779
+ this.getColumns(true).forEach(column =>
780
+ {
781
+ parts.push(this.getColumnWidth(column) + "px");
782
+ });
783
+ return parts.join(" ");
784
+ }
785
+ private startResize(e: React.MouseEvent, column: TableColumnInfo)
786
+ {
787
+ e.preventDefault();
788
+ e.stopPropagation();
789
+ let key = this.getColumnKey(column);
790
+ let startX = e.clientX;
791
+ let startWidth = this.getColumnWidth(column);
792
+ let currentWidths = { ...this.state.columnWidths };
793
+ document.body.style.cursor = 'col-resize';
794
+ document.body.style.userSelect = 'none';
795
+ const onMouseMove = (ev: MouseEvent) =>
796
+ {
797
+ let delta = ev.clientX - startX;
798
+ let newWidth = Math.max(80, startWidth + delta);
799
+ currentWidths = { ...currentWidths, [key]: newWidth };
800
+ this.setState({ columnWidths: currentWidths });
801
+ };
802
+ const onMouseUp = () =>
803
+ {
804
+ document.body.style.cursor = '';
805
+ document.body.style.userSelect = '';
806
+ document.removeEventListener('mousemove', onMouseMove);
807
+ document.removeEventListener('mouseup', onMouseUp);
808
+ this.resizeMoveHandler = null;
809
+ this.resizeUpHandler = null;
810
+ this.saveColumnWidths(currentWidths);
811
+ };
812
+ this.resizeMoveHandler = onMouseMove;
813
+ this.resizeUpHandler = onMouseUp;
814
+ document.addEventListener('mousemove', onMouseMove);
815
+ document.addEventListener('mouseup', onMouseUp);
816
+ }
817
+ override componentDidMount(): void
818
+ {
819
+ if (!this.NSPagination.current)
820
+ this.reload(null, null);
821
+ }
822
+ override componentWillUnmount(): void
823
+ {
824
+ if (this.resizeMoveHandler)
825
+ document.removeEventListener('mousemove', this.resizeMoveHandler);
826
+ if (this.resizeUpHandler)
827
+ document.removeEventListener('mouseup', this.resizeUpHandler);
828
+ }
829
+ override render()
830
+ {
831
+
832
+ let show_pagination = this.props.ui?.pagination ?? true;
833
+ let button_panel_config = this.props.ui?.button_panel;
834
+ let show_button_panel = button_panel_config !== false;
835
+ let isButtonEnabled = (key: NSTableButtonKey): boolean =>
836
+ {
837
+ if (button_panel_config === undefined || typeof button_panel_config === "boolean")
838
+ return true;
839
+ return button_panel_config[key] ?? true;
840
+ };
841
+ let getRowAttributes = (row_value: RowType, row_index: number) =>
842
+ {
843
+ if (this.props.getRowAttributes)
844
+ return this.props.getRowAttributes(row_value, row_index);
845
+ return {};
846
+ }
847
+ let getColumnAttributes = (column: TableColumnInfo) =>
848
+ {
849
+ if (this.props.getColumnAttributes)
850
+ return this.props.getColumnAttributes(column);
851
+ return {};
852
+ }
853
+ let onColumnClick = (e: React.MouseEvent<HTMLTableCellElement, MouseEvent>, column: TableColumnInfo) =>
854
+ {
855
+ if ((e.target as HTMLElement).tagName !== "SPAN")
856
+ return;
857
+
858
+ if (this.props.onColumnClick)
859
+ this.props.onColumnClick(e, column);
860
+ }
861
+ let onRowClick = (e: React.MouseEvent<HTMLTableRowElement, MouseEvent>, row: TableRowInfo<RowType>, selected: boolean) =>
862
+ {
863
+ const target = e.target as HTMLElement;
864
+
865
+ if (target.tagName === "TR" || target.tagName === "TD" || target.closest("td")?.dataset.cellType === "checkbox")
866
+ this.handleSelectRow(row, selected, e.ctrlKey, e.shiftKey);
867
+
868
+ if (this.props.onRowClick)
869
+ this.props.onRowClick(e, row);
870
+ };
871
+ let onCellClick = (e: React.MouseEvent<HTMLTableCellElement, MouseEvent>, cell: TableCellInfo<RowType>) =>
872
+ {
873
+ if ((e.target as HTMLElement).tagName !== "SPAN")
874
+ return;
875
+
876
+ let overided = cell.column.formatter.onclick_overrided;
877
+ if (this.props.onCellClick)
878
+ overided = this.props.onCellClick(e, cell) ?? true;
879
+ if (!overided)
880
+ this.showModal(cell.formatted, cell.column.text);
881
+ };
882
+ let gridTemplateColumns = this.getGridTemplateColumns();
883
+ let content;
884
+ if (this.state.rows === null)
885
+ {
886
+ content = (
887
+ <tr className="d-flex justify-content-center align-items-center p-0">
888
+ <td className="w-100 h-100 m-0 mh-100">
889
+ <NSLoading
890
+ title="Loading table rows ..."
891
+ />
892
+ </td>
893
+ </tr>
894
+ );
895
+ }
896
+ else if (this.state.rows.length === 0)
897
+ {
898
+ content = (
899
+ <tr className="d-flex justify-content-center align-items-center p-0">
900
+ <td className="w-100 h-100 m-0 mh-100">
901
+ <NSNoData lable='No Data' classList={["p-3"]} />
902
+ </td>
903
+ </tr>
904
+ );
905
+ }
906
+ else
907
+ {
908
+ let displayRows = this.getDisplayRows() ?? [];
909
+ let hierarchical = !!this.props.hierarchy;
910
+ let childrenMap = this.buildChildrenMap(displayRows);
911
+ let indexOfRow = new Map<RowType, number>();
912
+ displayRows.forEach((r, i) => indexOfRow.set(r, i));
913
+ let INDENT = 16;
914
+ let lineColor = "rgba(20, 27, 92, 0.35)";
915
+ let renderRow = (rowValue: RowType, rowIndex: number, depth: number, parentKey: string, ancestorHasNext: boolean[], isLastChild: boolean): React.ReactNode[] =>
916
+ {
917
+ let row = { index: rowIndex, value: rowValue };
918
+ let rowKey = this.props.getRowKey(row);
919
+ let reactKey = parentKey ? `${parentKey}>${rowKey}` : rowKey;
920
+ let isSelected = this.state.selectedIDs.includes(rowKey);
921
+ let isLastSelected = this.LastSelectedRowID === row.index;
922
+ let isCheckboxDisabled = this.props.getRowCheckboxDisabled?.(row) ?? false;
923
+ let children: RowType[] = [];
924
+ if (hierarchical && childrenMap)
925
+ {
926
+ let lookupKey = this.getRowChildLookupKey(rowValue);
927
+ children = childrenMap.get(lookupKey) ?? [];
928
+ }
929
+ let hasChildren = children.length > 0;
930
+ let expanded = this.isExpanded(rowKey);
931
+ let rowAttrs = getRowAttributes(rowValue, rowIndex);
932
+ let nodes: React.ReactNode[] = [];
933
+ nodes.push(
934
+ <tr
935
+ key={reactKey}
936
+ {...rowAttrs}
937
+ style={{ ...(rowAttrs.style || {}), gridTemplateColumns }}
938
+ className={`${isSelected ? Styles.ns_selected : ""} ${isLastSelected ? Styles.ns_last_selected : ""}`.trim()}
939
+ onClick={(e) => !isCheckboxDisabled && onRowClick(e, row, isSelected)}
940
+ >
941
+ {
942
+ this.props.checkbox &&
943
+ <td data-cell-type="checkbox">
944
+ <label
945
+ htmlFor={`checkbox_device_${rowKey}`}
946
+ className={Styles.ns_check_box_label}
947
+ >
948
+ <input
949
+ id={`checkbox_device_${rowKey}`}
950
+ type="checkbox"
951
+ checked={isSelected}
952
+ disabled={isCheckboxDisabled}
953
+ className={Styles.ns_check_box}
954
+ onChange={() => { }}
955
+ />
956
+ </label>
957
+ </td>
958
+ }
959
+ {
960
+ this.getColumns(true).map((column, index) =>
961
+ {
962
+ column.index = index + (this.props.checkbox ? 1 : 0);
963
+ let cell = this.getCellTableInfo(column, row, false);
964
+ let isFirstDataColumn = index === 0;
965
+ return (
966
+ <td
967
+ key={column.index}
968
+ {...getColumnAttributes(column)}
969
+ data-label={`${column.text}: `}
970
+ width={column.formatter.width}
971
+ className=''
972
+ style={isFirstDataColumn && hierarchical ? { overflow: "visible" } : undefined}
973
+ onClick={(e) =>
974
+ {
975
+ onColumnClick(e, column);
976
+ onCellClick(e, cell);
977
+ }}
978
+ >
979
+ {isFirstDataColumn && hierarchical ? (
980
+ <div
981
+ style={{
982
+ display: "flex",
983
+ alignItems: "center",
984
+ }}
985
+ >
986
+ {Array.from({ length: depth }).map((_, i) =>
987
+ {
988
+ let isLastLevel = i === depth - 1;
989
+ let drawVerticalTop = i < depth - 1 ? ancestorHasNext[i] : true;
990
+ let drawVerticalBottom = i < depth - 1 ? ancestorHasNext[i] : !isLastChild;
991
+ let drawHorizontal = isLastLevel;
992
+ return (
993
+ <span
994
+ key={i}
995
+ style={{
996
+ position: "relative",
997
+ display: "inline-block",
998
+ width: INDENT,
999
+ alignSelf: "stretch",
1000
+ flex: "0 0 auto",
1001
+ }}
1002
+ >
1003
+ {drawVerticalTop && (
1004
+ <span style={{
1005
+ position: "absolute",
1006
+ left: INDENT / 2,
1007
+ top: -24,
1008
+ height: "calc(50% + 24px)",
1009
+ width: 1,
1010
+ background: lineColor,
1011
+ }} />
1012
+ )}
1013
+ {drawVerticalBottom && (
1014
+ <span style={{
1015
+ position: "absolute",
1016
+ left: INDENT / 2,
1017
+ top: "50%",
1018
+ height: "calc(50% + 24px)",
1019
+ width: 1,
1020
+ background: lineColor,
1021
+ }} />
1022
+ )}
1023
+ {drawHorizontal && (
1024
+ <span style={{
1025
+ position: "absolute",
1026
+ left: INDENT / 2,
1027
+ top: "50%",
1028
+ width: INDENT / 2,
1029
+ height: 1,
1030
+ background: lineColor,
1031
+ }} />
1032
+ )}
1033
+ </span>
1034
+ );
1035
+ })}
1036
+ <span
1037
+ data-cell-type="tree-toggle"
1038
+ style={{
1039
+ display: "inline-flex",
1040
+ alignItems: "center",
1041
+ justifyContent: "center",
1042
+ marginRight: 6,
1043
+ }}
1044
+ onClick={(e) => e.stopPropagation()}
1045
+ >
1046
+ {hasChildren ? (
1047
+ <button
1048
+ type="button"
1049
+ onClick={(e) =>
1050
+ {
1051
+ e.stopPropagation();
1052
+ this.toggleExpanded(rowKey);
1053
+ }}
1054
+ style={{
1055
+ width: 18,
1056
+ height: 18,
1057
+ lineHeight: "16px",
1058
+ padding: 0,
1059
+ border: "1px solid rgba(20, 27, 92, 0.4)",
1060
+ borderRadius: 3,
1061
+ background: "#fff",
1062
+ cursor: "pointer",
1063
+ fontSize: 14,
1064
+ fontWeight: "bold",
1065
+ color: "rgba(20, 27, 92, 1)",
1066
+ }}
1067
+ aria-label={expanded ? "Collapse" : "Expand"}
1068
+ >
1069
+ {expanded ? "−" : "+"}
1070
+ </button>
1071
+ ) : (
1072
+ <span style={{ display: "inline-block", width: 18, height: 18 }} />
1073
+ )}
1074
+ </span>
1075
+ {cell.formatted ?? ""}
1076
+ </div>
1077
+ ) : (
1078
+ <>
1079
+ {cell.formatted ?? ""}
1080
+ </>
1081
+ )}
1082
+ </td>
1083
+ );
1084
+ })
1085
+ }
1086
+ </tr>
1087
+ );
1088
+ if (hierarchical && hasChildren && expanded)
1089
+ {
1090
+ let childAncestors = [...ancestorHasNext, !isLastChild];
1091
+ children.forEach((child, ci2) =>
1092
+ {
1093
+ let ci = indexOfRow.get(child) ?? -1;
1094
+ let lastChild = ci2 === children.length - 1;
1095
+ renderRow(child, ci, depth + 1, reactKey, childAncestors, lastChild).forEach(n => nodes.push(n));
1096
+ });
1097
+ }
1098
+ return nodes;
1099
+ };
1100
+ let topLevel: RowType[];
1101
+ if (hierarchical && this.props.hierarchy)
1102
+ {
1103
+ let parentCol = this.props.hierarchy.parent;
1104
+ let ids = new Set<string>();
1105
+ displayRows.forEach(r => ids.add(this.getRowIdValue(r) + "" + this.getGroupKey(r)));
1106
+ topLevel = displayRows.filter(r =>
1107
+ {
1108
+ let pid = (r as any)[parentCol];
1109
+ let pidKey = pid === null || pid === undefined ? "" : String(pid);
1110
+ if (pidKey === "")
1111
+ return true;
1112
+ return !ids.has(pidKey + "" + this.getGroupKey(r));
1113
+ });
1114
+ }
1115
+ else
1116
+ {
1117
+ topLevel = displayRows;
1118
+ }
1119
+ content = topLevel.flatMap((rowValue, i) => renderRow(rowValue, indexOfRow.get(rowValue) ?? 0, 0, "", [], i === topLevel.length - 1));
1120
+ }
1121
+
1122
+ let getSortSign = (column: TableColumnInfo) =>
1123
+ {
1124
+ if (this.state.sortItems)
1125
+ {
1126
+ let c = this.state.sortItems.filter(c => c.table.name === column.table.name && c.column.name === column.name)[0]
1127
+ if (c)
1128
+ {
1129
+ if (c.ascending)
1130
+ return "↑"
1131
+ return "↓";
1132
+ }
1133
+ }
1134
+ return "";
1135
+ };
1136
+
1137
+ let buttonPanel = show_button_panel && (
1138
+ <div className={Styles.ns_button_panel}>
1139
+ {
1140
+ this.state.pageSelectionMode !== PageSelectionMode.Hidden &&
1141
+ <NSDialogPageSelection<RowType>
1142
+ icon={
1143
+ this.state.pageSelectionMode === PageSelectionMode.Print ? "https://static.namirasoft.com/image/concept/print/blue.svg" :
1144
+ this.state.pageSelectionMode === PageSelectionMode.CSV ? "https://static.namirasoft.com/image/concept/export/blue.svg" : ""
1145
+ }
1146
+ current_page={this.NSPagination.current?.getCurrentPage() ?? 1}
1147
+ current_size={this.NSPagination.current?.getPageSize() ?? 0}
1148
+ max_page={this.state.totalItems}
1149
+ getItems={async (page, size, selected) =>
1150
+ {
1151
+ if (selected)
1152
+ {
1153
+ let rows: RowType[] = [];
1154
+ if (this.state.rows)
1155
+ for (let i = 0; i < this.state.rows.length; i++)
1156
+ {
1157
+ const row = { index: i, value: this.state.rows[i] };
1158
+ let id = this.props.getRowKey(row);
1159
+ if (this.state.selectedIDs.includes(id))
1160
+ rows.push(row.value);
1161
+ }
1162
+ return rows;
1163
+ }
1164
+ let res = await this.props.getRows(page, size, this.getSortItems());
1165
+ return res.rows;
1166
+ }}
1167
+ onFinish={async (items: RowType[], expandAll: boolean) =>
1168
+ {
1169
+ if (this.state.pageSelectionMode === PageSelectionMode.Print)
1170
+ this.print(items, expandAll);
1171
+ else if (this.state.pageSelectionMode === PageSelectionMode.CSV)
1172
+ this.exportCSV(items);
1173
+ }}
1174
+ showExpandAll={this.state.pageSelectionMode === PageSelectionMode.Print && !!this.props.hierarchy}
1175
+ onClose={() => this.setState({ pageSelectionMode: PageSelectionMode.Hidden })}
1176
+ />
1177
+ }
1178
+ {
1179
+ this.props.hierarchy && isButtonEnabled("expand") &&
1180
+ <NSButton
1181
+ attributes={{ title: this.isAnyExpanded() ? 'Collapse All' : 'Expand All' }}
1182
+ icon={{ src: "https://static.namirasoft.com/image/concept/hierarchy/blue.svg", alt: "expand" }}
1183
+ onClick={{
1184
+ action: this.toggleExpandAll,
1185
+ showLoading: false
1186
+ }}
1187
+ classList={[Styles.ns_table_button]}
1188
+ />
1189
+ }
1190
+ {
1191
+ isButtonEnabled("reorder") &&
1192
+ <NSButton
1193
+ attributes={{ title: 'Reorder' }}
1194
+ icon={{ src: "https://static.namirasoft.com/image/concept/grip-vertical/blue.svg", alt: "reorder" }}
1195
+ onClick={{
1196
+ action: () => this.setState({ showReorderDialog: true }),
1197
+ showLoading: false
1198
+ }}
1199
+ classList={[Styles.ns_table_button]}
1200
+ />
1201
+ }
1202
+ {
1203
+ isButtonEnabled("sort") && this.isSortEnabled() &&
1204
+ <NSButton
1205
+ attributes={{ title: 'Sort' }}
1206
+ icon={{ src: "https://static.namirasoft.com/image/concept/arrow-down-short-wide/blue.svg", alt: "sort" }}
1207
+ onClick={{
1208
+ action: () => this.setState({ showSortDialog: true }),
1209
+ showLoading: false
1210
+ }}
1211
+ classList={[Styles.ns_table_button]}
1212
+ />
1213
+ }
1214
+ {
1215
+ this.props.buttons?.map((button, index) => (
1216
+ <NSButton
1217
+ key={index}
1218
+ {...button}
1219
+ classList={[Styles.ns_table_button, ...(button.classList ?? [])]}
1220
+ />
1221
+ ))
1222
+ }
1223
+ {
1224
+ isButtonEnabled("fullscreen") &&
1225
+ <NSButton
1226
+ icon={{ src: `https://static.namirasoft.com/image/concept/full-screen/blue.svg` }}
1227
+ classList={[Styles.ns_table_button, Styles.ns_fullscreen_button]}
1228
+ onClick={{
1229
+ action: this.toggleFullscreen,
1230
+ showLoading: false
1231
+ }}
1232
+ attributes={{ title: this.state.fullscreen ? 'Minimize' : 'Maximize' }}
1233
+ />
1234
+ }
1235
+ {
1236
+ isButtonEnabled("chart") &&
1237
+ <NSButton
1238
+ attributes={{ title: 'Chart' }}
1239
+ icon={{ src: "https://static.namirasoft.com/image/concept/chart/blue.svg" }}
1240
+ onClick={{
1241
+ action: () =>
1242
+ {
1243
+ if (!this.state.rows || this.state.rows.length === 0)
1244
+ this.showModal(" ", "There is no data to display as a chart.");
1245
+ else
1246
+ this.setState({ showChart: true });
1247
+ },
1248
+ showLoading: false
1249
+ }}
1250
+ classList={[Styles.ns_table_button]}
1251
+ />
1252
+ }
1253
+ {
1254
+ isButtonEnabled("export") &&
1255
+ <NSButton
1256
+ icon={{ src: "https://static.namirasoft.com/image/concept/export/blue.svg" }}
1257
+ onClick={{
1258
+ action: () => { this.setState({ pageSelectionMode: PageSelectionMode.CSV }); },
1259
+ showLoading: false
1260
+ }}
1261
+ classList={[Styles.ns_table_button]}
1262
+ attributes={{ title: 'Export' }}
1263
+ />
1264
+ }
1265
+ {
1266
+ isButtonEnabled("print") &&
1267
+ <NSButton
1268
+ attributes={{ title: 'Print' }}
1269
+ icon={{ src: "https://static.namirasoft.com/image/concept/print/blue.svg" }}
1270
+ onClick={{
1271
+ action: () => { this.setState({ pageSelectionMode: PageSelectionMode.Print }); },
1272
+ showLoading: false
1273
+ }}
1274
+ classList={[Styles.ns_table_button]}
1275
+ />
1276
+ }
1277
+ {
1278
+ isButtonEnabled("refresh") &&
1279
+ <NSButton
1280
+ attributes={{ title: 'Refresh' }}
1281
+ icon={{ src: "https://static.namirasoft.com/image/concept/refresh/blue.svg" }}
1282
+ onClick={{
1283
+ action: async (onFinished) =>
1284
+ {
1285
+ this.reload(null, null).then(onFinished).catch(onFinished);
1286
+ }
1287
+ }}
1288
+ classList={[Styles.ns_table_button]}
1289
+ style={{ border: '1px solid rgb(3, 119, 255)' }}
1290
+ />
1291
+ }
1292
+ </div>
1293
+ );
1294
+
1295
+ return (
1296
+ <div
1297
+ id={this.props.id}
1298
+ className={[...this.props.classList ?? [], Styles.ns_table_wrapper, this.state.fullscreen ? Styles.ns_fullscreen : ""].filter(Boolean).join(" ")}
1299
+ style={{ ...this.props.style }}
1300
+ >
1301
+ {
1302
+ ((!this.state.fullscreen && this.props.headerChildren) || show_pagination || buttonPanel) &&
1303
+ <div className={Styles.ns_table_topbar}>
1304
+ {
1305
+ !this.state.fullscreen &&
1306
+ this.props.headerChildren
1307
+ }
1308
+ {
1309
+ show_pagination &&
1310
+ <NSPagination ref={this.NSPagination} totalItems={this.state.totalItems} onPageChange={this.onPageChange} />
1311
+ }
1312
+ {buttonPanel}
1313
+ </div>
1314
+ }
1315
+ {
1316
+ !this.state.fullscreen && this.props.children &&
1317
+ <div className={Styles.ns_table_children}>
1318
+ {this.props.children}
1319
+ </div>
1320
+ }
1321
+ <table
1322
+ ref={this.Table_Ref}
1323
+ className={Styles.ns_table}
1324
+ >
1325
+ <thead>
1326
+ <tr style={{ gridTemplateColumns }}>
1327
+ {
1328
+ this.props.checkbox &&
1329
+ <th data-cell-type="checkbox" className="d-flex">
1330
+ <label
1331
+ htmlFor='checkbox'
1332
+ className={Styles.ns_checkbox_label}
1333
+ >
1334
+ <input
1335
+ id="checkbox"
1336
+ type="checkbox"
1337
+ className={`${Styles.ns_check_box} ${this.isSomeSelected() ? Styles.ns_indeterminate : ""}`}
1338
+ checked={this.getSelectedIDs().length > 0}
1339
+ onChange={this.toggleAllSelections}
1340
+ ></input>
1341
+ </label>
1342
+ </th>
1343
+ }
1344
+ {
1345
+ this.getColumns(true).map(column =>
1346
+ <th
1347
+ key={column.name}
1348
+ scope="col"
1349
+ onMouseDown={(e) =>
1350
+ {
1351
+ if ((e.target as HTMLElement).closest('[data-resize-handle]'))
1352
+ return;
1353
+ if (!this.isSortEnabled())
1354
+ return;
1355
+ let t = new BaseMetaTable(null, column.table.name, column.table.text);
1356
+ let c = new BaseMetaColumn(t, column.name, column.text, "", false);
1357
+ let items = this.getSortItems();
1358
+ if (!items || items.length === 0 || items[0].table.name !== t.name || items[0].column.name !== c.name)
1359
+ this.setSortItems([new SortItem(t, c, true)]);
1360
+ else
1361
+ {
1362
+ if (items[0].ascending)
1363
+ this.setSortItems([new SortItem(t, c, false)]);
1364
+ else
1365
+ this.setSortItems(null);
1366
+ }
1367
+ }}
1368
+ style={{ width: this.getColumnWidth(column) }}
1369
+ >
1370
+ <span className={Styles.ns_th_title}>
1371
+ {column.text + " " + getSortSign(column)}
1372
+ </span>
1373
+ <div
1374
+ data-resize-handle="true"
1375
+ className={Styles.ns_resize_handle}
1376
+ onMouseDown={(e) => this.startResize(e, column)}
1377
+ onClick={(e) => { e.stopPropagation() }}
1378
+ />
1379
+ </th>
1380
+ )
1381
+ }
1382
+ </tr>
1383
+ </thead>
1384
+
1385
+ <tbody className={Styles.ns_tbody}>
1386
+ {content}
1387
+ </tbody>
1388
+ </table>
1389
+
1390
+ {
1391
+ this.state.model.show &&
1392
+ <NSDialogInfo
1393
+ description={this.state.model.description}
1394
+ onClose={() => { this.hideModal(); }}
1395
+ title={this.state.title}
1396
+ show_as_code={true}
1397
+ >
1398
+ <></>
1399
+ </NSDialogInfo>
1400
+ }
1401
+ {
1402
+ this.state.showChart &&
1403
+ <NSTableChart<RowType>
1404
+ name={this.props.name}
1405
+ columns={this.getColumns(true)}
1406
+ rows={this.state.rows ?? []}
1407
+ selectedRows={this.state.selectedRows}
1408
+ onClose={() => this.setState({ showChart: false })}
1409
+ />
1410
+ }
1411
+ {
1412
+ this.state.showSortDialog && this.isSortEnabled() &&
1413
+ <NSSortDialog
1414
+ sorts={this.getSortItems()}
1415
+ getColumns={() => this.getColumns(null)}
1416
+ onCancel={() => this.setState({ showSortDialog: false })}
1417
+ onApply={sorts =>
1418
+ {
1419
+ this.setSortItems(sorts);
1420
+ this.setState({ showSortDialog: false });
1421
+ }}
1422
+ />
1423
+ }
1424
+ {
1425
+ this.state.showReorderDialog &&
1426
+ <NSReorderDialog
1427
+ visibleColumnKeys={this.getVisibleColumns().map(c => c.table + "." + c.column)}
1428
+ getColumns={() => this.getColumns(null)}
1429
+ onCancel={() => this.setState({ showReorderDialog: false })}
1430
+ onApply={orderedKeys =>
1431
+ {
1432
+ const all = this.getColumns(null);
1433
+ const cs = orderedKeys
1434
+ .map(k => all.find(c => c.table.name + "." + c.name === k))
1435
+ .filter((c): c is TableColumnInfo => !!c);
1436
+ this.setVisibleColumns(cs.map(c => ({ table: c.table.name, column: c.name })));
1437
+ this.setState({ showReorderDialog: false });
1438
+ }}
1439
+ />
1440
+ }
1441
+ </div>
1442
+ );
1443
+ }
1444
1444
  }