@axinom/mosaic-ui 0.65.3 → 0.65.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": "@axinom/mosaic-ui",
3
- "version": "0.65.3",
3
+ "version": "0.65.4",
4
4
  "description": "UI components for building Axinom Mosaic applications",
5
5
  "author": "Axinom",
6
6
  "license": "PROPRIETARY",
@@ -112,5 +112,5 @@
112
112
  "publishConfig": {
113
113
  "access": "public"
114
114
  },
115
- "gitHead": "cac34831ec1092ebcbe246bb09f610a64edc81a6"
115
+ "gitHead": "ac37c5be8b4c739425573c41be818692020f1f25"
116
116
  }
@@ -21,6 +21,7 @@ import {
21
21
  } from '../FormStation';
22
22
  import { IconName } from '../Icons';
23
23
  import { ListSelectMode } from '../List';
24
+ import { createConnectionRenderer } from '../Utils';
24
25
  import { generateBulkEditMutation } from './BulkEdit/GenerateMutation';
25
26
  import { Explorer } from './Explorer';
26
27
  import { QuickEditContext } from './QuickEdit/QuickEditContext';
@@ -34,6 +35,7 @@ interface ExplorerStoryData {
34
35
  title: string;
35
36
  date?: Date;
36
37
  desc: string;
38
+ tags?: { nodes: { name: string }[] };
37
39
  }
38
40
 
39
41
  type ExplorerStoryType = typeof Explorer<ExplorerStoryData>;
@@ -120,6 +122,14 @@ const generateData = (
120
122
  title: `${usePrefix ? `Index ${index}: ` : ''}${faker.random.words(
121
123
  faker.datatype.number({ min: 1, max: 3 }),
122
124
  )}`,
125
+ tags: {
126
+ nodes: Array.from(
127
+ { length: faker.datatype.number({ min: 0, max: 10 }) },
128
+ () => ({
129
+ name: faker.random.word(),
130
+ }),
131
+ ),
132
+ },
123
133
  };
124
134
  });
125
135
 
@@ -140,6 +150,13 @@ export const Default: StoryObj<ExplorerStoryType> = {
140
150
  propertyName: 'title',
141
151
  label: 'Title',
142
152
  },
153
+ {
154
+ propertyName: 'tags',
155
+ label: 'Tags',
156
+ render: createConnectionRenderer<{ nodes: { name: string }[] }>(
157
+ (tag) => tag.name,
158
+ ),
159
+ },
143
160
  {
144
161
  propertyName: 'date',
145
162
  label: 'Date',
@@ -34,27 +34,6 @@ describe('TagsRenderer', () => {
34
34
  expect(container.firstChild).toBeInTheDocument();
35
35
  });
36
36
 
37
- it('returns empty content when value is not an array', () => {
38
- const { container } = render(
39
- <RendererWrapper {...defaultProps} value="not-an-array" />,
40
- );
41
- expect(container.firstChild).toBeNull();
42
- });
43
-
44
- it('returns empty content when value is null', () => {
45
- const { container } = render(
46
- <RendererWrapper {...defaultProps} value={null} />,
47
- );
48
- expect(container.firstChild).toBeNull();
49
- });
50
-
51
- it('returns empty content when value is undefined', () => {
52
- const { container } = render(
53
- <RendererWrapper {...defaultProps} value={undefined} />,
54
- );
55
- expect(container.firstChild).toBeNull();
56
- });
57
-
58
37
  it('renders tags when array is provided', () => {
59
38
  const mockTags = ['tag1', 'tag2', 'tag3'];
60
39
 
@@ -127,13 +106,6 @@ describe('TagsRenderer', () => {
127
106
  expect(container).toBeInTheDocument();
128
107
  });
129
108
 
130
- it('handles empty array', () => {
131
- const { container } = render(
132
- <RendererWrapper {...defaultProps} value={[]} />,
133
- );
134
- expect(container.firstChild).toBeNull();
135
- });
136
-
137
109
  it('cleans up ResizeObserver on unmount', () => {
138
110
  const mockDisconnect = jest.fn();
139
111
  (global.ResizeObserver as jest.Mock).mockImplementation(() => ({
@@ -6,91 +6,120 @@ export const TagsRenderer = (val: unknown): JSX.Element => {
6
6
  const resizeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
7
7
  const [visibleCount, setVisibleCount] = useState<number>(0);
8
8
 
9
- useEffect(() => {
10
- if (!Array.isArray(val) || !containerRef.current || val.length === 0) {
11
- setVisibleCount(0);
9
+ const prevValRef = useRef<string[]>([]);
10
+ const valRef = useRef(val);
11
+ valRef.current = val; // Update on every render
12
+
13
+ // Shared calculation function that uses valRef for current value
14
+ const calculateVisibleItems = (): void => {
15
+ const currentVal = valRef.current;
16
+ const container = containerRef.current;
17
+
18
+ if (!container || !Array.isArray(currentVal) || currentVal.length === 0) {
12
19
  return;
13
20
  }
14
21
 
15
- const calculateVisibleItems = (): void => {
16
- const container = containerRef.current;
17
- if (!container) {
18
- return;
19
- }
22
+ const containerWidth = container.offsetWidth;
23
+ const gap = 5; // Gap between items from SCSS
24
+ const overflowIndicatorWidth = 60; // Approximate width for "... +X"
25
+
26
+ // Measure actual tag widths using hidden temporary elements in the actual container
27
+ const tagWidths: number[] = [];
28
+ const tempElements: HTMLDivElement[] = [];
29
+
30
+ try {
31
+ (currentVal as string[]).forEach((item) => {
32
+ const tempTag = document.createElement('div');
33
+ tempTag.className = classes.tag;
34
+ tempTag.textContent = item;
35
+ tempTag.style.position = 'absolute';
36
+ tempTag.style.visibility = 'hidden';
37
+ tempTag.style.pointerEvents = 'none';
38
+ container.appendChild(tempTag);
39
+ tempElements.push(tempTag);
40
+ tagWidths.push(tempTag.offsetWidth);
41
+ });
42
+ } finally {
43
+ // Clean up temporary elements
44
+ tempElements.forEach((el) => container.removeChild(el));
45
+ }
20
46
 
21
- const containerWidth = container.offsetWidth;
22
- const gap = 5; // Gap between items from SCSS
23
- const overflowIndicatorWidth = 60; // Approximate width for "... +X"
24
-
25
- // Measure actual tag widths using hidden temporary elements in the actual container
26
- const tagWidths: number[] = [];
27
- const tempElements: HTMLDivElement[] = [];
28
-
29
- try {
30
- (val as string[]).forEach((item) => {
31
- const tempTag = document.createElement('div');
32
- tempTag.className = classes.tag;
33
- tempTag.textContent = item;
34
- tempTag.style.position = 'absolute';
35
- tempTag.style.visibility = 'hidden';
36
- tempTag.style.pointerEvents = 'none';
37
- container.appendChild(tempTag);
38
- tempElements.push(tempTag);
39
- tagWidths.push(tempTag.offsetWidth);
40
- });
41
- } finally {
42
- // Clean up temporary elements
43
- tempElements.forEach((el) => container.removeChild(el));
47
+ // Calculate how many items can fit
48
+ let totalWidth = 0;
49
+ let newVisibleCount = 0;
50
+
51
+ for (let i = 0; i < (currentVal as string[]).length; i++) {
52
+ const itemWidth = tagWidths[i] + (i > 0 ? gap : 0);
53
+ const wouldNeedOverflow = i < (currentVal as string[]).length - 1;
54
+ const requiredWidth =
55
+ totalWidth +
56
+ itemWidth +
57
+ (wouldNeedOverflow ? overflowIndicatorWidth + gap : 0);
58
+
59
+ if (requiredWidth <= containerWidth) {
60
+ totalWidth += itemWidth;
61
+ newVisibleCount++;
62
+ } else {
63
+ break;
44
64
  }
65
+ }
45
66
 
46
- // Calculate how many items can fit
47
- let totalWidth = 0;
48
- let newVisibleCount = 0;
67
+ // If we can't fit all items, reserve space for overflow indicator
68
+ if (
69
+ newVisibleCount < (currentVal as string[]).length &&
70
+ newVisibleCount > 0
71
+ ) {
72
+ // Re-calculate with overflow indicator space reserved
73
+ totalWidth = 0;
74
+ newVisibleCount = 0;
49
75
 
50
- for (let i = 0; i < val.length; i++) {
76
+ for (let i = 0; i < (currentVal as string[]).length; i++) {
51
77
  const itemWidth = tagWidths[i] + (i > 0 ? gap : 0);
52
- const wouldNeedOverflow = i < val.length - 1;
53
78
  const requiredWidth =
54
- totalWidth +
55
- itemWidth +
56
- (wouldNeedOverflow ? overflowIndicatorWidth + gap : 0);
79
+ totalWidth + itemWidth + overflowIndicatorWidth + gap;
57
80
 
58
- if (requiredWidth <= containerWidth) {
81
+ if (
82
+ requiredWidth <= containerWidth &&
83
+ i < (currentVal as string[]).length - 1
84
+ ) {
59
85
  totalWidth += itemWidth;
60
86
  newVisibleCount++;
61
87
  } else {
62
88
  break;
63
89
  }
64
90
  }
91
+ }
65
92
 
66
- // If we can't fit all items, reserve space for overflow indicator
67
- if (newVisibleCount < val.length && newVisibleCount > 0) {
68
- // Re-calculate with overflow indicator space reserved
69
- totalWidth = 0;
70
- newVisibleCount = 0;
71
-
72
- for (let i = 0; i < val.length; i++) {
73
- const itemWidth = tagWidths[i] + (i > 0 ? gap : 0);
74
- const requiredWidth =
75
- totalWidth + itemWidth + overflowIndicatorWidth + gap;
76
-
77
- if (requiredWidth <= containerWidth && i < val.length - 1) {
78
- totalWidth += itemWidth;
79
- newVisibleCount++;
80
- } else {
81
- break;
82
- }
83
- }
84
- }
93
+ // Update state with the new visible count
94
+ setVisibleCount(newVisibleCount);
95
+ };
85
96
 
86
- // Update state with the new visible count
87
- setVisibleCount(newVisibleCount);
88
- };
97
+ // Effect 1: Handle tag changes (runs when val changes)
98
+ useEffect(() => {
99
+ if (!Array.isArray(val) || val.length === 0) {
100
+ setVisibleCount(0);
101
+ prevValRef.current = [];
102
+ return;
103
+ }
104
+
105
+ // Check if array contents actually changed
106
+ const valArray = val as string[];
107
+ const hasChanged =
108
+ valArray.length !== prevValRef.current.length ||
109
+ valArray.some((item, index) => item !== prevValRef.current[index]);
110
+
111
+ if (hasChanged) {
112
+ calculateVisibleItems();
113
+ prevValRef.current = [...valArray];
114
+ }
115
+ }, [val]);
89
116
 
90
- // Initial calculation
91
- calculateVisibleItems();
117
+ // Effect 2: Set up ResizeObserver (runs once on mount)
118
+ useEffect(() => {
119
+ if (!containerRef.current) {
120
+ return;
121
+ }
92
122
 
93
- // Debounced resize handler
94
123
  const debouncedCalculate = (): void => {
95
124
  if (resizeTimeoutRef.current) {
96
125
  clearTimeout(resizeTimeoutRef.current);
@@ -109,13 +138,9 @@ export const TagsRenderer = (val: unknown): JSX.Element => {
109
138
  }
110
139
  resizeObserver.disconnect();
111
140
  };
112
- }, [val]);
113
-
114
- if (!Array.isArray(val) || val.length === 0) {
115
- return <></>;
116
- }
141
+ }, []); // Empty deps - only runs once
117
142
 
118
- const stringArray = val as string[];
143
+ const stringArray = Array.isArray(val) ? (val as string[]) : [];
119
144
  const visibleItems = stringArray.slice(0, visibleCount);
120
145
  const hiddenItems = stringArray.slice(visibleCount);
121
146