@teselagen/ui 0.10.2 → 0.10.4

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.4",
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 = {};
@@ -544,6 +563,7 @@ const DataTable = ({
544
563
  }, [
545
564
  convertedSchema, // If the schema changes we want to take into account the synced tableConfig again
546
565
  formName,
566
+ setTableConfig,
547
567
  syncDisplayOptionsToDb,
548
568
  tableConfigurations,
549
569
  withDisplayOptions,
@@ -663,6 +683,7 @@ const DataTable = ({
663
683
  convertedSchema,
664
684
  currentParams,
665
685
  entities,
686
+ setTableConfig,
666
687
  history,
667
688
  isInfinite,
668
689
  isOpenable,
@@ -780,6 +801,7 @@ const DataTable = ({
780
801
  currentUser?.user?.id,
781
802
  deleteTableConfiguration,
782
803
  formName,
804
+ setTableConfig,
783
805
  schema.fields,
784
806
  syncDisplayOptionsToDb,
785
807
  tableConfig,
@@ -2635,13 +2657,23 @@ const DataTable = ({
2635
2657
  _selectedAndTotalMessage += `/ `;
2636
2658
  }
2637
2659
  if (showCount) {
2638
- _selectedAndTotalMessage += `${entityCount || 0} Total`;
2660
+ if (isEntityCountLoading && entityCount < 1) {
2661
+ _selectedAndTotalMessage += `Loading...`;
2662
+ } else {
2663
+ _selectedAndTotalMessage += `${entityCount || 0} Total`;
2664
+ }
2639
2665
  }
2640
2666
  if (_selectedAndTotalMessage) {
2641
2667
  _selectedAndTotalMessage = <div>{_selectedAndTotalMessage}</div>;
2642
2668
  }
2643
2669
  return _selectedAndTotalMessage;
2644
- }, [entityCount, selectedRowCount, showCount, showNumSelected]);
2670
+ }, [
2671
+ entityCount,
2672
+ selectedRowCount,
2673
+ showCount,
2674
+ showNumSelected,
2675
+ isEntityCountLoading
2676
+ ]);
2645
2677
 
2646
2678
  const shouldShowPaging =
2647
2679
  !isInfinite &&
@@ -2729,6 +2761,8 @@ const DataTable = ({
2729
2761
  columns,
2730
2762
  currentParams,
2731
2763
  compact,
2764
+ // withDisplayOptions,
2765
+ resetDefaultVisibility,
2732
2766
  editingCellSelectAll,
2733
2767
  entities,
2734
2768
  expandedEntityIdMap,
@@ -2766,6 +2800,7 @@ const DataTable = ({
2766
2800
  startCellEdit,
2767
2801
  SubComponent,
2768
2802
  tableRef,
2803
+ updateColumnVisibility,
2769
2804
  updateEntitiesHelper,
2770
2805
  updateValidation,
2771
2806
  withCheckboxes,
@@ -2809,7 +2844,8 @@ const DataTable = ({
2809
2844
  expanded={expandedRows}
2810
2845
  showPagination={false}
2811
2846
  sortable={false}
2812
- loading={isLoading || disabled}
2847
+ // loading={isLoading || disabled}
2848
+ loading={disabled}
2813
2849
  defaultResized={resized}
2814
2850
  onResizedChange={(newResized = []) => {
2815
2851
  const resizedToUse = newResized.map(column => {
@@ -2830,7 +2866,9 @@ const DataTable = ({
2830
2866
  getTrGroupProps={getTableRowProps}
2831
2867
  getTdProps={getTableCellProps}
2832
2868
  NoDataComponent={({ children }) =>
2833
- isLoading ? null : (
2869
+ isLoading ? (
2870
+ <div className="rt-noData">Loading...</div>
2871
+ ) : (
2834
2872
  <div className="rt-noData">{noRowsFoundMessage || children}</div>
2835
2873
  )
2836
2874
  }
@@ -3215,6 +3253,7 @@ const DataTable = ({
3215
3253
  {!noFullscreenButton && toggleFullscreenButton}
3216
3254
  {withDisplayOptions && (
3217
3255
  <DisplayOptions
3256
+ doNotSearchHiddenColumns={doNotSearchHiddenColumns}
3218
3257
  compact={compact}
3219
3258
  extraCompact={extraCompact}
3220
3259
  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 {