@teselagen/ui 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teselagen/ui",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "main": "./src/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -2,6 +2,7 @@ import React, { useState } from "react";
2
2
  import { Button, Classes, Icon } from "@blueprintjs/core";
3
3
  import classNames from "classnames";
4
4
  import "./style.css";
5
+ import InfoHelper from "../InfoHelper";
5
6
 
6
7
  export default function CollapsibleCard({
7
8
  title,
@@ -11,6 +12,7 @@ export default function CollapsibleCard({
11
12
  className,
12
13
  style,
13
14
  children,
15
+ helperText,
14
16
  initialClosed = false,
15
17
  toggle,
16
18
  isOpen
@@ -48,7 +50,12 @@ export default function CollapsibleCard({
48
50
  >
49
51
  {title}
50
52
  </h6>
51
- <div>{open && openTitleElements}</div>
53
+ {helperText && (
54
+ <>
55
+ <InfoHelper children={helperText}></InfoHelper> &nbsp;
56
+ </>
57
+ )}
58
+ {open && <div> {openTitleElements}</div>}
52
59
  </div>
53
60
  <div>
54
61
  <Button
@@ -37,6 +37,7 @@ import { useDispatch } from "react-redux";
37
37
  import { change as _change } from "redux-form";
38
38
  import { RenderCell } from "./RenderCell";
39
39
  import { getCCDisplayName } from "./utils/tableQueryParamsToHasuraClauses";
40
+ import { showContextMenu } from "../utils/menuUtils";
40
41
 
41
42
  dayjs.extend(localizedFormat);
42
43
 
@@ -50,8 +51,13 @@ const RenderColumnHeader = ({
50
51
  entities,
51
52
  extraCompact,
52
53
  filters,
54
+ resetDefaultVisibility,
55
+ onlyOneVisibleColumn,
53
56
  formName,
54
57
  isCellEditable,
58
+ updateColumnVisibility,
59
+ schema,
60
+ withDisplayOptions,
55
61
  isLocalCall,
56
62
  order,
57
63
  removeSingleFilter,
@@ -163,6 +169,60 @@ const RenderColumnHeader = ({
163
169
  <strong>${columnTitle}:</strong> <br>
164
170
  ${description} ${isUnique ? "<br>Must be unique" : ""}</div>`
165
171
  })}
172
+ onContextMenu={e => {
173
+ if (!withDisplayOptions) {
174
+ return
175
+ }
176
+ e.preventDefault();
177
+ e.persist();
178
+ showContextMenu(
179
+ [
180
+ {
181
+ text: "Hide Column",
182
+ disabled: onlyOneVisibleColumn,
183
+ icon: "eye-off",
184
+ onClick: () => {
185
+ updateColumnVisibility({
186
+ shouldShow: false,
187
+ path
188
+ });
189
+ }
190
+ },
191
+ {
192
+ text: "Hide Other Columns",
193
+ icon: "eye-off",
194
+ onClick: () => {
195
+ updateColumnVisibility({
196
+ shouldShow: false,
197
+ paths: schema.fields
198
+ .map(c => c.path)
199
+ .filter(Boolean)
200
+ .filter(p => p !== path)
201
+ });
202
+ }
203
+ },
204
+ {
205
+ text: "Reset Column Visibilities",
206
+ icon: "reset",
207
+ onClick: () => {
208
+ resetDefaultVisibility();
209
+ }
210
+ },
211
+ {
212
+ text: "Table Display Options",
213
+ icon: "cog",
214
+ onClick: () => {
215
+ e.target
216
+ .closest(".data-table-container")
217
+ ?.querySelector(".tg-table-display-options")
218
+ ?.click();
219
+ }
220
+ }
221
+ ],
222
+ undefined,
223
+ e
224
+ );
225
+ }}
166
226
  data-test={columnTitleTextified}
167
227
  data-path={path}
168
228
  data-copy-text={columnTitleTextified}
@@ -340,6 +400,7 @@ export const useColumns = ({
340
400
  addFilters,
341
401
  cellRenderer,
342
402
  columns,
403
+ resetDefaultVisibility,
343
404
  currentParams,
344
405
  compact,
345
406
  editingCell,
@@ -374,6 +435,7 @@ export const useColumns = ({
374
435
  selectedCells,
375
436
  setExpandedEntityIdMap,
376
437
  setNewParams,
438
+ updateColumnVisibility,
377
439
  setOrder = noop,
378
440
  setSelectedCells,
379
441
  shouldShowSubComponent,
@@ -387,7 +449,8 @@ export const useColumns = ({
387
449
  withFilter: _withFilter,
388
450
  withSort = true,
389
451
  recordIdToIsVisibleMap,
390
- setRecordIdToIsVisibleMap
452
+ setRecordIdToIsVisibleMap,
453
+ withDisplayOptions
391
454
  }) => {
392
455
  const dispatch = useDispatch();
393
456
  const change = useCallback(
@@ -858,15 +921,20 @@ export const useColumns = ({
858
921
  const tableColumn = {
859
922
  ...column,
860
923
  Header: RenderColumnHeader({
924
+ onlyOneVisibleColumn: columns.length === 1,
861
925
  recordIdToIsVisibleMap,
862
926
  setRecordIdToIsVisibleMap,
863
927
  column,
928
+ withDisplayOptions,
864
929
  isLocalCall,
865
930
  filters,
866
931
  currentParams,
867
932
  order,
933
+ resetDefaultVisibility,
868
934
  setOrder,
869
935
  withSort,
936
+ schema,
937
+ updateColumnVisibility,
870
938
  formName,
871
939
  extraCompact,
872
940
  withFilter,
@@ -11,11 +11,13 @@ import {
11
11
  Switch
12
12
  } from "@blueprintjs/core";
13
13
  import { getCCDisplayName } from "./utils/tableQueryParamsToHasuraClauses";
14
+ import InfoHelper from "../InfoHelper";
14
15
 
15
16
  const DisplayOptions = ({
16
17
  compact,
17
18
  extraCompact,
18
19
  disabled,
20
+ doNotSearchHiddenColumns,
19
21
  hideDisplayOptionsIcon,
20
22
  resetDefaultVisibility = noop,
21
23
  updateColumnVisibility = noop,
@@ -52,7 +54,7 @@ const DisplayOptions = ({
52
54
  let numVisible = 0;
53
55
 
54
56
  const getFieldCheckbox = (field, i) => {
55
- const { displayName, isHidden, isForcedHidden, path } = field;
57
+ const { displayName, isHidden, isForcedHidden, path, subFrag } = field;
56
58
  if (isForcedHidden) return;
57
59
  if (!isHidden) numVisible++;
58
60
  return (
@@ -68,7 +70,20 @@ const DisplayOptions = ({
68
70
  updateColumnVisibility({ shouldShow: isHidden, path });
69
71
  }}
70
72
  checked={!isHidden}
71
- label={displayName}
73
+ label={
74
+ <span style={{ display: "flex", marginTop: -17 }}>
75
+ {displayName}
76
+ {subFrag && (
77
+ <InfoHelper
78
+ icon="warning-sign"
79
+ intent="warning"
80
+ style={{ marginLeft: 5 }}
81
+ >
82
+ Viewing this column may cause the table to load slower
83
+ </InfoHelper>
84
+ )}
85
+ </span>
86
+ }
72
87
  />
73
88
  );
74
89
  };
@@ -127,7 +142,9 @@ const DisplayOptions = ({
127
142
  content={
128
143
  <Menu>
129
144
  <div style={{ padding: 10, paddingLeft: 20, paddingRight: 20 }}>
130
- <h5 style={{ marginBottom: 10 }}>Display Density:</h5>
145
+ <h5 style={{ marginBottom: 10, fontWeight: "bold" }}>
146
+ Display Density:
147
+ </h5>
131
148
  <div className={Classes.SELECT + " tg-table-display-density"}>
132
149
  <select
133
150
  onChange={changeTableDensity}
@@ -150,8 +167,19 @@ const DisplayOptions = ({
150
167
  </option>
151
168
  </select>
152
169
  </div>
153
- <h5 style={{ marginBottom: 10, marginTop: 10 }}>
154
- Displayed Columns:
170
+ <h5
171
+ style={{
172
+ fontWeight: "bold",
173
+ marginBottom: 10,
174
+ marginTop: 10,
175
+ display: "flex"
176
+ }}
177
+ >
178
+ Displayed Columns: &nbsp;
179
+ {doNotSearchHiddenColumns && <InfoHelper>
180
+ Note: Hidden columns will NOT be used when searching the
181
+ datatable
182
+ </InfoHelper>}
155
183
  </h5>
156
184
  <div style={{ maxHeight: 260, overflowY: "auto", padding: 2 }}>
157
185
  {mainFields.map(getFieldCheckbox)}
@@ -176,9 +204,10 @@ const DisplayOptions = ({
176
204
  <Button
177
205
  onClick={resetDefaultVisibility}
178
206
  title="Display Options"
207
+ icon="reset"
179
208
  minimal
180
209
  >
181
- Reset
210
+ Reset Column Visibilites
182
211
  </Button>
183
212
  </div>
184
213
  </div>
@@ -118,6 +118,7 @@ const DataTable = ({
118
118
  controlled_pageSize,
119
119
  formName = "tgDataTable",
120
120
  history,
121
+ doNotSearchHiddenColumns,
121
122
  isSimple,
122
123
  isLocalCall = true,
123
124
  isTableParamsConnected,
@@ -481,7 +482,8 @@ const DataTable = ({
481
482
  withSelectAll,
482
483
  withSort,
483
484
  withTitle = !isSimple,
484
- noExcessiveCheck
485
+ noExcessiveCheck,
486
+ isEntityCountLoading
485
487
  } = props;
486
488
 
487
489
  const _entities = useMemo(
@@ -513,8 +515,25 @@ const DataTable = ({
513
515
  formName
514
516
  ]);
515
517
 
516
- const [tableConfig, setTableConfig] = useState({ fieldOptions: [] });
517
-
518
+ const [tableConfig, _setTableConfig] = useState({ fieldOptions: [] });
519
+ const setTableConfig = useCallback(
520
+ newConfig => {
521
+ _setTableConfig(prev => {
522
+ let newConfigVal = newConfig;
523
+ if (typeof newConfig === "function") {
524
+ newConfigVal = newConfig(prev);
525
+ }
526
+ if (!isEqual(prev.fieldOptions, newConfigVal.fieldOptions)) {
527
+ change(
528
+ "reduxFormReadOnlyFieldOptions",
529
+ newConfigVal.fieldOptions || []
530
+ );
531
+ }
532
+ return newConfigVal;
533
+ });
534
+ },
535
+ [change]
536
+ );
518
537
  useEffect(() => {
519
538
  if (withDisplayOptions) {
520
539
  let newTableConfig = {};
@@ -663,6 +682,7 @@ const DataTable = ({
663
682
  convertedSchema,
664
683
  currentParams,
665
684
  entities,
685
+ setTableConfig,
666
686
  history,
667
687
  isInfinite,
668
688
  isOpenable,
@@ -780,6 +800,7 @@ const DataTable = ({
780
800
  currentUser?.user?.id,
781
801
  deleteTableConfiguration,
782
802
  formName,
803
+ setTableConfig,
783
804
  schema.fields,
784
805
  syncDisplayOptionsToDb,
785
806
  tableConfig,
@@ -2635,13 +2656,23 @@ const DataTable = ({
2635
2656
  _selectedAndTotalMessage += `/ `;
2636
2657
  }
2637
2658
  if (showCount) {
2638
- _selectedAndTotalMessage += `${entityCount || 0} Total`;
2659
+ if (isEntityCountLoading && entityCount < 1) {
2660
+ _selectedAndTotalMessage += `Loading...`;
2661
+ } else {
2662
+ _selectedAndTotalMessage += `${entityCount || 0} Total`;
2663
+ }
2639
2664
  }
2640
2665
  if (_selectedAndTotalMessage) {
2641
2666
  _selectedAndTotalMessage = <div>{_selectedAndTotalMessage}</div>;
2642
2667
  }
2643
2668
  return _selectedAndTotalMessage;
2644
- }, [entityCount, selectedRowCount, showCount, showNumSelected]);
2669
+ }, [
2670
+ entityCount,
2671
+ selectedRowCount,
2672
+ showCount,
2673
+ showNumSelected,
2674
+ isEntityCountLoading
2675
+ ]);
2645
2676
 
2646
2677
  const shouldShowPaging =
2647
2678
  !isInfinite &&
@@ -2729,6 +2760,8 @@ const DataTable = ({
2729
2760
  columns,
2730
2761
  currentParams,
2731
2762
  compact,
2763
+ withDisplayOptions,
2764
+ resetDefaultVisibility,
2732
2765
  editingCellSelectAll,
2733
2766
  entities,
2734
2767
  expandedEntityIdMap,
@@ -2766,6 +2799,7 @@ const DataTable = ({
2766
2799
  startCellEdit,
2767
2800
  SubComponent,
2768
2801
  tableRef,
2802
+ updateColumnVisibility,
2769
2803
  updateEntitiesHelper,
2770
2804
  updateValidation,
2771
2805
  withCheckboxes,
@@ -2809,7 +2843,8 @@ const DataTable = ({
2809
2843
  expanded={expandedRows}
2810
2844
  showPagination={false}
2811
2845
  sortable={false}
2812
- loading={isLoading || disabled}
2846
+ // loading={isLoading || disabled}
2847
+ loading={disabled}
2813
2848
  defaultResized={resized}
2814
2849
  onResizedChange={(newResized = []) => {
2815
2850
  const resizedToUse = newResized.map(column => {
@@ -2830,7 +2865,9 @@ const DataTable = ({
2830
2865
  getTrGroupProps={getTableRowProps}
2831
2866
  getTdProps={getTableCellProps}
2832
2867
  NoDataComponent={({ children }) =>
2833
- isLoading ? null : (
2868
+ isLoading ? (
2869
+ <div className="rt-noData">Loading...</div>
2870
+ ) : (
2834
2871
  <div className="rt-noData">{noRowsFoundMessage || children}</div>
2835
2872
  )
2836
2873
  }
@@ -3215,6 +3252,7 @@ const DataTable = ({
3215
3252
  {!noFullscreenButton && toggleFullscreenButton}
3216
3253
  {withDisplayOptions && (
3217
3254
  <DisplayOptions
3255
+ doNotSearchHiddenColumns={doNotSearchHiddenColumns}
3218
3256
  compact={compact}
3219
3257
  extraCompact={extraCompact}
3220
3258
  disabled={disabled}
@@ -24,10 +24,10 @@ export function tableQueryParamsToHasuraClauses({
24
24
  const searchTerms = searchTerm.split(",");
25
25
 
26
26
  schema.fields.forEach(field => {
27
- const { type, path, searchDisabled } = field;
27
+ const { type, path, searchDisabled, isHidden } = field;
28
28
  if (uniqueFieldsByPath[path]) return; // Skip if already added
29
29
  uniqueFieldsByPath[path] = true;
30
- if (searchDisabled || field.filterDisabled || type === "color") return;
30
+ if (searchDisabled || field.filterDisabled || type === "color" || isHidden) return;
31
31
 
32
32
  // Process each search term
33
33
  searchTerms.forEach(term => {
@@ -63,6 +63,27 @@ describe("tableQueryParamsToHasuraClauses", () => {
63
63
  offset: 0
64
64
  });
65
65
  });
66
+ it("shouldn't search on hidden fields", () => {
67
+ const result = tableQueryParamsToHasuraClauses({
68
+ searchTerm: "test",
69
+ schema: {
70
+ fields: [
71
+ { path: "name", type: "string", isHidden: true },
72
+ { path: "age", type: "number" },
73
+ { path: "isActive", type: "boolean" },
74
+ { path: "email", type: "string" }
75
+ ]
76
+ }
77
+ });
78
+ expect(result).toEqual({
79
+ where: {
80
+ _or: [{ email: { _ilike: "%test%" } }]
81
+ },
82
+ order_by: [],
83
+ limit: 25,
84
+ offset: 0
85
+ });
86
+ });
66
87
  it("should flatten queries with dup paths", () => {
67
88
  const result = tableQueryParamsToHasuraClauses({
68
89
  searchTerm: "test",
@@ -76,7 +76,39 @@ export const useTableParams = props => {
76
76
  [_defaults, controlled_pageSize]
77
77
  );
78
78
 
79
- const convertedSchema = useMemo(() => convertSchema(schema), [schema]);
79
+ const formValueStateSelector = state =>
80
+ formValueSelector(formName)(
81
+ state,
82
+ "reduxFormQueryParams",
83
+ "reduxFormSelectedEntityIdMap",
84
+ "reduxFormReadOnlyFieldOptions"
85
+ );
86
+
87
+ const {
88
+ reduxFormQueryParams: _reduxFormQueryParams = {},
89
+ reduxFormSelectedEntityIdMap: _reduxFormSelectedEntityIdMap = {},
90
+ reduxFormReadOnlyFieldOptions
91
+ } = useSelector(formValueStateSelector);
92
+
93
+ const convertedSchema = useMemo(() => {
94
+ const pathToHiddenMap = {};
95
+ reduxFormReadOnlyFieldOptions?.forEach(({ path, isHidden }) => {
96
+ if (path) pathToHiddenMap[path] = isHidden;
97
+ });
98
+ const s = convertSchema(schema);
99
+
100
+ s.fields = s.fields.map(f => {
101
+ const isHidden = pathToHiddenMap[f.path];
102
+ if (pathToHiddenMap[f.path] !== undefined) {
103
+ return {
104
+ ...f,
105
+ isHidden
106
+ };
107
+ }
108
+ return f;
109
+ });
110
+ return s;
111
+ }, [schema, reduxFormReadOnlyFieldOptions]);
80
112
 
81
113
  if (isLocalCall) {
82
114
  if (!noForm && (!formName || formName === "tgDataTable")) {
@@ -98,18 +130,6 @@ export const useTableParams = props => {
98
130
  }
99
131
  }
100
132
 
101
- const formValueStateSelector = state =>
102
- formValueSelector(formName)(
103
- state,
104
- "reduxFormQueryParams",
105
- "reduxFormSelectedEntityIdMap"
106
- );
107
-
108
- const {
109
- reduxFormQueryParams: _reduxFormQueryParams = {},
110
- reduxFormSelectedEntityIdMap: _reduxFormSelectedEntityIdMap = {}
111
- } = useSelector(formValueStateSelector);
112
-
113
133
  // We want to make sure we don't rerender everything unnecessary
114
134
  // with redux-forms we tend to do unnecessary renders
115
135
  const reduxFormQueryParams = useDeepEqualMemo(_reduxFormQueryParams);
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import { Classes } from "@blueprintjs/core";
3
+ import classNames from "classnames";
4
+ import "./style.css";
5
+
6
+ /**
7
+ * A divider component with optional text in the middle.
8
+ * Compatible with BlueprintJS styling.
9
+ *
10
+ * @param {Object} props - Component props
11
+ * @param {string} [props.text] - Optional text to display in the middle of the divider
12
+ * @param {string} [props.className] - Additional CSS class name
13
+ * @param {React.CSSProperties} [props.style] - Additional inline styles
14
+ * @returns {JSX.Element} The divider component
15
+ */
16
+ export default function DividerWithText({ text, className, style }) {
17
+ return (
18
+ <div
19
+ className={classNames(
20
+ Classes.DIVIDER,
21
+ "tg-blueprint-divider",
22
+ { "tg-blueprint-divider-with-text": !!text },
23
+ className
24
+ )}
25
+ style={style}
26
+ >
27
+ {text && <span className="tg-blueprint-divider-text">{text}</span>}
28
+ </div>
29
+ );
30
+ }
@@ -0,0 +1,37 @@
1
+ .tg-blueprint-divider {
2
+ position: relative;
3
+ }
4
+
5
+ /* Override Blueprint's default divider when we use text */
6
+ .tg-blueprint-divider-with-text.bp3-divider {
7
+ /* Remove Blueprint's default border styling to avoid duplication */
8
+ border: none;
9
+ display: flex;
10
+ align-items: center;
11
+ margin: 16px 0;
12
+ height: auto;
13
+ }
14
+
15
+ .tg-blueprint-divider-with-text::before,
16
+ .tg-blueprint-divider-with-text::after {
17
+ content: "";
18
+ flex: 1;
19
+ border-bottom: 1px solid rgba(16, 22, 26, 0.15);
20
+ }
21
+
22
+ .tg-blueprint-divider-text {
23
+ padding: 0 10px;
24
+ font-size: 14px;
25
+ color: rgba(92, 112, 128, 0.6);
26
+ white-space: nowrap;
27
+ }
28
+
29
+ /* Dark theme support */
30
+ .bp3-dark .tg-blueprint-divider-with-text::before,
31
+ .bp3-dark .tg-blueprint-divider-with-text::after {
32
+ border-bottom-color: rgba(255, 255, 255, 0.15);
33
+ }
34
+
35
+ .bp3-dark .tg-blueprint-divider-text {
36
+ color: rgba(167, 182, 194, 0.6);
37
+ }
@@ -1,5 +1,6 @@
1
1
  .bp3-popover.tg-info-helper-popover .bp3-popover-content {
2
2
  max-width: 340px;
3
+ white-space: pre-line;
3
4
  }
4
5
 
5
6
  .info-helper-clickable .bp3-popover-target {
package/src/index.js CHANGED
@@ -5,6 +5,7 @@ import "./style.css";
5
5
  import "./autoTooltip";
6
6
  export { LoadingDots } from "./FormComponents/LoadingDots";
7
7
  export { FormSeparator } from "./FormComponents/FormSeparator";
8
+ export { default as DividerWithText } from "./DividerWithText";
8
9
  export * from "./AssignDefaultsModeContext";
9
10
  export { default as Uploader } from "./FormComponents/Uploader";
10
11
  export { mergeSchemas } from "./DataTable/utils/convertSchema";
package/ui.css CHANGED
@@ -9356,6 +9356,43 @@ button:not(:disabled):active {
9356
9356
  margin-left: 16px;
9357
9357
  }
9358
9358
  }
9359
+ .tg-blueprint-divider {
9360
+ position: relative;
9361
+ }
9362
+
9363
+ /* Override Blueprint's default divider when we use text */
9364
+ .tg-blueprint-divider-with-text.bp3-divider {
9365
+ /* Remove Blueprint's default border styling to avoid duplication */
9366
+ border: none;
9367
+ display: flex;
9368
+ align-items: center;
9369
+ margin: 16px 0;
9370
+ height: auto;
9371
+ }
9372
+
9373
+ .tg-blueprint-divider-with-text::before,
9374
+ .tg-blueprint-divider-with-text::after {
9375
+ content: "";
9376
+ flex: 1;
9377
+ border-bottom: 1px solid rgba(16, 22, 26, 0.15);
9378
+ }
9379
+
9380
+ .tg-blueprint-divider-text {
9381
+ padding: 0 10px;
9382
+ font-size: 14px;
9383
+ color: rgba(92, 112, 128, 0.6);
9384
+ white-space: nowrap;
9385
+ }
9386
+
9387
+ /* Dark theme support */
9388
+ .bp3-dark .tg-blueprint-divider-with-text::before,
9389
+ .bp3-dark .tg-blueprint-divider-with-text::after {
9390
+ border-bottom-color: rgba(255, 255, 255, 0.15);
9391
+ }
9392
+
9393
+ .bp3-dark .tg-blueprint-divider-text {
9394
+ color: rgba(167, 182, 194, 0.6);
9395
+ }
9359
9396
  .rg-celleditor-input,
9360
9397
  .rg-celleditor input {
9361
9398
  border: none;
@@ -9423,6 +9460,7 @@ button:not(:disabled):active {
9423
9460
  }
9424
9461
  .bp3-popover.tg-info-helper-popover .bp3-popover-content {
9425
9462
  max-width: 340px;
9463
+ white-space: pre-line;
9426
9464
  }
9427
9465
 
9428
9466
  .info-helper-clickable .bp3-popover-target {