@qoretechnologies/reqore 0.38.7 → 0.39.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 (40) hide show
  1. package/dist/components/Collection/index.d.ts +4 -1
  2. package/dist/components/Collection/index.d.ts.map +1 -1
  3. package/dist/components/Collection/index.js +32 -14
  4. package/dist/components/Collection/index.js.map +1 -1
  5. package/dist/components/Collection/item.d.ts +1 -0
  6. package/dist/components/Collection/item.d.ts.map +1 -1
  7. package/dist/components/Collection/item.js.map +1 -1
  8. package/dist/components/Dropdown/list.d.ts +1 -0
  9. package/dist/components/Dropdown/list.d.ts.map +1 -1
  10. package/dist/components/Dropdown/list.js +2 -2
  11. package/dist/components/Dropdown/list.js.map +1 -1
  12. package/dist/components/InputClearButton/index.d.ts.map +1 -1
  13. package/dist/components/InputClearButton/index.js +1 -1
  14. package/dist/components/InputClearButton/index.js.map +1 -1
  15. package/dist/components/Menu/divider.d.ts +1 -0
  16. package/dist/components/Menu/divider.d.ts.map +1 -1
  17. package/dist/components/Menu/divider.js +8 -5
  18. package/dist/components/Menu/divider.js.map +1 -1
  19. package/dist/components/Table/helpers.d.ts +1 -1
  20. package/dist/components/Table/helpers.d.ts.map +1 -1
  21. package/dist/components/Table/helpers.js +4 -1
  22. package/dist/components/Table/helpers.js.map +1 -1
  23. package/dist/components/Table/index.d.ts +1 -1
  24. package/dist/components/Table/index.d.ts.map +1 -1
  25. package/dist/components/Textarea/index.d.ts +1 -0
  26. package/dist/components/Textarea/index.d.ts.map +1 -1
  27. package/dist/components/Textarea/index.js +5 -2
  28. package/dist/components/Textarea/index.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/components/Collection/index.tsx +64 -18
  31. package/src/components/Collection/item.tsx +2 -0
  32. package/src/components/Dropdown/list.tsx +6 -1
  33. package/src/components/InputClearButton/index.tsx +0 -1
  34. package/src/components/Menu/divider.tsx +8 -1
  35. package/src/components/Table/helpers.ts +2 -0
  36. package/src/components/Table/index.tsx +1 -1
  37. package/src/components/Textarea/index.tsx +4 -0
  38. package/src/mock/collectionData.tsx +36 -0
  39. package/src/stories/Collection/Collection.stories.tsx +32 -0
  40. package/tests.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qoretechnologies/reqore",
3
- "version": "0.38.7",
3
+ "version": "0.39.0",
4
4
  "description": "ReQore is a highly theme-able and modular UI library for React",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,4 +1,4 @@
1
- import { size } from 'lodash';
1
+ import { map, size } from 'lodash';
2
2
  import { useCallback, useMemo, useState } from 'react';
3
3
  import { useUpdateEffect } from 'react-use';
4
4
  import styled, { css } from 'styled-components';
@@ -12,7 +12,13 @@ import { IReqoreColumnsProps, StyledColumns } from '../Columns';
12
12
  import ReqoreControlGroup from '../ControlGroup';
13
13
  import ReqoreInput, { IReqoreInputProps } from '../Input';
14
14
  import ReqoreMessage from '../Message';
15
- import { IReqorePanelAction, IReqorePanelProps, ReqorePanel, TReqorePanelActions } from '../Panel';
15
+ import {
16
+ IReqorePanelAction,
17
+ IReqorePanelProps,
18
+ IReqorePanelSubAction,
19
+ ReqorePanel,
20
+ TReqorePanelActions,
21
+ } from '../Panel';
16
22
  import { ReqoreVerticalSpacer } from '../Spacer';
17
23
  import { getZoomActions, sizeToZoom, sortTableData, zoomToSize } from '../Table/helpers';
18
24
  import { IReqoreCollectionItemProps, ReqoreCollectionItem } from './item';
@@ -32,6 +38,10 @@ export interface IReqoreCollectionProps extends IReqorePanelProps, IReqoreColumn
32
38
 
33
39
  defaultQuery?: string;
34
40
  defaultZoom?: 0 | 0.5 | 1 | 1.5 | 2;
41
+ defaultSort?: 'asc' | 'desc';
42
+ defaultSortBy?: string;
43
+
44
+ sortKeys?: Record<string, string>;
35
45
 
36
46
  filterable?: boolean;
37
47
  sortable?: boolean;
@@ -103,7 +113,13 @@ export const ReqoreCollection = ({
103
113
  flat = true,
104
114
  minimal,
105
115
  transparent = true,
116
+
106
117
  defaultQuery,
118
+ defaultSort = 'asc',
119
+ defaultSortBy = 'label',
120
+
121
+ sortKeys = {},
122
+
107
123
  onQueryChange,
108
124
  contentRenderer = (children, _items, searchInput) => (
109
125
  <>
@@ -130,7 +146,8 @@ export const ReqoreCollection = ({
130
146
  ...rest
131
147
  }: IReqoreCollectionProps) => {
132
148
  const [_showAs, setShowAs] = useState<'list' | 'grid'>(showAs);
133
- const [sort, setSort] = useState<'asc' | 'desc'>('asc');
149
+ const [sort, setSort] = useState<'asc' | 'desc'>(defaultSort);
150
+ const [sortBy, setSortBy] = useState<string>(defaultSortBy);
134
151
  const [contentRef, setContentRef] = useState<HTMLDivElement>(undefined);
135
152
  const isMobile = useReqoreProperty('isMobile');
136
153
  const [zoom, setZoom] = useState<number>(defaultZoom || sizeToZoom.normal);
@@ -150,6 +167,8 @@ export const ReqoreCollection = ({
150
167
  return items;
151
168
  }
152
169
 
170
+ const _sortBy = !!sortKeys[sortBy] ? (v) => v?.metadata?.[sortBy] : sortBy;
171
+
153
172
  if (showSelectedFirst) {
154
173
  // Filter out the selected items
155
174
  const selectedItems = items.filter((item) => item.selected);
@@ -157,12 +176,12 @@ export const ReqoreCollection = ({
157
176
  const unselectedItems = items.filter((item) => !item.selected);
158
177
  // Sort the selected items
159
178
  const sortedSelectedItems = sortTableData(selectedItems, {
160
- by: 'label',
179
+ by: _sortBy,
161
180
  direction: sort,
162
181
  });
163
182
  // Sort the unselected items
164
183
  const sortedUnselectedItems = sortTableData(unselectedItems, {
165
- by: 'label',
184
+ by: _sortBy,
166
185
  direction: sort,
167
186
  });
168
187
 
@@ -170,10 +189,10 @@ export const ReqoreCollection = ({
170
189
  }
171
190
 
172
191
  return sortTableData(items, {
173
- by: 'label',
192
+ by: _sortBy,
174
193
  direction: sort,
175
194
  });
176
- }, [items, sort]);
195
+ }, [items, sort, sortBy, showSelectedFirst, sortable]);
177
196
 
178
197
  const filteredItems: IReqoreCollectionItemProps[] = useMemo(() => {
179
198
  if (!filterable || query === '') {
@@ -216,17 +235,6 @@ export const ReqoreCollection = ({
216
235
  ],
217
236
  };
218
237
 
219
- if (sortable) {
220
- toolbarGroup.actions.push({
221
- icon: sort === 'desc' ? 'SortDesc' : 'SortAsc',
222
- onClick: () => setSort(sort === 'desc' ? 'asc' : 'desc'),
223
- tooltip: sortButtonTooltip(sort),
224
- label: sortButtonTooltip(sort),
225
- disabled: !size(filteredItems),
226
- ...sortButtonProps,
227
- });
228
- }
229
-
230
238
  if (zoomable) {
231
239
  toolbarGroup.actions = [
232
240
  ...toolbarGroup.actions,
@@ -258,6 +266,44 @@ export const ReqoreCollection = ({
258
266
  ];
259
267
  }
260
268
 
269
+ if (sortable) {
270
+ const _sortKeys = {
271
+ label: 'Label',
272
+ intent: 'Intent',
273
+ ...sortKeys,
274
+ };
275
+
276
+ actions.push({
277
+ icon: sort === 'desc' ? 'SortDesc' : 'SortAsc',
278
+ tooltip: sortButtonTooltip(sort),
279
+ className: 'reqore-collection-sort',
280
+ fixed: true,
281
+ fluid: false,
282
+ disabled: !size(filteredItems),
283
+ actions: [
284
+ {
285
+ divider: true,
286
+ label: 'Sort by',
287
+ dividerAlign: 'left',
288
+ minimal: true,
289
+ dividerPadded: 'bottom',
290
+ },
291
+ ...map(
292
+ _sortKeys,
293
+ (label, key): IReqorePanelSubAction => ({
294
+ label,
295
+ selected: sortBy === key,
296
+ onClick: () => {
297
+ setSortBy(key);
298
+ setSort(sort === 'desc' ? 'asc' : 'desc');
299
+ },
300
+ })
301
+ ),
302
+ ],
303
+ ...sortButtonProps,
304
+ });
305
+ }
306
+
261
307
  return [...actions, toolbarGroup];
262
308
  }, [
263
309
  filterable,
@@ -41,6 +41,8 @@ export interface IReqoreCollectionItemProps
41
41
  maxContentHeight?: number;
42
42
  showContentFade?: boolean;
43
43
  searchString?: string;
44
+ // Used for passing values and sorting
45
+ metadata?: Record<string, any>;
44
46
  }
45
47
 
46
48
  export const StyledCollectionItemContent = styled.div`
@@ -21,6 +21,7 @@ export interface IReqoreDropdownItem
21
21
  onClick?: TDropdownItemOnClick;
22
22
  divider?: boolean;
23
23
  dividerAlign?: 'left' | 'center' | 'right';
24
+ dividerPadded?: 'top' | 'bottom' | 'both' | 'none';
24
25
  line?: boolean;
25
26
  }
26
27
 
@@ -188,12 +189,16 @@ const ReqoreDropdownList = memo(
188
189
  ref={setMenuRef}
189
190
  >
190
191
  {finalItems.map(
191
- ({ dividerAlign, divider, ...item }: IReqoreDropdownItem, index: number) =>
192
+ (
193
+ { dividerAlign, dividerPadded, divider, ...item }: IReqoreDropdownItem,
194
+ index: number
195
+ ) =>
192
196
  divider ? (
193
197
  <ReqoreMenuDivider
194
198
  key={index}
195
199
  {...(item as unknown as IReqoreMenuDividerProps)}
196
200
  align={dividerAlign}
201
+ padded={dividerPadded}
197
202
  />
198
203
  ) : (
199
204
  <ReqoreDropdownItem
@@ -19,7 +19,6 @@ export const StyledInputClearButton = styled(ReqoreIcon)<{ show?: boolean }>`
19
19
  align-items: center;
20
20
  transition: all 0.1s ease-out;
21
21
  cursor: pointer;
22
- display: none;
23
22
 
24
23
  &:hover {
25
24
  opacity: 1 !important;
@@ -14,12 +14,17 @@ export interface IReqoreMenuDividerProps
14
14
  label?: string | number;
15
15
  margin?: 'left' | 'right' | 'both' | 'none';
16
16
  align?: 'left' | 'center' | 'right';
17
+ padded?: 'top' | 'bottom' | 'both' | 'none';
17
18
  line?: boolean;
18
19
  }
19
20
 
20
21
  export const StyledMenuDivider = styled(StyledTextEffect)`
21
22
  width: 100%;
22
- padding: ${({ size }) => `${PADDING_FROM_SIZE[size]}px 0`};
23
+ padding: 0;
24
+ padding-top: ${({ padded, size }) =>
25
+ padded === 'top' || padded === 'both' ? `${PADDING_FROM_SIZE[size]}px` : undefined};
26
+ padding-bottom: ${({ padded, size }) =>
27
+ padded === 'bottom' || padded === 'both' ? `${PADDING_FROM_SIZE[size]}px` : undefined};
23
28
  background-color: transparent;
24
29
 
25
30
  ${({ margin, size }) =>
@@ -48,6 +53,7 @@ const ReqoreMenuDivider = forwardRef<HTMLDivElement, IReqoreMenuDividerProps>(
48
53
  effect,
49
54
  size = 'normal',
50
55
  align = 'center',
56
+ padded = 'both',
51
57
  line,
52
58
  ...rest
53
59
  }: IReqoreMenuDividerProps,
@@ -63,6 +69,7 @@ const ReqoreMenuDivider = forwardRef<HTMLDivElement, IReqoreMenuDividerProps>(
63
69
  <StyledMenuDivider
64
70
  as='div'
65
71
  {...rest}
72
+ padded={padded}
66
73
  size={size}
67
74
  className={`${className || ''} reqore-menu-divider`}
68
75
  ref={ref}
@@ -21,10 +21,12 @@ export const sortTableData = (data: any[], sort: IReqoreTableSort) => {
21
21
 
22
22
  if (thenBy) {
23
23
  return [...data].sort(
24
+ // @ts-expect-error Needed because of the thenby library
24
25
  firstBy(by, { ignoreCase: true, direction }).thenBy(thenBy, { ignoreCase: true, direction })
25
26
  );
26
27
  }
27
28
 
29
+ // @ts-expect-error Needed because of the thenby library
28
30
  return [...data].sort(firstBy(by, { ignoreCase: true, direction }));
29
31
  };
30
32
 
@@ -160,7 +160,7 @@ export interface IReqoreTableStyle {
160
160
  }
161
161
 
162
162
  export interface IReqoreTableSort {
163
- by: string;
163
+ by: string | ((v: any) => any) | ((v1: any, v2: any) => number);
164
164
  thenBy?: string;
165
165
  direction?: 'asc' | 'desc';
166
166
  }
@@ -61,6 +61,7 @@ export interface IReqoreTextareaProps
61
61
 
62
62
  export interface IReqoreTextareaStyle extends IReqoreTextareaProps {
63
63
  theme: IReqoreTheme;
64
+ hasClearButton?: boolean;
64
65
  _size?: TSizes;
65
66
  }
66
67
 
@@ -88,6 +89,8 @@ export const StyledTextarea = styled(StyledEffect)<IReqoreTextareaStyle>`
88
89
  font-size: ${({ _size }) => CONTROL_TEXT_FROM_SIZE[_size]}px;
89
90
  margin: 0;
90
91
  padding: ${({ _size }) => PADDING_FROM_SIZE[_size] / 2}px;
92
+ padding-right: ${({ hasClearButton, _size }) =>
93
+ hasClearButton ? `${SIZE_TO_PX[_size]}px` : undefined};
91
94
  min-height: ${({ _size }) => SIZE_TO_PX[_size]}px;
92
95
  line-height: ${({ _size }) => SIZE_TO_PX[_size] - CONTROL_TEXT_FROM_SIZE[_size]}px;
93
96
  vertical-align: middle;
@@ -218,6 +221,7 @@ const ReqoreInput = forwardRef<HTMLTextAreaElement, IReqoreTextareaProps>(
218
221
  value={_value}
219
222
  readonly={rest?.readOnly}
220
223
  tabIndex={rest?.disabled ? -1 : 0}
224
+ hasClearButton={!rest?.readOnly && !rest?.disabled && !!(onClearClick && onChange)}
221
225
  />
222
226
  <ReqoreInputClearButton
223
227
  enabled={!rest?.readOnly && !rest?.disabled && !!(onClearClick && onChange)}
@@ -45,6 +45,10 @@ export default [
45
45
  },
46
46
  ],
47
47
  expandable: true,
48
+ metadata: {
49
+ id: 23,
50
+ category: 'Article',
51
+ },
48
52
  },
49
53
  {
50
54
  label: 'Test with tooltip',
@@ -69,6 +73,10 @@ export default [
69
73
  icon: 'CodeLine',
70
74
  },
71
75
  ],
76
+ metadata: {
77
+ id: 24,
78
+ category: 'Post',
79
+ },
72
80
  },
73
81
  {
74
82
  icon: 'TextWrap',
@@ -80,6 +88,10 @@ export default [
80
88
  minimal: false,
81
89
  badge: 0,
82
90
  searchString: 'secret',
91
+ metadata: {
92
+ id: 1,
93
+ category: 'None',
94
+ },
83
95
  },
84
96
  {
85
97
  icon: 'ZcoolLine',
@@ -87,6 +99,10 @@ export default [
87
99
  content:
88
100
  'Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long Hello I am a test item content and I am very long so will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long Hello I am a test item content and I am very long so will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long',
89
101
  showContentFade: false,
102
+ metadata: {
103
+ id: 13,
104
+ category: 'Article',
105
+ },
90
106
  },
91
107
  {
92
108
  icon: 'FolderWarningFill',
@@ -114,8 +130,16 @@ export default [
114
130
  onRemoveClick: noop(),
115
131
  },
116
132
  ],
133
+ metadata: {
134
+ id: 99,
135
+ category: 'Post',
136
+ },
117
137
  },
118
138
  {
139
+ metadata: {
140
+ id: 30,
141
+ category: 'Article',
142
+ },
119
143
  label: 'Item with custom content when expanded',
120
144
  tooltip: 'Expandable item with custom content',
121
145
  selected: true,
@@ -234,6 +258,10 @@ export default [
234
258
  ],
235
259
  },
236
260
  {
261
+ metadata: {
262
+ id: 31,
263
+ category: 'None',
264
+ },
237
265
  label: 'I have intent!',
238
266
  transparent: true,
239
267
  minimal: false,
@@ -287,6 +315,10 @@ export default [
287
315
  intent: 'info',
288
316
  },
289
317
  {
318
+ metadata: {
319
+ id: 25,
320
+ category: 'Article',
321
+ },
290
322
  label: 'I have actions',
291
323
  content:
292
324
  'Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long. Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long',
@@ -309,6 +341,10 @@ export default [
309
341
  expandable: true,
310
342
  },
311
343
  {
344
+ metadata: {
345
+ id: 2,
346
+ category: 'None',
347
+ },
312
348
  content:
313
349
  'Hello I am a test item content and I am very long so I will wrap to the next line and I will be very long',
314
350
  expandedActions: [
@@ -5,6 +5,10 @@ import { IReqoreColumnsProps } from '../../components/Columns';
5
5
  import { PADDING_FROM_SIZE } from '../../constants/sizes';
6
6
  import items, { bigCollection } from '../../mock/collectionData';
7
7
 
8
+ import { expect } from '@storybook/jest';
9
+ import { fireEvent } from '@storybook/testing-library';
10
+ import { waitFor } from '@testing-library/react';
11
+ import { sleep } from '../../helpers/utils';
8
12
  import { StoryMeta } from '../utils';
9
13
  import { argManager, IntentArg, SizeArg } from '../utils/args';
10
14
 
@@ -283,3 +287,31 @@ export const DefaultFilter: Story = {
283
287
  ],
284
288
  } as IReqoreCollectionProps,
285
289
  };
290
+
291
+ export const CustomSortKeysWithDefaultSort: Story = {
292
+ args: {
293
+ label: 'Posts',
294
+ items: items.map((item) => ({
295
+ ...item,
296
+ badge: [item.metadata.id, item.metadata.category],
297
+ })),
298
+ defaultSort: 'desc',
299
+ defaultSortBy: 'id',
300
+ sortKeys: {
301
+ id: 'ID',
302
+ category: 'Category',
303
+ },
304
+ },
305
+ play: async ({ canvasElement }) => {
306
+ await sleep(500);
307
+
308
+ await fireEvent.click(canvasElement.querySelector('.reqore-collection-sort'));
309
+
310
+ await waitFor(
311
+ () => expect(document.querySelectorAll('.reqore-popover-content')).toHaveLength(1),
312
+ {
313
+ timeout: 5000,
314
+ }
315
+ );
316
+ },
317
+ };