@pareto-engineering/design-system 2.0.0-alpha.45 → 2.0.0-alpha.48

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 (25) hide show
  1. package/dist/cjs/f/FormInput/FormInput.js +8 -1
  2. package/dist/cjs/f/fields/QueryCombobox/QueryCombobox.js +29 -6
  3. package/dist/cjs/f/fields/QueryCombobox/common/Combobox/Combobox.js +29 -5
  4. package/dist/cjs/f/fields/QueryCombobox/common/MultipleCombobox/MultipleCombobox.js +101 -26
  5. package/dist/cjs/f/fields/QueryCombobox/common/index.js +9 -1
  6. package/dist/cjs/f/fields/QueryCombobox/styles.scss +24 -5
  7. package/dist/es/f/FormInput/FormInput.js +9 -2
  8. package/dist/es/f/fields/QueryCombobox/QueryCombobox.js +30 -7
  9. package/dist/es/f/fields/QueryCombobox/common/Combobox/Combobox.js +30 -6
  10. package/dist/es/f/fields/QueryCombobox/common/MultipleCombobox/MultipleCombobox.js +101 -27
  11. package/dist/es/f/fields/QueryCombobox/common/index.js +2 -1
  12. package/dist/es/f/fields/QueryCombobox/styles.scss +24 -5
  13. package/package.json +2 -2
  14. package/src/__snapshots__/Storyshots.test.js.snap +379 -14
  15. package/src/local.scss +3 -3
  16. package/src/stories/f/FormInput.stories.jsx +115 -0
  17. package/src/stories/f/QueryCombobox.stories.jsx +55 -8
  18. package/src/stories/f/__generated__/FormInputAllTeamsQuery.graphql.js +139 -0
  19. package/src/ui/f/FormInput/FormInput.jsx +11 -0
  20. package/src/ui/f/fields/QueryCombobox/QueryCombobox.jsx +29 -6
  21. package/src/ui/f/fields/QueryCombobox/common/Combobox/Combobox.jsx +27 -3
  22. package/src/ui/f/fields/QueryCombobox/common/MultipleCombobox/MultipleCombobox.jsx +318 -0
  23. package/src/ui/f/fields/QueryCombobox/common/MultipleCombobox/index.js +2 -0
  24. package/src/ui/f/fields/QueryCombobox/common/index.js +1 -0
  25. package/src/ui/f/fields/QueryCombobox/styles.scss +24 -5
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @flow
3
+ */
4
+
5
+ /* eslint-disable */
6
+
7
+ 'use strict';
8
+
9
+ /*::
10
+ import type { ConcreteRequest } from 'relay-runtime';
11
+ export type FormInputAllTeamsQueryVariables = {|
12
+ name_Icontains?: ?string
13
+ |};
14
+ export type FormInputAllTeamsQueryResponse = {|
15
+ +allTeams: ?{|
16
+ +edges: $ReadOnlyArray<?{|
17
+ +node: ?{|
18
+ +id: string,
19
+ +name: string,
20
+ |}
21
+ |}>
22
+ |}
23
+ |};
24
+ export type FormInputAllTeamsQuery = {|
25
+ variables: FormInputAllTeamsQueryVariables,
26
+ response: FormInputAllTeamsQueryResponse,
27
+ |};
28
+ */
29
+
30
+
31
+ /*
32
+ query FormInputAllTeamsQuery(
33
+ $name_Icontains: String
34
+ ) {
35
+ allTeams(name_Icontains: $name_Icontains) {
36
+ edges {
37
+ node {
38
+ id
39
+ name
40
+ }
41
+ }
42
+ }
43
+ }
44
+ */
45
+
46
+ const node/*: ConcreteRequest*/ = (function(){
47
+ var v0 = [
48
+ {
49
+ "defaultValue": null,
50
+ "kind": "LocalArgument",
51
+ "name": "name_Icontains"
52
+ }
53
+ ],
54
+ v1 = [
55
+ {
56
+ "alias": null,
57
+ "args": [
58
+ {
59
+ "kind": "Variable",
60
+ "name": "name_Icontains",
61
+ "variableName": "name_Icontains"
62
+ }
63
+ ],
64
+ "concreteType": "TeamNodeConnection",
65
+ "kind": "LinkedField",
66
+ "name": "allTeams",
67
+ "plural": false,
68
+ "selections": [
69
+ {
70
+ "alias": null,
71
+ "args": null,
72
+ "concreteType": "TeamNodeEdge",
73
+ "kind": "LinkedField",
74
+ "name": "edges",
75
+ "plural": true,
76
+ "selections": [
77
+ {
78
+ "alias": null,
79
+ "args": null,
80
+ "concreteType": "TeamNode",
81
+ "kind": "LinkedField",
82
+ "name": "node",
83
+ "plural": false,
84
+ "selections": [
85
+ {
86
+ "alias": null,
87
+ "args": null,
88
+ "kind": "ScalarField",
89
+ "name": "id",
90
+ "storageKey": null
91
+ },
92
+ {
93
+ "alias": null,
94
+ "args": null,
95
+ "kind": "ScalarField",
96
+ "name": "name",
97
+ "storageKey": null
98
+ }
99
+ ],
100
+ "storageKey": null
101
+ }
102
+ ],
103
+ "storageKey": null
104
+ }
105
+ ],
106
+ "storageKey": null
107
+ }
108
+ ];
109
+ return {
110
+ "fragment": {
111
+ "argumentDefinitions": (v0/*: any*/),
112
+ "kind": "Fragment",
113
+ "metadata": null,
114
+ "name": "FormInputAllTeamsQuery",
115
+ "selections": (v1/*: any*/),
116
+ "type": "Query",
117
+ "abstractKey": null
118
+ },
119
+ "kind": "Request",
120
+ "operation": {
121
+ "argumentDefinitions": (v0/*: any*/),
122
+ "kind": "Operation",
123
+ "name": "FormInputAllTeamsQuery",
124
+ "selections": (v1/*: any*/)
125
+ },
126
+ "params": {
127
+ "cacheID": "dc9287c6d087d0f0e1be2e8ef405cb1f",
128
+ "id": null,
129
+ "metadata": {},
130
+ "name": "FormInputAllTeamsQuery",
131
+ "operationKind": "query",
132
+ "text": "query FormInputAllTeamsQuery(\n $name_Icontains: String\n) {\n allTeams(name_Icontains: $name_Icontains) {\n edges {\n node {\n id\n name\n }\n }\n }\n}\n"
133
+ }
134
+ };
135
+ })();
136
+ // prettier-ignore
137
+ (node/*: any*/).hash = 'c76b6f84436895e8a6d0725bdd41cfe0';
138
+
139
+ module.exports = node;
@@ -10,6 +10,7 @@ import {
10
10
  TextareaInput,
11
11
  ChoicesInput,
12
12
  SelectInput,
13
+ QueryCombobox,
13
14
  } from '../fields'
14
15
 
15
16
  // Local Definitions
@@ -64,6 +65,15 @@ const FormInput = ({
64
65
  />
65
66
  )
66
67
  }
68
+ if (type === 'query-combobox') {
69
+ return (
70
+ <QueryCombobox
71
+ className={newClassName}
72
+ disabled={disabled}
73
+ {...otherProps}
74
+ />
75
+ )
76
+ }
67
77
  if (extraTypes?.[type]) {
68
78
  const Component = extraTypes[type]
69
79
  return (
@@ -106,6 +116,7 @@ FormInput.propTypes = {
106
116
  'select',
107
117
  'choices',
108
118
  'textarea',
119
+ 'query-combobox',
109
120
  // to be removed
110
121
  'extendedTypeInput',
111
122
  ]),
@@ -11,14 +11,17 @@ import PropTypes from 'prop-types'
11
11
 
12
12
  // Local Definitions
13
13
 
14
- import { Combobox } from './common'
14
+ import { Combobox, MultipleCombobox } from './common'
15
15
 
16
16
  /**
17
17
  * This is the component description.
18
18
  */
19
19
  const QueryCombobox = ({
20
+ id,
21
+ style,
22
+ className,
20
23
  query,
21
- // multiple,
24
+ multiple,
22
25
  name,
23
26
  label,
24
27
  color,
@@ -29,6 +32,8 @@ const QueryCombobox = ({
29
32
  searchVariable,
30
33
  extraVariables,
31
34
  optionsKeyMap,
35
+ minLength,
36
+ transformSearch,
32
37
  // ...otherProps
33
38
  }) => {
34
39
  useLayoutEffect(() => {
@@ -85,6 +90,8 @@ const QueryCombobox = ({
85
90
  }
86
91
 
87
92
  const comboboxProps = {
93
+ id,
94
+ style,
88
95
  options,
89
96
  getOptions,
90
97
  debounceMs,
@@ -96,9 +103,13 @@ const QueryCombobox = ({
96
103
  error,
97
104
  value,
98
105
  color,
106
+ isFetching,
107
+ className,
108
+ minLength,
109
+ transformSearch,
99
110
  }
100
111
 
101
- const Input = Combobox
112
+ const Input = multiple ? MultipleCombobox : Combobox
102
113
 
103
114
  return <Input {...comboboxProps} />
104
115
  }
@@ -185,6 +196,16 @@ QueryCombobox.propTypes = {
185
196
  * The variable to be used to search the data
186
197
  */
187
198
  searchVariable:PropTypes.string,
199
+
200
+ /**
201
+ * The minimum length of the search input to start fetching the options
202
+ */
203
+ minLength:PropTypes.number,
204
+
205
+ /**
206
+ * The function to transform the search input
207
+ */
208
+ transformSearch:PropTypes.func,
188
209
  }
189
210
 
190
211
  QueryCombobox.defaultProps = {
@@ -192,9 +213,11 @@ QueryCombobox.defaultProps = {
192
213
  value:'id',
193
214
  label:'name',
194
215
  },
195
- multiple :false,
196
- color :'background2',
197
- searchVariable:'search',
216
+ multiple :false,
217
+ color :'background2',
218
+ searchVariable :'search',
219
+ transformSearch:(search) => search,
220
+ minLength :2,
198
221
  }
199
222
 
200
223
  export default QueryCombobox
@@ -11,7 +11,7 @@ import styleNames from '@pareto-engineering/bem'
11
11
 
12
12
  import { FormLabel, FormDescription } from 'ui/f'
13
13
 
14
- import { Popover } from 'ui/a'
14
+ import { Popover, LoadingCircle } from 'ui/a'
15
15
 
16
16
  // Local Definitions
17
17
 
@@ -37,6 +37,9 @@ const Combobox = ({
37
37
  description,
38
38
  value,
39
39
  color,
40
+ minLength,
41
+ isFetching,
42
+ transformSearch,
40
43
  // ...otherProps
41
44
  }) => {
42
45
  const {
@@ -54,7 +57,10 @@ const Combobox = ({
54
57
  initialSelectedItem:value,
55
58
  itemToString :(item) => (item ? item.label : ''),
56
59
  onInputValueChange :({ inputValue }) => {
57
- getOptions(inputValue)
60
+ const transformedInput = transformSearch(inputValue)
61
+ if (transformedInput.length > minLength) {
62
+ getOptions(transformedInput)
63
+ }
58
64
  },
59
65
  })
60
66
 
@@ -97,6 +103,9 @@ const Combobox = ({
97
103
 
98
104
  <div {...getComboboxProps()} className="input-wrapper">
99
105
  <input {...getInputProps()} className="input" />
106
+ {isFetching && (
107
+ <LoadingCircle className="x-main2" />
108
+ )}
100
109
  </div>
101
110
 
102
111
  <Popover
@@ -189,10 +198,25 @@ Combobox.propTypes = {
189
198
  * The base color of the combobox custom select input
190
199
  */
191
200
  color:PropTypes.string,
201
+
202
+ /**
203
+ * Whether the query getting the combobox options is inFlight
204
+ */
205
+ isFetching:PropTypes.bool.isRequired,
206
+
207
+ /**
208
+ * The minimum length of the search input to start fetching the options
209
+ */
210
+ minLength:PropTypes.number,
211
+
212
+ /**
213
+ * The function to transform the search input
214
+ */
215
+ transformSearch:PropTypes.func,
192
216
  }
193
217
 
194
218
  Combobox.defaultProps = {
195
- // someProp:false
219
+ // someProp: false
196
220
  }
197
221
 
198
222
  export default Combobox
@@ -0,0 +1,318 @@
1
+ /* @pareto-engineering/generator-front 1.0.12 */
2
+ import * as React from 'react'
3
+
4
+ import { useState, useEffect, useRef } from 'react'
5
+
6
+ import PropTypes from 'prop-types'
7
+
8
+ import styleNames from '@pareto-engineering/bem'
9
+
10
+ import { useCombobox, useMultipleSelection } from 'downshift'
11
+
12
+ import { Button } from 'ui/b'
13
+
14
+ import { Popover, LoadingCircle } from 'ui/a'
15
+
16
+ import { FormDescription, FormLabel } from 'ui/f'
17
+
18
+ // Local Definitions
19
+
20
+ import { Menu } from '../Menu'
21
+
22
+ const baseClassName = styleNames.base
23
+
24
+ const componentClassName = 'multiple-combobox'
25
+
26
+ /**
27
+ * @param {Array[Object]} first - first array to check if it has an item not in the second array.
28
+ * @param {Array[Object]} second - second array to check against the first array.
29
+ *
30
+ * @returns {Boolean} - true if the first array has an item not in the second array.
31
+ */
32
+ const testIfArraysAreUnique = (first, second) => first
33
+ .filter((objInFirstArray) => !second
34
+ .some((objInSecondArray) => objInFirstArray.value === objInSecondArray.value))
35
+ .length > 0
36
+
37
+ /**
38
+ * This is the component description.
39
+ */
40
+ const MultipleCombobox = ({
41
+ id,
42
+ className:userClassName,
43
+ style,
44
+ label,
45
+ name,
46
+ options:items,
47
+ getOptions,
48
+ setValue,
49
+ error,
50
+ description,
51
+ value,
52
+ color,
53
+ isFetching,
54
+ minLength,
55
+ transformSearch,
56
+ // ...otherProps
57
+ }) => {
58
+ const [searchInputValue, setSearchInputValue] = useState('')
59
+ const {
60
+ getSelectedItemProps,
61
+ getDropdownProps,
62
+ addSelectedItem,
63
+ removeSelectedItem,
64
+ setSelectedItems,
65
+ selectedItems,
66
+ } = useMultipleSelection({
67
+ initialSelectedItems:value || [],
68
+ })
69
+
70
+ /**
71
+ * @returns {Boolean} - Unique items from the options array so that the combobox
72
+ * shows only the options that are not yet selected.
73
+ */
74
+ const getFilteredItems = () => items
75
+ .filter((item) => selectedItems
76
+ .findIndex((e) => e.label === item.label) < 0)
77
+
78
+ const {
79
+ isOpen,
80
+ getLabelProps,
81
+ getMenuProps,
82
+ getInputProps,
83
+ getComboboxProps,
84
+ highlightedIndex,
85
+ getItemProps,
86
+ } = useCombobox({
87
+ searchInputValue,
88
+ defaultHighlightedIndex:0, // after selection, highlight the first item.
89
+ selectedItem :null,
90
+ items :getFilteredItems(),
91
+ circularNavigation :true,
92
+ stateReducer :(state, actionAndChanges) => {
93
+ const { changes, type } = actionAndChanges
94
+ switch (type) {
95
+ case useCombobox.stateChangeTypes.InputKeyDownEnter:
96
+ case useCombobox.stateChangeTypes.ItemClick:
97
+ return {
98
+ ...changes,
99
+ isOpen:true, // keep the menu open after selection.
100
+ }
101
+ default:
102
+ break
103
+ }
104
+ return changes
105
+ },
106
+ onStateChange:({ inputValue:newSearchInputValue, type, selectedItem }) => {
107
+ switch (type) {
108
+ case useCombobox.stateChangeTypes.InputChange: {
109
+ const transformedInput = transformSearch(newSearchInputValue)
110
+ if (transformedInput.length > minLength) {
111
+ getOptions(transformedInput)
112
+ }
113
+ setSearchInputValue(newSearchInputValue)
114
+ break
115
+ }
116
+ case useCombobox.stateChangeTypes.InputKeyDownEnter:
117
+ case useCombobox.stateChangeTypes.ItemClick:
118
+ case useCombobox.stateChangeTypes.InputBlur:
119
+ if (selectedItem) {
120
+ setSearchInputValue('')
121
+ addSelectedItem(selectedItem)
122
+ }
123
+ break
124
+ default:
125
+ break
126
+ }
127
+ },
128
+ })
129
+
130
+ useEffect(() => {
131
+ if (selectedItems?.length > 0) {
132
+ setValue(selectedItems)
133
+ }
134
+ }, [selectedItems])
135
+
136
+ useEffect(() => {
137
+ if (value?.length > 0 && (
138
+ testIfArraysAreUnique(value, selectedItems)
139
+ || testIfArraysAreUnique(selectedItems, value)
140
+ )) {
141
+ setSelectedItems(value)
142
+ }
143
+ }, [value])
144
+
145
+ const parentRef = useRef(null)
146
+
147
+ return (
148
+ <div
149
+ id={id}
150
+ className={[
151
+
152
+ baseClassName,
153
+
154
+ componentClassName,
155
+ userClassName,
156
+ `y-${color}`,
157
+ ]
158
+ .filter((e) => e)
159
+ .join(' ')}
160
+ style={style}
161
+ >
162
+
163
+ <FormLabel {...getLabelProps()} name={name}>
164
+ {label}
165
+ </FormLabel>
166
+
167
+ {selectedItems?.length > 0 && (
168
+ <div className="selected-items">
169
+ {selectedItems.map((selectedItem, index) => (
170
+ <div
171
+ key={selectedItem.label}
172
+ {...getSelectedItemProps({ selectedItem, index })}
173
+ className="item"
174
+ >
175
+ <Button
176
+ onClick={(e) => {
177
+ e.stopPropagation()
178
+ removeSelectedItem(selectedItem)
179
+ }}
180
+ isCompact
181
+ isSimple
182
+ color={color}
183
+ >
184
+ <span className="v25 mr-v">{selectedItem.label}</span>
185
+ <span className="f-icons close">Y</span>
186
+ </Button>
187
+ </div>
188
+ ))}
189
+ </div>
190
+ )}
191
+ <div {...getComboboxProps()} className="input-wrapper">
192
+ <input
193
+ {...getInputProps(
194
+ getDropdownProps({ preventKeyAction: isOpen }),
195
+ )}
196
+ className="input"
197
+ />
198
+ {isFetching && (
199
+ <LoadingCircle className="x-main2" />
200
+ )}
201
+ </div>
202
+
203
+ <Popover
204
+ isOpen={isOpen}
205
+ parentRef={parentRef}
206
+ >
207
+ <Menu
208
+ isOpen={isOpen}
209
+ getItemProps={getItemProps}
210
+ highlightedIndex={highlightedIndex}
211
+ items={getFilteredItems()}
212
+ {...getMenuProps()}
213
+ />
214
+ </Popover>
215
+
216
+ {(description || error) && (
217
+ <FormDescription isError={!!error}>
218
+ { error || description }
219
+ </FormDescription>
220
+ )}
221
+
222
+ </div>
223
+ )
224
+ }
225
+
226
+ MultipleCombobox.propTypes = {
227
+ /**
228
+ * The HTML id for this element
229
+ */
230
+ id:PropTypes.string,
231
+
232
+ /**
233
+ * The HTML class names for this element
234
+ */
235
+ className:PropTypes.string,
236
+
237
+ /**
238
+ * The React-written, css properties for this element.
239
+ */
240
+ style:PropTypes.objectOf(PropTypes.string),
241
+
242
+ /**
243
+ * The label of the custom select input
244
+ */
245
+ label:PropTypes.string,
246
+
247
+ /**
248
+ * The custom select input options from the backend
249
+ */
250
+ options:PropTypes.arrayOf(
251
+ PropTypes.shape({
252
+ value:PropTypes.string,
253
+ label:PropTypes.string,
254
+ }),
255
+ ),
256
+
257
+ /**
258
+ * The name of the custom select input
259
+ */
260
+ name:PropTypes.string,
261
+
262
+ /**
263
+ * The function to fetch the options from the backend
264
+ */
265
+ getOptions:PropTypes.func,
266
+
267
+ /**
268
+ * The function to set the value of the custom select input
269
+ */
270
+ setValue:PropTypes.func.isRequired,
271
+
272
+ /**
273
+ * The custom select input description
274
+ */
275
+ description:PropTypes.string,
276
+
277
+ /**
278
+ * The error object
279
+ */
280
+ error:PropTypes.objectOf(PropTypes.string),
281
+
282
+ /**
283
+ * The value of the custom select input
284
+ */
285
+ value:PropTypes.arrayOf(
286
+ PropTypes.shape({
287
+ value:PropTypes.string,
288
+ label:PropTypes.string,
289
+ }),
290
+ ),
291
+
292
+ /**
293
+ * The base color of the custom select input
294
+ */
295
+ color:PropTypes.string,
296
+
297
+ /**
298
+ * Whether the query getting the combobox options is inFlight
299
+ */
300
+ isFetching:PropTypes.bool.isRequired,
301
+
302
+ /**
303
+ * The minimum length of the search input to start fetching the options
304
+ */
305
+ minLength:PropTypes.number,
306
+
307
+ /**
308
+ * The function to transform the search input
309
+ */
310
+ transformSearch:PropTypes.func,
311
+
312
+ }
313
+
314
+ MultipleCombobox.defaultProps = {
315
+ // someProp: false
316
+ }
317
+
318
+ export default MultipleCombobox
@@ -0,0 +1,2 @@
1
+ /* @pareto-engineering/generator-front 1.0.12 */
2
+ export { default as MultipleCombobox } from './MultipleCombobox'
@@ -1,2 +1,3 @@
1
1
  export { Menu } from './Menu'
2
2
  export { Combobox } from './Combobox'
3
+ export { MultipleCombobox } from './MultipleCombobox'
@@ -5,11 +5,17 @@
5
5
  $default-input-padding: .75em .75em .55em;
6
6
  $default-padding: 1em;
7
7
  $default-margin: 1em;
8
+ $default-gap: 1em;
9
+ $default-loading-circle-displacement: 1em;
8
10
 
9
11
  .#{bem.$base}.combobox,
10
12
  .#{bem.$base}.multiple-combobox {
11
13
  position: relative;
12
14
 
15
+ .#{bem.$base}.label {
16
+ margin-bottom: $default-margin
17
+ }
18
+
13
19
  .#{bem.$base}.popover {
14
20
  width: 100%;
15
21
 
@@ -30,6 +36,14 @@ $default-margin: 1em;
30
36
  }
31
37
 
32
38
  >.input-wrapper {
39
+ position: relative;
40
+
41
+ >.#{bem.$base}.loading-circle {
42
+ position: absolute;
43
+ top: $default-loading-circle-displacement;
44
+ right: $default-loading-circle-displacement;
45
+ }
46
+
33
47
  >.input {
34
48
  background: var(--light-y);
35
49
  border: var(--theme-border-style) var(--dark-y);
@@ -60,12 +74,17 @@ $default-margin: 1em;
60
74
  .#{bem.$base}.multiple-combobox {
61
75
  >.selected-items {
62
76
  display: flex;
77
+ gap: $default-gap / 2;
78
+ flex-wrap: wrap;
79
+ margin-bottom: $default-margin / 2;
63
80
 
64
- /* stylelint-disable selector-max-universal -- Allow */
65
- >*:not(:first-child) {
66
- margin-left: $default-margin;
67
- }
81
+ >.item {
82
+ background-color: var(--main2);
83
+ padding: $default-padding / 4;
68
84
 
69
- /* stylelint-enable selector-max-universal */
85
+ .close {
86
+ font-size: calc(var(--s-3) * 1em);
87
+ }
88
+ }
70
89
  }
71
90
  }