@teselagen/ui 0.8.4 → 0.8.6

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.
@@ -0,0 +1,260 @@
1
+ import { camelCase, set } from "lodash-es";
2
+
3
+ export function tableQueryParamsToHasuraClauses({
4
+ page,
5
+ pageSize,
6
+ searchTerm,
7
+ filters,
8
+ order,
9
+ schema, // Add schema as a parameter
10
+ additionalFilter
11
+ }) {
12
+ const ccFields = getFieldsMappedByCCDisplayName(schema);
13
+ let where = {};
14
+ const order_by = {};
15
+ const limit = pageSize || 25;
16
+ const offset = page && pageSize ? (page - 1) * pageSize : 0;
17
+
18
+ if (searchTerm) {
19
+ const searchTermFilters = [];
20
+ schema.fields.forEach(field => {
21
+ const { type, path, searchDisabled } = field;
22
+ if (searchDisabled || field.filterDisabled || type === "color") return;
23
+ const filterValue = searchTerm; // No cleaning needed here, we're using _ilike
24
+
25
+ if (type === "string" || type === "lookup") {
26
+ const o = set({}, path, { _ilike: `%${filterValue}%` });
27
+ searchTermFilters.push(o);
28
+ } else if (type === "boolean") {
29
+ let regex;
30
+ try {
31
+ regex = new RegExp("^" + searchTerm, "ig");
32
+ } catch (error) {
33
+ //ignore
34
+ }
35
+ if (regex) {
36
+ if ("true".replace(regex, "") !== "true") {
37
+ const o = set({}, path, { _eq: true });
38
+ searchTermFilters.push(o);
39
+ } else if ("false".replace(regex, "") !== "false") {
40
+ const o = set({}, path, { _eq: false });
41
+ searchTermFilters.push(o);
42
+ }
43
+ }
44
+ } else if (
45
+ (type === "number" || type === "integer") &&
46
+ !isNaN(filterValue)
47
+ ) {
48
+ const o = set({}, path, { _eq: parseFloat(filterValue) });
49
+ searchTermFilters.push(o);
50
+ }
51
+ });
52
+ if (searchTermFilters.length > 0) {
53
+ if (Object.keys(where).length > 0) {
54
+ where = { _and: [where, { _or: searchTermFilters }] };
55
+ } else {
56
+ where = { _or: searchTermFilters };
57
+ }
58
+ }
59
+ }
60
+
61
+ if (filters && filters.length > 0) {
62
+ const filterClauses = filters
63
+ .map(filter => {
64
+ let { selectedFilter, filterOn, filterValue } = filter;
65
+ const fieldSchema = ccFields[filterOn] || {};
66
+
67
+ const { path, reference, type } = fieldSchema;
68
+ let stringFilterValue =
69
+ filterValue && filterValue.toString
70
+ ? filterValue.toString()
71
+ : filterValue;
72
+ if (stringFilterValue === false) {
73
+ // we still want to be able to search for the string "false" which will get parsed to false
74
+ stringFilterValue = "false";
75
+ } else {
76
+ stringFilterValue = stringFilterValue || "";
77
+ }
78
+ const arrayFilterValue = Array.isArray(filterValue)
79
+ ? filterValue
80
+ : stringFilterValue.split(";");
81
+
82
+ if (type === "number" || type === "integer") {
83
+ filterValue = Array.isArray(filterValue)
84
+ ? filterValue.map(val => Number(val))
85
+ : Number(filterValue);
86
+ }
87
+
88
+ if (fieldSchema.normalizeFilter) {
89
+ filterValue = fieldSchema.normalizeFilter(
90
+ filterValue,
91
+ selectedFilter,
92
+ filterOn
93
+ );
94
+ }
95
+
96
+ if (reference) {
97
+ filterOn = reference.sourceField;
98
+ } else {
99
+ filterOn = path || filterOn;
100
+ }
101
+ switch (selectedFilter) {
102
+ case "none":
103
+ return {};
104
+ case "startsWith":
105
+ return { [filterOn]: { _ilike: `${filterValue}%` } };
106
+ case "endsWith":
107
+ return { [filterOn]: { _ilike: `%${filterValue}` } };
108
+ case "contains":
109
+ return { [filterOn]: { _ilike: `%${filterValue}%` } };
110
+ case "notContains":
111
+ return { [filterOn]: { _not_ilike: `%${filterValue}%` } };
112
+ case "isExactly":
113
+ return { [filterOn]: { _eq: filterValue } };
114
+ case "isEmpty":
115
+ return {
116
+ _or: [
117
+ { [filterOn]: { _eq: "" } },
118
+ { [filterOn]: { _is_null: true } }
119
+ ]
120
+ };
121
+ case "notEmpty":
122
+ return {
123
+ _and: [
124
+ { [filterOn]: { _neq: "" } },
125
+ { [filterOn]: { _is_null: false } }
126
+ ]
127
+ };
128
+ case "inList":
129
+ return { [filterOn]: { _in: filterValue } };
130
+ case "notInList":
131
+ return { [filterOn]: { _nin: filterValue } };
132
+ case "true":
133
+ return { [filterOn]: { _eq: true } };
134
+ case "false":
135
+ return { [filterOn]: { _eq: false } };
136
+ case "dateIs":
137
+ return { [filterOn]: { _eq: filterValue } };
138
+ case "notBetween":
139
+ return {
140
+ _or: [
141
+ {
142
+ [filterOn]: {
143
+ _lt: new Date(arrayFilterValue[0])
144
+ }
145
+ },
146
+ {
147
+ [filterOn]: {
148
+ _gt: new Date(
149
+ new Date(arrayFilterValue[1]).setHours(23, 59)
150
+ )
151
+ }
152
+ }
153
+ ]
154
+ };
155
+ case "isBetween":
156
+ return {
157
+ [filterOn]: {
158
+ _gte: new Date(arrayFilterValue[0]),
159
+ _lte: new Date(new Date(arrayFilterValue[1]).setHours(23, 59))
160
+ }
161
+ };
162
+ case "isBefore":
163
+ return { [filterOn]: { _lt: new Date(filterValue) } };
164
+ case "isAfter":
165
+ return { [filterOn]: { _gt: new Date(filterValue) } };
166
+ case "greaterThan":
167
+ return { [filterOn]: { _gt: parseFloat(filterValue) } };
168
+ case "lessThan":
169
+ return { [filterOn]: { _lt: parseFloat(filterValue) } };
170
+ case "inRange":
171
+ return {
172
+ [filterOn]: {
173
+ _gte: parseFloat(arrayFilterValue[0]),
174
+ _lte: parseFloat(arrayFilterValue[1])
175
+ }
176
+ };
177
+ case "outsideRange":
178
+ return {
179
+ _or: [
180
+ {
181
+ [filterOn]: {
182
+ _lt: parseFloat(arrayFilterValue[0])
183
+ }
184
+ },
185
+ {
186
+ [filterOn]: {
187
+ _gt: parseFloat(arrayFilterValue[1])
188
+ }
189
+ }
190
+ ]
191
+ };
192
+ case "equalTo":
193
+ return {
194
+ [filterOn]: {
195
+ _eq:
196
+ type === "number" || type === "integer"
197
+ ? parseFloat(filterValue)
198
+ : filterValue
199
+ }
200
+ };
201
+ case "regex":
202
+ return { [filterOn]: { _regex: filterValue } };
203
+ default:
204
+ console.warn(`Unsupported filter type: ${selectedFilter}`);
205
+ return {};
206
+ }
207
+ })
208
+ .map(filter => {
209
+ const o = {};
210
+ set(o, Object.keys(filter)[0], filter[Object.keys(filter)[0]]);
211
+ return o;
212
+ });
213
+
214
+ if (filterClauses.length > 0) {
215
+ if (Object.keys(where).length > 0) {
216
+ where = { _and: [where, ...filterClauses] };
217
+ } else {
218
+ where = { _and: filterClauses };
219
+ }
220
+ }
221
+ }
222
+
223
+ if (order && order.length > 0) {
224
+ order.forEach(item => {
225
+ const field = item.startsWith("-") ? item.substring(1) : item;
226
+ const direction = item.startsWith("-") ? "desc" : "asc";
227
+ order_by[field] = direction;
228
+ });
229
+ }
230
+
231
+ if (additionalFilter) {
232
+ where = { _and: [where, additionalFilter] };
233
+ }
234
+ return { where, order_by, limit, offset };
235
+ }
236
+
237
+ /**
238
+ * Takes a schema and returns an object with the fields mapped by their camelCased display name.
239
+ * If the displayName is not set or is a jsx element, the path is used instead.
240
+ * The same conversion must be done when using the result of this method
241
+ */
242
+ export function getFieldsMappedByCCDisplayName(schema) {
243
+ if (!schema || !schema.fields) return {};
244
+ return schema.fields.reduce((acc, field) => {
245
+ const ccDisplayName = getCCDisplayName(field);
246
+ acc[ccDisplayName] = field;
247
+ return acc;
248
+ }, {});
249
+ }
250
+
251
+ /**
252
+ *
253
+ * @param {object} field
254
+ * @returns the camelCase display name of the field, to be used for filters, sorting, etc
255
+ */
256
+ export function getCCDisplayName(field) {
257
+ return camelCase(
258
+ typeof field.displayName === "string" ? field.displayName : field.path
259
+ );
260
+ }
@@ -0,0 +1,206 @@
1
+ import { tableQueryParamsToHasuraClauses } from "./tableQueryParamsToHasuraClauses";
2
+
3
+ describe("tableQueryParamsToHasuraClauses", () => {
4
+ const schema = {
5
+ fields: [
6
+ { path: "name", type: "string" },
7
+ { path: "age", type: "number" },
8
+ { path: "isActive", type: "boolean" },
9
+ { path: "email", type: "string" }
10
+ ]
11
+ };
12
+
13
+ it("should handle empty query params", () => {
14
+ const result = tableQueryParamsToHasuraClauses({});
15
+ expect(result).toEqual({
16
+ where: {},
17
+ order_by: {},
18
+ limit: 25,
19
+ offset: 0
20
+ });
21
+ });
22
+
23
+ it("should handle page and pageSize", () => {
24
+ const result = tableQueryParamsToHasuraClauses({ page: 2, pageSize: 10 });
25
+ expect(result).toEqual({
26
+ where: {},
27
+ order_by: {},
28
+ limit: 10,
29
+ offset: 10
30
+ });
31
+ });
32
+
33
+ it("should handle searchTerm with string fields", () => {
34
+ const result = tableQueryParamsToHasuraClauses({
35
+ searchTerm: "test",
36
+ schema
37
+ });
38
+ expect(result).toEqual({
39
+ where: {
40
+ _or: [{ name: { _ilike: "%test%" } }, { email: { _ilike: "%test%" } }]
41
+ },
42
+ order_by: {},
43
+ limit: 25,
44
+ offset: 0
45
+ });
46
+ });
47
+
48
+ it("should handle searchTerm with number fields", () => {
49
+ const result = tableQueryParamsToHasuraClauses({
50
+ searchTerm: "30",
51
+ schema
52
+ });
53
+ expect(result).toEqual({
54
+ where: {
55
+ _or: [
56
+ { name: { _ilike: "%30%" } },
57
+ { age: { _eq: 30 } },
58
+ { email: { _ilike: "%30%" } }
59
+ ]
60
+ },
61
+ order_by: {},
62
+ limit: 25,
63
+ offset: 0
64
+ });
65
+ });
66
+
67
+ it("should handle searchTerm with boolean fields", () => {
68
+ const result = tableQueryParamsToHasuraClauses({
69
+ searchTerm: "true",
70
+ schema
71
+ });
72
+ expect(result).toEqual({
73
+ where: {
74
+ _or: [
75
+ { name: { _ilike: "%true%" } },
76
+ { isActive: { _eq: true } },
77
+ { email: { _ilike: "%true%" } }
78
+ ]
79
+ },
80
+ order_by: {},
81
+ limit: 25,
82
+ offset: 0
83
+ });
84
+ });
85
+
86
+ it("should handle searchTerm with multiple field types", () => {
87
+ const result = tableQueryParamsToHasuraClauses({
88
+ searchTerm: "test",
89
+ schema
90
+ });
91
+ expect(result).toEqual({
92
+ where: {
93
+ _or: [{ name: { _ilike: "%test%" } }, { email: { _ilike: "%test%" } }]
94
+ },
95
+ order_by: {},
96
+ limit: 25,
97
+ offset: 0
98
+ });
99
+ });
100
+
101
+ it("should handle contains filter", () => {
102
+ const result = tableQueryParamsToHasuraClauses({
103
+ filters: [
104
+ {
105
+ selectedFilter: "contains",
106
+ filterOn: "name",
107
+ filterValue: "test"
108
+ }
109
+ ]
110
+ });
111
+ expect(result).toEqual({
112
+ where: { _and: [{ name: { _ilike: "%test%" } }] },
113
+ order_by: {},
114
+ limit: 25,
115
+ offset: 0
116
+ });
117
+ });
118
+
119
+ it("should handle equalTo filter for number", () => {
120
+ const result = tableQueryParamsToHasuraClauses({
121
+ filters: [
122
+ { selectedFilter: "equalTo", filterOn: "age", filterValue: "30" }
123
+ ],
124
+ schema
125
+ });
126
+ expect(result).toEqual({
127
+ where: { _and: [{ age: { _eq: 30 } }] },
128
+ order_by: {},
129
+ limit: 25,
130
+ offset: 0
131
+ });
132
+ });
133
+
134
+ it("should handle order", () => {
135
+ const result = tableQueryParamsToHasuraClauses({ order: ["name", "-age"] });
136
+ expect(result).toEqual({
137
+ where: {},
138
+ order_by: { name: "asc", age: "desc" },
139
+ limit: 25,
140
+ offset: 0
141
+ });
142
+ });
143
+
144
+ it("should combine all params", () => {
145
+ const result = tableQueryParamsToHasuraClauses({
146
+ page: 2,
147
+ pageSize: 10,
148
+ searchTerm: "test",
149
+ filters: [
150
+ {
151
+ selectedFilter: "greaterThan",
152
+ filterOn: "age",
153
+ filterValue: "30"
154
+ }
155
+ ],
156
+ order: ["name"],
157
+ schema
158
+ });
159
+ expect(result).toEqual({
160
+ where: {
161
+ _and: [
162
+ {
163
+ _or: [
164
+ { name: { _ilike: "%test%" } },
165
+ { email: { _ilike: "%test%" } }
166
+ ]
167
+ },
168
+ { age: { _gt: 30 } }
169
+ ]
170
+ },
171
+ order_by: { name: "asc" },
172
+ limit: 10,
173
+ offset: 10
174
+ });
175
+ });
176
+
177
+ it("should combine searchTerm and filters", () => {
178
+ const result = tableQueryParamsToHasuraClauses({
179
+ searchTerm: "test",
180
+ filters: [
181
+ {
182
+ selectedFilter: "greaterThan",
183
+ filterOn: "age",
184
+ filterValue: "30"
185
+ }
186
+ ],
187
+ schema
188
+ });
189
+ expect(result).toEqual({
190
+ where: {
191
+ _and: [
192
+ {
193
+ _or: [
194
+ { name: { _ilike: "%test%" } },
195
+ { email: { _ilike: "%test%" } }
196
+ ]
197
+ },
198
+ { age: { _gt: 30 } }
199
+ ]
200
+ },
201
+ order_by: {},
202
+ limit: 25,
203
+ offset: 0
204
+ });
205
+ });
206
+ });
@@ -17,125 +17,13 @@ document.addEventListener("mouseup", () => {
17
17
  isDragging = false;
18
18
  });
19
19
 
20
- // Track elements that had their tooltip attributes moved to parent
21
- const processedDisabledElements = new WeakMap();
22
-
23
- // Move tooltip attributes from disabled elements to their parent
24
- function moveTooltipToParent(element) {
25
- // Check if element already processed
26
- if (processedDisabledElements.has(element)) {
27
- return;
28
- }
29
-
30
- // Check if the element is disabled and has tooltip attributes
31
- const isDisabled = element.disabled === true || element.getAttribute("disabled") !== null;
32
- const hasTipData = element.getAttribute("data-tip") ||
33
- element.getAttribute("data-title") ||
34
- (element.offsetWidth < element.scrollWidth && element.textContent?.trim().length > 0);
35
-
36
- if (!isDisabled || !hasTipData) {
37
- return;
38
- }
39
-
40
- const parent = element.parentElement;
41
- if (!parent) {
42
- return;
43
- }
44
-
45
- // Copy tooltip-relevant attributes to the parent
46
- const tooltipAttrs = ["data-tip", "data-title", "data-avoid", "data-avoid-backup"];
47
- let attrsMoved = false;
48
- const movedAttrs = []; // Track which attributes were moved
49
-
50
- tooltipAttrs.forEach(attr => {
51
- const value = element.getAttribute(attr);
52
- if (value) {
53
- // Add a data attribute to the parent only if it doesn't already have one
54
- if (!parent.hasAttribute(attr)) {
55
- parent.setAttribute(attr, value);
56
- movedAttrs.push(attr); // Record this attribute was moved
57
- attrsMoved = true;
58
- }
59
- }
60
- });
61
-
62
- // If element is ellipsized, add its text content as a data-tip to parent
63
- if (element.offsetWidth < element.scrollWidth && element.textContent?.trim().length > 0) {
64
- if (!parent.hasAttribute("data-tip")) {
65
- parent.setAttribute("data-tip", element.textContent);
66
- movedAttrs.push("data-tip"); // Record this attribute was moved
67
- attrsMoved = true;
68
- }
69
- }
70
-
71
- // Store information about moved attributes
72
- if (attrsMoved) {
73
- processedDisabledElements.set(element, {
74
- parent,
75
- movedAttrs
76
- });
77
- }
78
- }
79
-
80
- // Function to clear tooltips from parent elements
81
- function clearParentTooltips(element) {
82
- if (!processedDisabledElements.has(element)) {
83
- return;
84
- }
85
-
86
- const { parent, movedAttrs } = processedDisabledElements.get(element);
87
- if (parent && movedAttrs) {
88
- // Remove all attributes that were added to the parent
89
- movedAttrs.forEach(attr => {
90
- parent.removeAttribute(attr);
91
- });
92
-
93
- // Remove the element from our tracking map
94
- processedDisabledElements.delete(element);
95
- }
96
- }
97
-
98
- // Function to scan for and process disabled elements
99
- function scanForDisabledElements() {
100
- // First, check if any previously disabled elements are now enabled and clear their parent tooltips
101
- processedDisabledElements.forEach((value, element) => {
102
- const isStillDisabled = element.disabled === true || element.getAttribute("disabled") !== null;
103
- const isConnected = element.isConnected;
104
-
105
- if (!isStillDisabled || !isConnected) {
106
- clearParentTooltips(element);
107
- }
108
- });
109
-
110
- // Then process currently disabled elements
111
- document.querySelectorAll("[disabled][data-tip], [disabled][data-title], button[disabled], input[disabled]").forEach(el => {
112
- moveTooltipToParent(el);
113
- });
114
- }
115
-
116
- // Initialize on load and periodically check for new disabled elements
117
- window.addEventListener('DOMContentLoaded', scanForDisabledElements);
118
- setInterval(scanForDisabledElements, 2000);
119
-
120
20
  let tippys = [];
121
21
  let recentlyHidden = false;
122
22
  let clearMe;
123
23
  (function () {
124
24
  let lastMouseOverElement = null;
125
25
  document.addEventListener("mouseover", function (event) {
126
- let element = event.target;
127
-
128
- // Special handling for disabled elements - we need to process their parent
129
- if (element instanceof Element &&
130
- (element.disabled === true || element.getAttribute("disabled") !== null)) {
131
- // If this is a disabled element, we want to also process its parent
132
- // since that's where we moved the tooltip attributes
133
- const parent = element.parentElement;
134
- if (parent && processedDisabledElements.has(element)) {
135
- // Only process the parent if we've previously moved attributes to it
136
- element = parent;
137
- }
138
- }
26
+ const element = event.target;
139
27
 
140
28
  if (element instanceof Element && element !== lastMouseOverElement) {
141
29
  lastMouseOverElement = element;
@@ -218,7 +106,7 @@ let clearMe;
218
106
 
219
107
  if (!customBoundary) return;
220
108
  const a = customBoundary.getBoundingClientRect();
221
-
109
+
222
110
  if (a.top < state.rects.reference.y) {
223
111
  const b = Math.abs(
224
112
  Math.abs(a.top - state.rects.reference.y) - 10
@@ -312,4 +200,4 @@ function parentIncludesNoChildDataTip(el, count) {
312
200
  }
313
201
 
314
202
  // Export the function to clear parent tooltips so it can be used elsewhere
315
- export { clearParentTooltips };
203
+ // export { clearParentTooltips };