@teselagen/ui 0.10.5 → 0.10.7

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.5",
3
+ "version": "0.10.7",
4
4
  "main": "./src/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -9,7 +9,8 @@ import {
9
9
  noop,
10
10
  cloneDeep,
11
11
  get,
12
- padStart
12
+ padStart,
13
+ flatMap
13
14
  } from "lodash-es";
14
15
  import dayjs from "dayjs";
15
16
  import localizedFormat from "dayjs/plugin/localizedFormat";
@@ -38,6 +39,7 @@ import { change as _change } from "redux-form";
38
39
  import { RenderCell } from "./RenderCell";
39
40
  import { getCCDisplayName } from "./utils/tableQueryParamsToHasuraClauses";
40
41
  import { showContextMenu } from "../utils/menuUtils";
42
+ import { dragNoticeEl } from "./dragNoticeEl";
41
43
 
42
44
  dayjs.extend(localizedFormat);
43
45
 
@@ -177,6 +179,7 @@ const RenderColumnHeader = ({
177
179
  e.persist();
178
180
  showContextMenu(
179
181
  [
182
+ dragNoticeEl,
180
183
  {
181
184
  text: "Hide Column",
182
185
  disabled: onlyOneVisibleColumn,
@@ -917,7 +920,10 @@ export const useColumns = ({
917
920
  });
918
921
  }
919
922
 
920
- const tableColumns = columns.map(column => {
923
+ const tableColumns = flatMap(columns, column => {
924
+ if (column.isHidden) {
925
+ return [];
926
+ }
921
927
  const tableColumn = {
922
928
  ...column,
923
929
  Header: RenderColumnHeader({
@@ -950,8 +956,10 @@ export const useColumns = ({
950
956
  getHeaderProps: () => ({
951
957
  // needs to be a string because it is getting passed
952
958
  // to the dom
953
- immovable: column.immovable ? "true" : "false",
954
- columnindex: column.columnIndex
959
+ immovable:
960
+ column.type === "action" || column.immovable ? "true" : "false",
961
+ columnindex: column.columnIndex,
962
+ path: column.path
955
963
  })
956
964
  };
957
965
  const noEllipsis = column.noEllipsis;
@@ -1,17 +1,9 @@
1
1
  import React, { useState } from "react";
2
- import { map, isEmpty, noop, startCase } from "lodash-es";
3
- import {
4
- Button,
5
- Checkbox,
6
- Menu,
7
- MenuItem,
8
- Classes,
9
- InputGroup,
10
- Popover,
11
- Switch
12
- } from "@blueprintjs/core";
13
- import { getCCDisplayName } from "./utils/tableQueryParamsToHasuraClauses";
2
+ import { noop } from "lodash-es";
3
+ import { Button, Menu, Classes, Popover, Switch } from "@blueprintjs/core";
14
4
  import InfoHelper from "../InfoHelper";
5
+ import DraggableColumnOptions from "./DraggableColumnOptions";
6
+ import { dragNoticeEl } from "./dragNoticeEl";
15
7
 
16
8
  const DisplayOptions = ({
17
9
  compact,
@@ -22,13 +14,13 @@ const DisplayOptions = ({
22
14
  resetDefaultVisibility = noop,
23
15
  updateColumnVisibility = noop,
24
16
  updateTableDisplayDensity,
17
+ moveColumnPersist = noop,
25
18
  showForcedHiddenColumns,
26
19
  setShowForcedHidden,
27
20
  hasOptionForForcedHidden,
28
21
  schema
29
22
  }) => {
30
23
  const [isOpen, setIsOpen] = useState(false);
31
- const [searchTerms, setSearchTerms] = useState({});
32
24
 
33
25
  const changeTableDensity = e => {
34
26
  updateTableDisplayDensity(e.target.value);
@@ -41,99 +33,13 @@ const DisplayOptions = ({
41
33
  return null; //don't show antyhing!
42
34
  }
43
35
  const { fields } = schema;
44
- const fieldGroups = {};
45
- const mainFields = [];
46
-
47
- fields.forEach(field => {
48
- if (field.hideInMenu) return;
49
- if (!field.fieldGroup) return mainFields.push(field);
50
- if (!fieldGroups[field.fieldGroup]) fieldGroups[field.fieldGroup] = [];
51
- fieldGroups[field.fieldGroup].push(field);
52
- });
53
36
 
54
37
  let numVisible = 0;
55
38
 
56
- const getFieldCheckbox = (field, i) => {
57
- const { displayName, isHidden, isForcedHidden, path, subFrag } = field;
58
- if (isForcedHidden) return;
59
- if (!isHidden) numVisible++;
60
- return (
61
- <Checkbox
62
- name={`${path}-${i}`}
63
- key={path || i}
64
- onChange={() => {
65
- if (numVisible <= 1 && !isHidden) {
66
- return window.toastr.warning(
67
- "We have to display at least one column :)"
68
- );
69
- }
70
- updateColumnVisibility({ shouldShow: isHidden, path });
71
- }}
72
- checked={!isHidden}
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
- }
87
- />
88
- );
89
- };
90
-
91
- let fieldGroupMenu;
92
- if (!isEmpty(fieldGroups)) {
93
- fieldGroupMenu = map(fieldGroups, (groupFields, groupName) => {
94
- const searchTerm = searchTerms[groupName] || "";
95
- const anyVisible = groupFields.some(
96
- field => !field.isHidden && !field.isForcedHidden
97
- );
98
- const anyNotForcedHidden = groupFields.some(
99
- field => !field.isForcedHidden
100
- );
101
- if (!anyNotForcedHidden) return;
102
- return (
103
- <MenuItem key={groupName} text={groupName}>
104
- <InputGroup
105
- leftIcon="search"
106
- value={searchTerm}
107
- onChange={e => {
108
- setSearchTerms(prev => ({
109
- ...prev,
110
- [groupName]: e.target.value
111
- }));
112
- }}
113
- />
114
- <Button
115
- className={Classes.MINIMAL}
116
- text={(anyVisible ? "Hide" : "Show") + " All"}
117
- style={{ margin: "10px 0" }}
118
- onClick={() => {
119
- updateColumnVisibility({
120
- shouldShow: !anyVisible,
121
- paths: groupFields.map(field => field.path)
122
- });
123
- }}
124
- />
125
- {groupFields
126
- .filter(
127
- field =>
128
- startCase(getCCDisplayName(field)) // We have to use startCase with the camelCase here because the displayName is not always a string
129
- .toLowerCase()
130
- .indexOf(searchTerm.toLowerCase()) > -1
131
- )
132
- .map(getFieldCheckbox)}
133
- </MenuItem>
134
- );
135
- });
136
- }
39
+ // Count number of visible fields
40
+ fields.forEach(field => {
41
+ if (!field.isHidden && field.type !== "action") numVisible++;
42
+ });
137
43
 
138
44
  return (
139
45
  <Popover
@@ -170,7 +76,7 @@ const DisplayOptions = ({
170
76
  <h5
171
77
  style={{
172
78
  fontWeight: "bold",
173
- marginBottom: 10,
79
+ marginBottom: 0,
174
80
  marginTop: 10,
175
81
  display: "flex"
176
82
  }}
@@ -183,10 +89,16 @@ const DisplayOptions = ({
183
89
  </InfoHelper>
184
90
  )}
185
91
  </h5>
186
- <div style={{ maxHeight: 260, overflowY: "auto", padding: 2 }}>
187
- {mainFields.map(getFieldCheckbox)}
92
+ {dragNoticeEl}
93
+
94
+ <div style={{ maxHeight: 360, overflowY: "auto", padding: 2 }}>
95
+ <DraggableColumnOptions
96
+ fields={fields}
97
+ onVisibilityChange={updateColumnVisibility}
98
+ moveColumnPersist={moveColumnPersist}
99
+ numVisible={numVisible}
100
+ />
188
101
  </div>
189
- <div>{fieldGroupMenu}</div>
190
102
  {hasOptionForForcedHidden && (
191
103
  <div style={{ marginTop: 15 }}>
192
104
  <Switch
@@ -196,22 +108,15 @@ const DisplayOptions = ({
196
108
  />
197
109
  </div>
198
110
  )}
199
- <div
200
- style={{
201
- width: "100%",
202
- display: "flex",
203
- justifyContent: "flex-end"
204
- }}
111
+ <Button
112
+ style={{ marginTop: 5 }}
113
+ onClick={resetDefaultVisibility}
114
+ title="Display Options"
115
+ icon="reset"
116
+ minimal
205
117
  >
206
- <Button
207
- onClick={resetDefaultVisibility}
208
- title="Display Options"
209
- icon="reset"
210
- minimal
211
- >
212
- Reset Column Visibilites
213
- </Button>
214
- </div>
118
+ Reset Column Visibilites
119
+ </Button>
215
120
  </div>
216
121
  </Menu>
217
122
  }
@@ -0,0 +1,174 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { DndContext, MouseSensor, useSensor, useSensors } from "@dnd-kit/core";
3
+ import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
4
+ import {
5
+ SortableContext,
6
+ verticalListSortingStrategy,
7
+ useSortable,
8
+ arrayMove
9
+ } from "@dnd-kit/sortable";
10
+ import { CSS } from "@dnd-kit/utilities";
11
+ import { Checkbox, Classes } from "@blueprintjs/core";
12
+ import InfoHelper from "../InfoHelper";
13
+
14
+ const DraggableColumnOption = ({
15
+ field,
16
+ index,
17
+ onVisibilityChange,
18
+ numVisible
19
+ }) => {
20
+ const { displayName, isHidden, path, subFrag, immovable, type } = field;
21
+
22
+ const { attributes, listeners, setNodeRef, transform, transition } =
23
+ useSortable({
24
+ id: path,
25
+ disabled: immovable === "true"
26
+ });
27
+
28
+ if (type === "action") {
29
+ return null; // Skip action columns
30
+ }
31
+
32
+ const style = {
33
+ transform: CSS.Transform.toString(transform),
34
+ transition,
35
+ cursor: "grab",
36
+ marginBottom: 5
37
+ };
38
+
39
+ return (
40
+ <div
41
+ ref={setNodeRef}
42
+ style={style}
43
+ className="SortableItem"
44
+ data-path={path}
45
+ {...attributes}
46
+ {...listeners}
47
+ >
48
+ <Checkbox
49
+ name={`${path}-${index}`}
50
+ key={index}
51
+ onChange={() => {
52
+ if (numVisible <= 1 && !isHidden) {
53
+ return window.toastr.warning(
54
+ "We have to display at least one column :)"
55
+ );
56
+ }
57
+ onVisibilityChange({ shouldShow: isHidden, path });
58
+ }}
59
+ checked={!isHidden}
60
+ label={
61
+ <span style={{ display: "flex", marginTop: -17 }}>
62
+ {displayName}{" "}
63
+ {field.fieldGroup && (
64
+ <span
65
+ style={{ fontSize: 10, marginLeft: 5, marginTop: 2 }}
66
+ className={Classes.TEXT_MUTED}
67
+ >
68
+ ({field.fieldGroup})
69
+ </span>
70
+ )}
71
+ {subFrag && (
72
+ <InfoHelper
73
+ style={{ marginLeft: 5 }}
74
+ >
75
+ Note: Viewing this complex column may cause the table to load slower. Try hiding it for better performance.
76
+ </InfoHelper>
77
+ )}
78
+ </span>
79
+ }
80
+ />
81
+ </div>
82
+ );
83
+ };
84
+
85
+ const DraggableColumnOptions = ({
86
+ fields,
87
+ onVisibilityChange,
88
+ moveColumnPersist,
89
+ numVisible
90
+ }) => {
91
+ const [sortedFields, setSortedFields] = useState(fields);
92
+
93
+ // Update sorted fields when external fields change
94
+ useEffect(() => {
95
+ setSortedFields(fields);
96
+ }, [fields]);
97
+
98
+ const mouseSensor = useSensor(MouseSensor, {
99
+ activationConstraint: {
100
+ distance: 5
101
+ }
102
+ });
103
+
104
+ const sensors = useSensors(mouseSensor);
105
+
106
+ const handleDragStart = event => {
107
+ // Add a class to the body to indicate dragging state
108
+ document.body.classList.add("column-dragging");
109
+
110
+ // Add a class to the active drag item
111
+ const { active } = event;
112
+ if (active) {
113
+ const activeNode = document.querySelector(`[data-path="${active.id}"]`);
114
+ if (activeNode) {
115
+ activeNode.classList.add("dragging");
116
+ }
117
+ }
118
+ };
119
+
120
+ const handleDragEnd = event => {
121
+ // Remove the dragging class
122
+ document.body.classList.remove("column-dragging");
123
+
124
+ // Remove the class from the active drag item
125
+ const draggingItem = document.querySelector(".SortableItem.dragging");
126
+ if (draggingItem) {
127
+ draggingItem.classList.remove("dragging");
128
+ }
129
+
130
+ const { active, over } = event;
131
+
132
+ if (!over || !active || active.id === over.id) {
133
+ return;
134
+ }
135
+
136
+ const oldIndex = sortedFields.findIndex(f => f.path === active.id);
137
+ const newIndex = sortedFields.findIndex(f => f.path === over.id);
138
+
139
+ // Update the local state immediately for a smooth UI
140
+ const newSortedFields = arrayMove(sortedFields, oldIndex, newIndex);
141
+ setSortedFields(newSortedFields);
142
+
143
+ // Notify parent component to persist the change
144
+ moveColumnPersist({ oldIndex, newIndex });
145
+ };
146
+
147
+ return (
148
+ <DndContext
149
+ sensors={sensors}
150
+ modifiers={[restrictToVerticalAxis]}
151
+ onDragStart={handleDragStart}
152
+ onDragEnd={handleDragEnd}
153
+ >
154
+ <SortableContext
155
+ items={sortedFields.map(field => field.path)}
156
+ strategy={verticalListSortingStrategy}
157
+ >
158
+ <div>
159
+ {sortedFields.map((field, index) => (
160
+ <DraggableColumnOption
161
+ key={field.path || index}
162
+ field={field}
163
+ index={index}
164
+ onVisibilityChange={onVisibilityChange}
165
+ numVisible={numVisible}
166
+ />
167
+ ))}
168
+ </div>
169
+ </SortableContext>
170
+ </DndContext>
171
+ );
172
+ };
173
+
174
+ export default DraggableColumnOptions;
@@ -1,21 +1,34 @@
1
- import React from "react";
1
+ import React, { useState, useEffect } from "react";
2
2
  import { MouseSensor, useSensor, useSensors, DndContext } from "@dnd-kit/core";
3
3
  import {
4
4
  SortableContext,
5
- horizontalListSortingStrategy
5
+ horizontalListSortingStrategy,
6
+ arrayMove as dndArrayMove
6
7
  } from "@dnd-kit/sortable";
7
8
  import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
8
9
 
9
10
  const CustomTheadComponent = ({
10
11
  children: _children,
11
12
  className,
12
- onSortEnd,
13
- onSortStart,
13
+ moveColumn,
14
+ sortedItemsFull,
14
15
  style
15
16
  }) => {
16
17
  // We need to do this because react table gives the children wrapped
17
18
  // in another component
18
19
  const children = _children.props.children;
20
+ const [sortedItems, setSortedItems] = useState(() =>
21
+ children.map(c => {
22
+ // Use the path as the ID for sorting instead of the index
23
+ return c.props.path || c.key.split("-")[1];
24
+ })
25
+ );
26
+
27
+ // Update local state when children change
28
+ useEffect(() => {
29
+ setSortedItems(children.map(c => c.props.path || c.key.split("-")[1]));
30
+ }, [children]);
31
+
19
32
  const mouseSensor = useSensor(MouseSensor, {
20
33
  activationConstraint: {
21
34
  distance: 10
@@ -34,15 +47,21 @@ const CustomTheadComponent = ({
34
47
  return;
35
48
  }
36
49
 
37
- onSortEnd({
38
- oldIndex: parseInt(active.id),
39
- newIndex: parseInt(over.id)
40
- });
50
+ // Update local state immediately for smooth UI
51
+ // Use path ID directly instead of parsing as integer
52
+ const oldPath = active.id;
53
+ const newPath = over.id;
54
+ const oldIndex = sortedItemsFull.indexOf(oldPath);
55
+ const newIndex = sortedItemsFull.indexOf(newPath);
56
+ const newSortedItems = dndArrayMove(sortedItemsFull, oldIndex, newIndex);
57
+ setSortedItems(newSortedItems);
58
+
59
+ // Pass to parent for persistence
60
+ moveColumn({ oldIndex, newIndex });
41
61
  };
42
62
 
43
63
  return (
44
64
  <DndContext
45
- onDragStart={onSortStart}
46
65
  onDragEnd={handleDragEnd}
47
66
  modifiers={[restrictToHorizontalAxis]}
48
67
  sensors={sensors}
@@ -50,7 +69,7 @@ const CustomTheadComponent = ({
50
69
  <div className={"rt-thead " + className} style={style}>
51
70
  <div className="rt-tr">
52
71
  <SortableContext
53
- items={children.map((_, index) => `${index}`)}
72
+ items={sortedItems}
54
73
  strategy={horizontalListSortingStrategy}
55
74
  >
56
75
  {children}
@@ -61,7 +80,13 @@ const CustomTheadComponent = ({
61
80
  );
62
81
  };
63
82
 
64
- const SortableColumns = ({ className, style, children, moveColumn }) => {
83
+ const SortableColumns = ({
84
+ className,
85
+ style,
86
+ children,
87
+ moveColumn,
88
+ sortedItemsFull
89
+ }) => {
65
90
  const shouldCancelStart = e => {
66
91
  const className = e.target.className;
67
92
  // if its an svg then it's a blueprint icon
@@ -70,25 +95,12 @@ const SortableColumns = ({ className, style, children, moveColumn }) => {
70
95
  );
71
96
  };
72
97
 
73
- const onSortEnd = (...args) => {
74
- const { oldIndex, newIndex } = args[0];
75
- document.body.classList.remove("drag-active");
76
- moveColumn({
77
- oldIndex,
78
- newIndex
79
- });
80
- };
81
-
82
- const onSortStart = () => {
83
- document.body.classList.add("drag-active");
84
- };
85
-
86
98
  return (
87
99
  <CustomTheadComponent
88
100
  className={className}
89
101
  style={style}
90
- onSortStart={onSortStart}
91
- onSortEnd={onSortEnd}
102
+ sortedItemsFull={sortedItemsFull}
103
+ moveColumn={moveColumn}
92
104
  helperClass="above-dialog"
93
105
  shouldCancelStart={shouldCancelStart}
94
106
  >
@@ -9,19 +9,31 @@ export const ThComponent = ({
9
9
  className,
10
10
  children,
11
11
  style,
12
+ path,
12
13
  columnindex,
13
14
  ...rest
14
15
  }) => {
15
- const index = columnindex ?? -1;
16
- const { attributes, listeners, setNodeRef, transform, transition } =
17
- useSortable({
18
- id: `${index}`,
19
- disabled: immovable === "true"
20
- });
21
-
16
+ const index = columnindex ? path : -1;
17
+ const disabled = !path || immovable === "true" || immovable === true;
18
+ const {
19
+ attributes,
20
+ listeners,
21
+ setNodeRef,
22
+ transform,
23
+ transition,
24
+ isDragging: _isDragging
25
+ } = useSortable({
26
+ id: path,
27
+ disabled
28
+ });
29
+ const isDragging = _isDragging && !disabled;
30
+ if (transform) {
31
+ transform.scaleX = 1; // Prevent column header from shrinking/expanding during drag (looks bad)
32
+ }
22
33
  const sortStyles = {
23
34
  transform: CSS.Transform.toString(transform),
24
- transition
35
+ transition,
36
+ zIndex: isDragging ? 999 : undefined
25
37
  };
26
38
 
27
39
  return (
@@ -30,11 +42,12 @@ export const ThComponent = ({
30
42
  ref={setNodeRef}
31
43
  {...attributes}
32
44
  {...listeners}
33
- className={classNames("rt-th", className)}
45
+ className={classNames("rt-th", className, { "th-dragging": isDragging })}
34
46
  onClick={e => toggleSort && toggleSort(e)}
35
47
  role="columnheader"
36
48
  tabIndex="-1" // Resolves eslint issues without implementing keyboard navigation incorrectly
37
49
  columnindex={columnindex}
50
+ path={path}
38
51
  index={index}
39
52
  {...rest}
40
53
  >
@@ -0,0 +1,13 @@
1
+ import { Classes, Icon } from "@blueprintjs/core";
2
+
3
+ export const dragNoticeEl = (
4
+ <div
5
+ className={Classes.TEXT_MUTED}
6
+ style={{
7
+ padding: 10,
8
+ fontSize: "12px"
9
+ }}
10
+ >
11
+ <Icon icon="info-sign" size={12} /> Drag columns to reorder them
12
+ </div>
13
+ );