@drivenets/design-system 0.6.1 → 0.7.0

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.
Files changed (134) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/index.cjs +12394 -8354
  3. package/dist/index.d.cts +806 -118
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +806 -118
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +12376 -8357
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +25 -23
  11. package/src/components/ds-autocomplete/ds-autocomplete.module.scss +2 -1
  12. package/src/components/ds-autocomplete/ds-autocomplete.tsx +47 -40
  13. package/src/components/ds-autocomplete/ds-autocomplete.types.tsx +37 -14
  14. package/src/components/ds-button/versions/ds-button-new/ds-button-new.module.scss +3 -7
  15. package/src/components/ds-comment-bubble/components/ds-comment-thread/ds-comment-thread.module.scss +62 -0
  16. package/src/components/ds-comment-bubble/components/ds-comment-thread/ds-comment-thread.tsx +83 -0
  17. package/src/components/ds-comment-bubble/components/ds-comment-thread/ds-comment-thread.types.ts +12 -0
  18. package/src/components/ds-comment-bubble/components/ds-comment-thread/index.ts +2 -0
  19. package/src/components/ds-comment-bubble/components/ds-thread-item/ds-thread-item.module.scss +149 -0
  20. package/src/components/ds-comment-bubble/components/ds-thread-item/ds-thread-item.tsx +147 -0
  21. package/src/components/ds-comment-bubble/components/ds-thread-item/ds-thread-item.types.ts +15 -0
  22. package/src/components/ds-comment-bubble/components/ds-thread-item/index.ts +2 -0
  23. package/src/components/ds-comment-bubble/ds-comment-bubble.module.scss +298 -0
  24. package/src/components/ds-comment-bubble/ds-comment-bubble.tsx +241 -0
  25. package/src/components/ds-comment-bubble/ds-comment-bubble.types.ts +27 -0
  26. package/src/components/ds-comment-bubble/hooks/index.ts +3 -0
  27. package/src/components/ds-comment-bubble/hooks/use-auto-resize.ts +17 -0
  28. package/src/components/ds-comment-bubble/hooks/use-click-outside.ts +22 -0
  29. package/src/components/ds-comment-bubble/hooks/use-scroll-overflow.ts +27 -0
  30. package/src/components/ds-comment-bubble/index.ts +6 -0
  31. package/src/components/ds-comment-card/ds-comment-card.module.scss +162 -0
  32. package/src/components/ds-comment-card/ds-comment-card.tsx +171 -0
  33. package/src/components/ds-comment-card/ds-comment-card.types.ts +45 -0
  34. package/src/components/ds-comment-card/index.ts +2 -0
  35. package/src/components/ds-comment-indicator/ds-comment-indicator.module.scss +65 -0
  36. package/src/components/ds-comment-indicator/ds-comment-indicator.tsx +48 -0
  37. package/src/components/ds-comment-indicator/ds-comment-indicator.types.ts +13 -0
  38. package/src/components/ds-comment-indicator/index.ts +2 -0
  39. package/src/components/ds-comments-drawer/comments-filters.test.ts +483 -0
  40. package/src/components/ds-comments-drawer/comments-filters.ts +134 -0
  41. package/src/components/ds-comments-drawer/comments-filters.types.ts +14 -0
  42. package/src/components/ds-comments-drawer/components/comments-filter-modal/comments-filter-modal.module.scss +92 -0
  43. package/src/components/ds-comments-drawer/components/comments-filter-modal/comments-filter-modal.tsx +219 -0
  44. package/src/components/ds-comments-drawer/components/comments-filter-modal/comments-filter-modal.types.ts +12 -0
  45. package/src/components/ds-comments-drawer/components/comments-filter-modal/index.ts +2 -0
  46. package/src/components/ds-comments-drawer/ds-comments-drawer.module.scss +67 -0
  47. package/src/components/ds-comments-drawer/ds-comments-drawer.tsx +215 -0
  48. package/src/components/ds-comments-drawer/ds-comments-drawer.types.ts +23 -0
  49. package/src/components/ds-comments-drawer/index.ts +3 -0
  50. package/src/components/ds-dropdown-menu/ds-dropdown-menu.tsx +2 -0
  51. package/src/components/ds-expandable-text-input/ds-expandable-text-input.tsx +8 -4
  52. package/src/components/ds-file-upload/types/accept-types.ts +0 -1
  53. package/src/components/ds-filter-status-icon/ds-filter-status-icon.module.scss +19 -0
  54. package/src/components/ds-filter-status-icon/ds-filter-status-icon.tsx +35 -0
  55. package/src/components/ds-filter-status-icon/ds-filter-status-icon.types.ts +31 -0
  56. package/src/components/ds-filter-status-icon/index.ts +3 -0
  57. package/src/components/ds-icon/custom-icons/index.ts +10 -0
  58. package/src/components/ds-icon/custom-icons/special-failed.tsx +16 -0
  59. package/src/components/ds-icon/custom-icons/special-paused.tsx +16 -0
  60. package/src/components/ds-icon/custom-icons/special-running.tsx +16 -0
  61. package/src/components/ds-icon/custom-icons/special-warning.tsx +16 -0
  62. package/src/components/ds-panel/ds-panel.module.scss +3 -3
  63. package/src/components/ds-progress-arc/ds-progress-arc.module.scss +57 -0
  64. package/src/components/ds-progress-arc/ds-progress-arc.tsx +111 -0
  65. package/src/components/ds-progress-arc/ds-progress-arc.types.ts +34 -0
  66. package/src/components/ds-progress-arc/index.ts +3 -0
  67. package/src/components/ds-progress-arc/utils.test.ts +79 -0
  68. package/src/components/ds-progress-arc/utils.ts +61 -0
  69. package/src/components/ds-progress-donut/ds-progress-donut.module.scss +67 -0
  70. package/src/components/ds-progress-donut/ds-progress-donut.tsx +83 -0
  71. package/src/components/ds-progress-donut/ds-progress-donut.types.ts +38 -0
  72. package/src/components/ds-progress-donut/index.ts +2 -0
  73. package/src/components/ds-progress-donut/utils.ts +1 -0
  74. package/src/components/ds-progress-linear/ds-progress-linear.module.scss +100 -0
  75. package/src/components/ds-progress-linear/ds-progress-linear.tsx +107 -0
  76. package/src/components/ds-progress-linear/ds-progress-linear.types.ts +72 -0
  77. package/src/components/ds-progress-linear/index.ts +2 -0
  78. package/src/components/ds-progress-linear/utils.test.ts +70 -0
  79. package/src/components/ds-progress-linear/utils.ts +7 -0
  80. package/src/components/ds-radio-group/ds-radio-group.tsx +2 -0
  81. package/src/components/ds-select/ds-select.tsx +2 -1
  82. package/src/components/ds-select/ds-select.types.tsx +7 -0
  83. package/src/components/ds-stepper/components/ds-step.tsx +1 -1
  84. package/src/components/ds-stepper/ds-stepper.module.scss +16 -2
  85. package/src/components/ds-stepper/ds-stepper.tsx +3 -1
  86. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +9 -0
  87. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +137 -0
  88. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.types.ts +22 -0
  89. package/src/components/ds-table/components/ds-table-body-virtualized/index.ts +2 -0
  90. package/src/components/ds-table/components/ds-table-cell/ds-table-cell.module.scss +3 -1
  91. package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +7 -10
  92. package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +3 -4
  93. package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +13 -53
  94. package/src/components/ds-table/components/ds-table-row/ds-table-row.tsx +32 -85
  95. package/src/components/ds-table/components/ds-table-row/ds-table-row.types.ts +0 -6
  96. package/src/components/ds-table/components/ds-table-row-expandable-cell/ds-table-row-expandable-cell.module.scss +10 -0
  97. package/src/components/ds-table/components/ds-table-row-expandable-cell/ds-table-row-expandable-cell.tsx +45 -0
  98. package/src/components/ds-table/components/ds-table-row-expandable-cell/ds-table-row-expandable-cell.types.ts +7 -0
  99. package/src/components/ds-table/components/ds-table-row-expandable-cell/index.ts +2 -0
  100. package/src/components/ds-table/components/ds-table-row-selectable-cell/ds-table-row-selectable-cell.module.scss +6 -0
  101. package/src/components/ds-table/components/ds-table-row-selectable-cell/ds-table-row-selectable-cell.tsx +30 -0
  102. package/src/components/ds-table/components/ds-table-row-selectable-cell/ds-table-row-selectable-cell.types.ts +6 -0
  103. package/src/components/ds-table/components/ds-table-row-selectable-cell/index.ts +2 -0
  104. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.module.scss +80 -0
  105. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +95 -0
  106. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.types.ts +11 -0
  107. package/src/components/ds-table/components/ds-table-row-virtualized/index.ts +2 -0
  108. package/src/components/ds-table/context/ds-table-context.tsx +0 -9
  109. package/src/components/ds-table/ds-table.module.scss +4 -17
  110. package/src/components/ds-table/ds-table.tsx +44 -77
  111. package/src/components/ds-table/ds-table.types.ts +57 -12
  112. package/src/components/ds-table/index.ts +1 -0
  113. package/src/components/ds-table/styles/_mixins.scss +15 -0
  114. package/src/components/ds-table/styles/_variables.scss +2 -2
  115. package/src/components/ds-table/utils/{column-styling.ts → column-size.ts} +4 -14
  116. package/src/components/ds-table/utils/constants.ts +4 -0
  117. package/src/components/ds-tag/index.tsx +1 -0
  118. package/src/components/ds-tag-filter/ds-tag-filter.module.scss +13 -15
  119. package/src/components/ds-tag-filter/ds-tag-filter.tsx +38 -72
  120. package/src/components/ds-tag-filter/ds-tag-filter.types.ts +2 -3
  121. package/src/components/ds-tag-filter/hooks/use-tag-overflow-calculation.ts +12 -62
  122. package/src/components/ds-tag-filter/utils.test.ts +2 -108
  123. package/src/components/ds-tag-filter/utils.ts +2 -12
  124. package/src/components/ds-tree/ds-tree.module.scss +283 -0
  125. package/src/components/ds-tree/ds-tree.tsx +172 -0
  126. package/src/components/ds-tree/ds-tree.types.ts +83 -0
  127. package/src/components/ds-tree/ds-tree.utils.ts +11 -0
  128. package/src/components/ds-tree/index.ts +3 -0
  129. package/src/index.ts +9 -0
  130. package/src/utils/format-relative-time.test.ts +74 -0
  131. package/src/utils/format-relative-time.ts +52 -0
  132. package/src/utils/numbers.test.ts +24 -0
  133. package/src/utils/numbers.ts +1 -0
  134. package/src/components/ds-table/utils/create-columns-grid-template.ts +0 -23
@@ -0,0 +1,483 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { CommentData } from '../ds-comment-card';
3
+ import type { CommentsFilterState, FilterTag } from './comments-filters.types';
4
+ import { applyFilters, filtersToTags, removeTag } from './comments-filters';
5
+
6
+ const makeAuthor = (id: string, name: string) => ({ id, name });
7
+
8
+ const makeComment = (overrides: Partial<CommentData> = {}): CommentData => ({
9
+ id: '1',
10
+ numericId: 1,
11
+ author: makeAuthor('user-1', 'Alice'),
12
+ createdAt: new Date('2025-06-15'),
13
+ isResolved: false,
14
+ messages: [],
15
+ ...overrides,
16
+ });
17
+
18
+ const emptyFilters: CommentsFilterState = {
19
+ authors: [],
20
+ labels: [],
21
+ statuses: [],
22
+ };
23
+
24
+ const neverActionRequired = () => false;
25
+ const alwaysActionRequired = () => true;
26
+
27
+ describe('comments-filters', () => {
28
+ describe('applyFilters', () => {
29
+ it('should return all comments when no filters are set', () => {
30
+ const comments = [makeComment({ id: '1' }), makeComment({ id: '2' })];
31
+
32
+ const result = applyFilters(comments, emptyFilters, neverActionRequired);
33
+
34
+ expect(result).toHaveLength(2);
35
+ });
36
+
37
+ it('should filter by resolved status', () => {
38
+ const comments = [
39
+ makeComment({ id: '1', isResolved: true }),
40
+ makeComment({ id: '2', isResolved: false }),
41
+ ];
42
+ const filters: CommentsFilterState = { ...emptyFilters, statuses: ['resolved'] };
43
+
44
+ const result = applyFilters(comments, filters, neverActionRequired);
45
+
46
+ expect(result).toHaveLength(1);
47
+ expect(result.at(0)?.id).toBe('1');
48
+ });
49
+
50
+ it('should filter by unresolved status', () => {
51
+ const comments = [
52
+ makeComment({ id: '1', isResolved: true }),
53
+ makeComment({ id: '2', isResolved: false }),
54
+ ];
55
+ const filters: CommentsFilterState = { ...emptyFilters, statuses: ['unresolved'] };
56
+
57
+ const result = applyFilters(comments, filters, neverActionRequired);
58
+
59
+ expect(result).toHaveLength(1);
60
+ expect(result.at(0)?.id).toBe('2');
61
+ });
62
+
63
+ it('should filter by action-required status using the callback', () => {
64
+ const comments = [
65
+ makeComment({ id: '1', isResolved: false }),
66
+ makeComment({ id: '2', isResolved: false }),
67
+ ];
68
+ const filters: CommentsFilterState = { ...emptyFilters, statuses: ['action-required'] };
69
+ const getIsActionRequired = (c: CommentData) => c.id === '1';
70
+
71
+ const result = applyFilters(comments, filters, getIsActionRequired);
72
+
73
+ expect(result).toHaveLength(1);
74
+ expect(result.at(0)?.id).toBe('1');
75
+ });
76
+
77
+ it('should include comment matching any of multiple statuses', () => {
78
+ const comments = [
79
+ makeComment({ id: '1', isResolved: true }),
80
+ makeComment({ id: '2', isResolved: false }),
81
+ ];
82
+ const filters: CommentsFilterState = { ...emptyFilters, statuses: ['resolved', 'unresolved'] };
83
+
84
+ const result = applyFilters(comments, filters, neverActionRequired);
85
+
86
+ expect(result).toHaveLength(2);
87
+ });
88
+
89
+ it('should match an unresolved action-required comment for either status', () => {
90
+ const comment = makeComment({ id: '1', isResolved: false });
91
+ const filters: CommentsFilterState = { ...emptyFilters, statuses: ['action-required'] };
92
+
93
+ const result = applyFilters([comment], filters, alwaysActionRequired);
94
+
95
+ expect(result).toHaveLength(1);
96
+ });
97
+
98
+ it('should filter by author', () => {
99
+ const comments = [
100
+ makeComment({ id: '1', author: makeAuthor('user-1', 'Alice') }),
101
+ makeComment({ id: '2', author: makeAuthor('user-2', 'Bob') }),
102
+ makeComment({ id: '3', author: makeAuthor('user-1', 'Alice') }),
103
+ ];
104
+ const filters: CommentsFilterState = { ...emptyFilters, authors: ['user-1'] };
105
+
106
+ const result = applyFilters(comments, filters, neverActionRequired);
107
+
108
+ expect(result).toHaveLength(2);
109
+ expect(result.map((c) => c.id)).toEqual(['1', '3']);
110
+ });
111
+
112
+ it('should filter by multiple authors', () => {
113
+ const comments = [
114
+ makeComment({ id: '1', author: makeAuthor('user-1', 'Alice') }),
115
+ makeComment({ id: '2', author: makeAuthor('user-2', 'Bob') }),
116
+ makeComment({ id: '3', author: makeAuthor('user-3', 'Carol') }),
117
+ ];
118
+ const filters: CommentsFilterState = { ...emptyFilters, authors: ['user-1', 'user-3'] };
119
+
120
+ const result = applyFilters(comments, filters, neverActionRequired);
121
+
122
+ expect(result).toHaveLength(2);
123
+ expect(result.map((c) => c.id)).toEqual(['1', '3']);
124
+ });
125
+
126
+ it('should filter by dateFrom', () => {
127
+ const comments = [
128
+ makeComment({ id: '1', createdAt: new Date('2025-01-01') }),
129
+ makeComment({ id: '2', createdAt: new Date('2025-06-15') }),
130
+ makeComment({ id: '3', createdAt: new Date('2025-12-31') }),
131
+ ];
132
+ const filters: CommentsFilterState = { ...emptyFilters, dateFrom: '2025-06-01' };
133
+
134
+ const result = applyFilters(comments, filters, neverActionRequired);
135
+
136
+ expect(result).toHaveLength(2);
137
+ expect(result.map((c) => c.id)).toEqual(['2', '3']);
138
+ });
139
+
140
+ it('should filter by dateTo (inclusive end of day)', () => {
141
+ const comments = [
142
+ makeComment({ id: '1', createdAt: new Date('2025-01-01') }),
143
+ makeComment({ id: '2', createdAt: new Date('2025-06-15T23:59:59.999') }),
144
+ makeComment({ id: '3', createdAt: new Date('2025-12-31') }),
145
+ ];
146
+ const filters: CommentsFilterState = { ...emptyFilters, dateTo: '2025-06-15' };
147
+
148
+ const result = applyFilters(comments, filters, neverActionRequired);
149
+
150
+ expect(result).toHaveLength(2);
151
+ expect(result.map((c) => c.id)).toEqual(['1', '2']);
152
+ });
153
+
154
+ it('should filter by date range', () => {
155
+ const comments = [
156
+ makeComment({ id: '1', createdAt: new Date('2025-01-01') }),
157
+ makeComment({ id: '2', createdAt: new Date('2025-06-15') }),
158
+ makeComment({ id: '3', createdAt: new Date('2025-12-31') }),
159
+ ];
160
+ const filters: CommentsFilterState = {
161
+ ...emptyFilters,
162
+ dateFrom: '2025-03-01',
163
+ dateTo: '2025-09-01',
164
+ };
165
+
166
+ const result = applyFilters(comments, filters, neverActionRequired);
167
+
168
+ expect(result).toHaveLength(1);
169
+ expect(result.at(0)?.id).toBe('2');
170
+ });
171
+
172
+ it('should filter by labels', () => {
173
+ const comments = [
174
+ makeComment({ id: '1', labels: ['bug', 'critical'] }),
175
+ makeComment({ id: '2', labels: ['feature'] }),
176
+ makeComment({ id: '3' }),
177
+ ];
178
+ const filters: CommentsFilterState = { ...emptyFilters, labels: ['bug'] };
179
+
180
+ const result = applyFilters(comments, filters, neverActionRequired);
181
+
182
+ expect(result).toHaveLength(1);
183
+ expect(result.at(0)?.id).toBe('1');
184
+ });
185
+
186
+ it('should match when comment has any of the filtered labels', () => {
187
+ const comments = [
188
+ makeComment({ id: '1', labels: ['bug'] }),
189
+ makeComment({ id: '2', labels: ['feature', 'enhancement'] }),
190
+ ];
191
+ const filters: CommentsFilterState = { ...emptyFilters, labels: ['enhancement', 'critical'] };
192
+
193
+ const result = applyFilters(comments, filters, neverActionRequired);
194
+
195
+ expect(result).toHaveLength(1);
196
+ expect(result.at(0)?.id).toBe('2');
197
+ });
198
+
199
+ it('should exclude comments with no labels when filtering by labels', () => {
200
+ const comments = [makeComment({ id: '1' }), makeComment({ id: '2', labels: undefined })];
201
+ const filters: CommentsFilterState = { ...emptyFilters, labels: ['bug'] };
202
+
203
+ const result = applyFilters(comments, filters, neverActionRequired);
204
+
205
+ expect(result).toHaveLength(0);
206
+ });
207
+
208
+ it('should combine multiple filter types (AND logic)', () => {
209
+ const comments = [
210
+ makeComment({ id: '1', author: makeAuthor('user-1', 'Alice'), labels: ['bug'], isResolved: false }),
211
+ makeComment({
212
+ id: '2',
213
+ author: makeAuthor('user-1', 'Alice'),
214
+ labels: ['feature'],
215
+ isResolved: false,
216
+ }),
217
+ makeComment({ id: '3', author: makeAuthor('user-2', 'Bob'), labels: ['bug'], isResolved: false }),
218
+ ];
219
+ const filters: CommentsFilterState = {
220
+ authors: ['user-1'],
221
+ labels: ['bug'],
222
+ statuses: ['unresolved'],
223
+ };
224
+
225
+ const result = applyFilters(comments, filters, neverActionRequired);
226
+
227
+ expect(result).toHaveLength(1);
228
+ expect(result.at(0)?.id).toBe('1');
229
+ });
230
+
231
+ it('should return empty array when no comments match', () => {
232
+ const comments = [makeComment({ id: '1', isResolved: false })];
233
+ const filters: CommentsFilterState = { ...emptyFilters, statuses: ['resolved'] };
234
+
235
+ const result = applyFilters(comments, filters, neverActionRequired);
236
+
237
+ expect(result).toHaveLength(0);
238
+ });
239
+
240
+ it('should handle empty comments array', () => {
241
+ const filters: CommentsFilterState = { ...emptyFilters, statuses: ['resolved'] };
242
+
243
+ const result = applyFilters([], filters, neverActionRequired);
244
+
245
+ expect(result).toHaveLength(0);
246
+ });
247
+ });
248
+
249
+ describe('filtersToTags', () => {
250
+ const authorMap = new Map([
251
+ ['user-1', 'Alice'],
252
+ ['user-2', 'Bob'],
253
+ ]);
254
+
255
+ it('should return empty array for empty filters', () => {
256
+ const result = filtersToTags(emptyFilters, authorMap);
257
+
258
+ expect(result).toEqual([]);
259
+ });
260
+
261
+ it('should create tags for statuses with proper label formatting', () => {
262
+ const filters: CommentsFilterState = {
263
+ ...emptyFilters,
264
+ statuses: ['resolved', 'unresolved', 'action-required'],
265
+ };
266
+
267
+ const result = filtersToTags(filters, authorMap);
268
+
269
+ expect(result).toHaveLength(3);
270
+ expect(result.at(0)).toEqual({
271
+ id: 'status-resolved',
272
+ label: 'Status: Resolved',
273
+ filterId: 'statuses',
274
+ value: 'resolved',
275
+ });
276
+ expect(result.at(1)).toEqual({
277
+ id: 'status-unresolved',
278
+ label: 'Status: Unresolved',
279
+ filterId: 'statuses',
280
+ value: 'unresolved',
281
+ });
282
+ expect(result.at(2)).toEqual({
283
+ id: 'status-action-required',
284
+ label: 'Status: Action required',
285
+ filterId: 'statuses',
286
+ value: 'action-required',
287
+ });
288
+ });
289
+
290
+ it('should create tags for authors using authorMap names', () => {
291
+ const filters: CommentsFilterState = { ...emptyFilters, authors: ['user-1', 'user-2'] };
292
+
293
+ const result = filtersToTags(filters, authorMap);
294
+
295
+ expect(result).toHaveLength(2);
296
+ expect(result.at(0)).toEqual({
297
+ id: 'author-user-1',
298
+ label: 'Author: Alice',
299
+ filterId: 'authors',
300
+ value: 'user-1',
301
+ });
302
+ expect(result.at(1)).toEqual({
303
+ id: 'author-user-2',
304
+ label: 'Author: Bob',
305
+ filterId: 'authors',
306
+ value: 'user-2',
307
+ });
308
+ });
309
+
310
+ it('should fall back to author id when not found in authorMap', () => {
311
+ const filters: CommentsFilterState = { ...emptyFilters, authors: ['unknown-user'] };
312
+
313
+ const result = filtersToTags(filters, authorMap);
314
+
315
+ expect(result).toHaveLength(1);
316
+ expect(result.at(0)?.label).toBe('Author: unknown-user');
317
+ });
318
+
319
+ it('should create date range tag with both dates', () => {
320
+ const filters: CommentsFilterState = {
321
+ ...emptyFilters,
322
+ dateFrom: '2025-01-01',
323
+ dateTo: '2025-12-31',
324
+ };
325
+
326
+ const result = filtersToTags(filters, authorMap);
327
+
328
+ expect(result).toHaveLength(1);
329
+ expect(result.at(0)).toEqual({
330
+ id: 'date-range',
331
+ label: 'Date: 2025-01-01 to 2025-12-31',
332
+ filterId: 'date',
333
+ value: 'range',
334
+ });
335
+ });
336
+
337
+ it('should create date range tag with only dateFrom', () => {
338
+ const filters: CommentsFilterState = { ...emptyFilters, dateFrom: '2025-01-01' };
339
+
340
+ const result = filtersToTags(filters, authorMap);
341
+
342
+ expect(result).toHaveLength(1);
343
+ expect(result.at(0)?.label).toBe('Date: 2025-01-01 to ...');
344
+ });
345
+
346
+ it('should create date range tag with only dateTo', () => {
347
+ const filters: CommentsFilterState = { ...emptyFilters, dateTo: '2025-12-31' };
348
+
349
+ const result = filtersToTags(filters, authorMap);
350
+
351
+ expect(result).toHaveLength(1);
352
+ expect(result.at(0)?.label).toBe('Date: ... to 2025-12-31');
353
+ });
354
+
355
+ it('should not create date tag when neither dateFrom nor dateTo is set', () => {
356
+ const result = filtersToTags(emptyFilters, authorMap);
357
+
358
+ expect(result.filter((t) => t.filterId === 'date')).toHaveLength(0);
359
+ });
360
+
361
+ it('should create tags for labels', () => {
362
+ const filters: CommentsFilterState = { ...emptyFilters, labels: ['bug', 'feature'] };
363
+
364
+ const result = filtersToTags(filters, authorMap);
365
+
366
+ expect(result).toHaveLength(2);
367
+ expect(result.at(0)).toEqual({
368
+ id: 'label-bug',
369
+ label: 'Label: bug',
370
+ filterId: 'labels',
371
+ value: 'bug',
372
+ });
373
+ expect(result.at(1)).toEqual({
374
+ id: 'label-feature',
375
+ label: 'Label: feature',
376
+ filterId: 'labels',
377
+ value: 'feature',
378
+ });
379
+ });
380
+
381
+ it('should create tags for all filter types in order: statuses, authors, date, labels', () => {
382
+ const filters: CommentsFilterState = {
383
+ statuses: ['resolved'],
384
+ authors: ['user-1'],
385
+ dateFrom: '2025-01-01',
386
+ dateTo: '2025-12-31',
387
+ labels: ['bug'],
388
+ };
389
+
390
+ const result = filtersToTags(filters, authorMap);
391
+
392
+ expect(result).toHaveLength(4);
393
+ expect(result.map((t) => t.filterId)).toEqual(['statuses', 'authors', 'date', 'labels']);
394
+ });
395
+ });
396
+
397
+ describe('removeTag', () => {
398
+ it('should remove a status tag', () => {
399
+ const filters: CommentsFilterState = {
400
+ ...emptyFilters,
401
+ statuses: ['resolved', 'unresolved'],
402
+ };
403
+ const tag: FilterTag = {
404
+ id: 'status-resolved',
405
+ label: 'Status: Resolved',
406
+ filterId: 'statuses',
407
+ value: 'resolved',
408
+ };
409
+
410
+ const result = removeTag(filters, tag);
411
+
412
+ expect(result.statuses).toEqual(['unresolved']);
413
+ });
414
+
415
+ it('should remove an author tag', () => {
416
+ const filters: CommentsFilterState = {
417
+ ...emptyFilters,
418
+ authors: ['user-1', 'user-2'],
419
+ };
420
+ const tag: FilterTag = {
421
+ id: 'author-user-1',
422
+ label: 'Author: Alice',
423
+ filterId: 'authors',
424
+ value: 'user-1',
425
+ };
426
+
427
+ const result = removeTag(filters, tag);
428
+
429
+ expect(result.authors).toEqual(['user-2']);
430
+ });
431
+
432
+ it('should remove a label tag', () => {
433
+ const filters: CommentsFilterState = {
434
+ ...emptyFilters,
435
+ labels: ['bug', 'feature'],
436
+ };
437
+ const tag: FilterTag = { id: 'label-bug', label: 'Label: bug', filterId: 'labels', value: 'bug' };
438
+
439
+ const result = removeTag(filters, tag);
440
+
441
+ expect(result.labels).toEqual(['feature']);
442
+ });
443
+
444
+ it('should clear both dateFrom and dateTo when removing a date tag', () => {
445
+ const filters: CommentsFilterState = {
446
+ ...emptyFilters,
447
+ dateFrom: '2025-01-01',
448
+ dateTo: '2025-12-31',
449
+ };
450
+ const tag: FilterTag = {
451
+ id: 'date-range',
452
+ label: 'Date: 2025-01-01 to 2025-12-31',
453
+ filterId: 'date',
454
+ value: 'range',
455
+ };
456
+
457
+ const result = removeTag(filters, tag);
458
+
459
+ expect(result.dateFrom).toBeUndefined();
460
+ expect(result.dateTo).toBeUndefined();
461
+ });
462
+
463
+ it('should not mutate the original filter object', () => {
464
+ const filters: CommentsFilterState = {
465
+ authors: ['user-1'],
466
+ labels: ['bug'],
467
+ statuses: ['resolved'],
468
+ };
469
+ const tag: FilterTag = {
470
+ id: 'status-resolved',
471
+ label: 'Status: Resolved',
472
+ filterId: 'statuses',
473
+ value: 'resolved',
474
+ };
475
+
476
+ const result = removeTag(filters, tag);
477
+
478
+ expect(filters.statuses).toEqual(['resolved']);
479
+ expect(result.statuses).toEqual([]);
480
+ expect(result).not.toBe(filters);
481
+ });
482
+ });
483
+ });
@@ -0,0 +1,134 @@
1
+ import type { CommentData } from '../ds-comment-card';
2
+ import type { FilterTag, CommentsFilterState } from './comments-filters.types';
3
+
4
+ export type { FilterTag, CommentsFilterState } from './comments-filters.types';
5
+
6
+ export const initialFilterState: CommentsFilterState = {
7
+ authors: [],
8
+ labels: [],
9
+ statuses: [],
10
+ };
11
+
12
+ export const applyFilters = (
13
+ comments: CommentData[],
14
+ filters: CommentsFilterState,
15
+ getIsActionRequired: (comment: CommentData) => boolean,
16
+ ): CommentData[] => {
17
+ return comments.filter((comment) => {
18
+ if (filters.statuses.length > 0) {
19
+ const commentStatuses: ('unresolved' | 'resolved' | 'action-required')[] = [];
20
+
21
+ if (comment.isResolved) {
22
+ commentStatuses.push('resolved');
23
+ } else {
24
+ commentStatuses.push('unresolved');
25
+ }
26
+
27
+ if (getIsActionRequired(comment)) {
28
+ commentStatuses.push('action-required');
29
+ }
30
+
31
+ const hasMatchingStatus = filters.statuses.some((status) => commentStatuses.includes(status));
32
+ if (!hasMatchingStatus) {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ if (filters.authors.length > 0) {
38
+ if (!filters.authors.includes(comment.author.id)) {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ if (filters.dateFrom) {
44
+ const fromDate = new Date(filters.dateFrom);
45
+ if (comment.createdAt < fromDate) {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ if (filters.dateTo) {
51
+ const toDate = new Date(filters.dateTo);
52
+ toDate.setHours(23, 59, 59, 999);
53
+ if (comment.createdAt > toDate) {
54
+ return false;
55
+ }
56
+ }
57
+ if (filters.labels.length > 0) {
58
+ if (!comment.labels || !filters.labels.some((label) => comment.labels?.includes(label))) {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ return true;
64
+ });
65
+ };
66
+
67
+ export const filtersToTags = (filters: CommentsFilterState, authorMap: Map<string, string>): FilterTag[] => {
68
+ const tags: FilterTag[] = [];
69
+
70
+ filters.statuses.forEach((status) => {
71
+ const label =
72
+ status === 'action-required' ? 'Action required' : status.charAt(0).toUpperCase() + status.slice(1);
73
+ tags.push({
74
+ id: `status-${status}`,
75
+ label: `Status: ${label}`,
76
+ filterId: 'statuses',
77
+ value: status,
78
+ });
79
+ });
80
+
81
+ filters.authors.forEach((authorId) => {
82
+ const authorName = authorMap.get(authorId) || authorId;
83
+ tags.push({
84
+ id: `author-${authorId}`,
85
+ label: `Author: ${authorName}`,
86
+ filterId: 'authors',
87
+ value: authorId,
88
+ });
89
+ });
90
+
91
+ if (filters.dateFrom || filters.dateTo) {
92
+ const from = filters.dateFrom || '...';
93
+ const to = filters.dateTo || '...';
94
+ tags.push({
95
+ id: 'date-range',
96
+ label: `Date: ${from} to ${to}`,
97
+ filterId: 'date',
98
+ value: 'range',
99
+ });
100
+ }
101
+
102
+ filters.labels.forEach((label) => {
103
+ tags.push({
104
+ id: `label-${label}`,
105
+ label: `Label: ${label}`,
106
+ filterId: 'labels',
107
+ value: label,
108
+ });
109
+ });
110
+
111
+ return tags;
112
+ };
113
+
114
+ export const removeTag = (filters: CommentsFilterState, tag: FilterTag): CommentsFilterState => {
115
+ const newFilters = { ...filters };
116
+
117
+ switch (tag.filterId) {
118
+ case 'statuses':
119
+ newFilters.statuses = newFilters.statuses.filter((s) => s !== tag.value);
120
+ break;
121
+ case 'authors':
122
+ newFilters.authors = newFilters.authors.filter((a) => a !== tag.value);
123
+ break;
124
+ case 'labels':
125
+ newFilters.labels = newFilters.labels.filter((l) => l !== tag.value);
126
+ break;
127
+ case 'date':
128
+ newFilters.dateFrom = undefined;
129
+ newFilters.dateTo = undefined;
130
+ break;
131
+ }
132
+
133
+ return newFilters;
134
+ };
@@ -0,0 +1,14 @@
1
+ export interface FilterTag {
2
+ id: string;
3
+ label: string;
4
+ filterId: string;
5
+ value: string;
6
+ }
7
+
8
+ export interface CommentsFilterState {
9
+ authors: string[];
10
+ dateFrom?: string;
11
+ dateTo?: string;
12
+ labels: string[];
13
+ statuses: ('unresolved' | 'resolved' | 'action-required')[];
14
+ }