@budibase/frontend-core 2.33.14 → 3.0.1

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 (53) hide show
  1. package/package.json +5 -5
  2. package/src/api/automations.js +2 -7
  3. package/src/api/index.js +2 -0
  4. package/src/api/rowActions.js +90 -0
  5. package/src/api/tables.js +1 -1
  6. package/src/api/viewsV2.js +1 -0
  7. package/src/components/CoreFilterBuilder.svelte +532 -0
  8. package/src/components/FilterField.svelte +319 -0
  9. package/src/components/FilterUsers.svelte +2 -1
  10. package/src/components/grid/cells/AICell.svelte +99 -0
  11. package/src/components/grid/cells/DataCell.svelte +1 -1
  12. package/src/components/grid/cells/GridCell.svelte +6 -1
  13. package/src/components/grid/cells/HeaderCell.svelte +20 -11
  14. package/src/components/grid/cells/NumberCell.svelte +23 -1
  15. package/src/components/grid/cells/RelationshipCell.svelte +1 -3
  16. package/src/components/grid/cells/RoleCell.svelte +45 -0
  17. package/src/components/grid/cells/TextCell.svelte +3 -1
  18. package/src/components/grid/layout/ButtonColumn.svelte +67 -36
  19. package/src/components/grid/layout/Grid.svelte +22 -27
  20. package/src/components/grid/lib/constants.js +2 -0
  21. package/src/components/grid/lib/renderers.js +9 -0
  22. package/src/components/grid/lib/utils.js +13 -31
  23. package/src/components/grid/lib/websocket.js +9 -0
  24. package/src/components/grid/overlays/GridPopover.svelte +4 -2
  25. package/src/components/grid/overlays/MenuOverlay.svelte +19 -0
  26. package/src/components/grid/stores/columns.js +2 -1
  27. package/src/components/grid/stores/config.js +12 -3
  28. package/src/components/grid/stores/datasource.js +27 -10
  29. package/src/components/grid/stores/datasources/nonPlus.js +3 -2
  30. package/src/components/grid/stores/datasources/table.js +3 -2
  31. package/src/components/grid/stores/datasources/viewV2.js +58 -27
  32. package/src/components/grid/stores/filter.js +25 -7
  33. package/src/components/grid/stores/rows.js +11 -6
  34. package/src/components/grid/stores/sort.js +7 -3
  35. package/src/components/index.js +1 -1
  36. package/src/constants.js +17 -30
  37. package/src/fetch/DataFetch.js +17 -9
  38. package/src/fetch/TableFetch.js +2 -1
  39. package/src/fetch/UserFetch.js +7 -8
  40. package/src/fetch/ViewV2Fetch.js +18 -13
  41. package/src/utils/index.js +1 -1
  42. package/src/utils/relatedColumns.js +6 -10
  43. package/src/utils/roles.js +0 -13
  44. package/src/utils/schema.js +24 -0
  45. package/src/utils/table.js +6 -6
  46. package/src/utils/utils.js +1 -1
  47. package/src/components/FilterBuilder.svelte +0 -379
  48. package/src/components/grid/controls/ColumnsSettingButton.svelte +0 -41
  49. package/src/components/grid/controls/ColumnsSettingContent.svelte +0 -270
  50. package/src/components/grid/controls/SizeButton.svelte +0 -136
  51. package/src/components/grid/controls/SortButton.svelte +0 -96
  52. package/src/components/grid/controls/ToggleActionButtonGroup.svelte +0 -41
  53. package/src/utils/theme.js +0 -12
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "2.33.14",
3
+ "version": "3.0.1",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
7
7
  "svelte": "src/index.js",
8
8
  "dependencies": {
9
- "@budibase/bbui": "2.33.14",
10
- "@budibase/shared-core": "2.33.14",
11
- "@budibase/types": "2.33.14",
9
+ "@budibase/bbui": "3.0.1",
10
+ "@budibase/shared-core": "3.0.1",
11
+ "@budibase/types": "3.0.1",
12
12
  "dayjs": "^1.10.8",
13
13
  "lodash": "4.17.21",
14
14
  "shortid": "2.2.15",
15
15
  "socket.io-client": "^4.7.5"
16
16
  },
17
- "gitHead": "0d3a15ef70e538e0f7bb85b651ca676e1c33b621"
17
+ "gitHead": "6528c1be8865a2cfee8bb80453fa314764d965f3"
18
18
  }
@@ -26,14 +26,9 @@ export const buildAutomationEndpoints = API => ({
26
26
  /**
27
27
  * Gets a list of all automations.
28
28
  */
29
- getAutomations: async ({ enrich }) => {
30
- const params = new URLSearchParams()
31
- if (enrich) {
32
- params.set("enrich", true)
33
- }
34
-
29
+ getAutomations: async () => {
35
30
  return await API.get({
36
- url: `/api/automations?${params.toString()}`,
31
+ url: "/api/automations",
37
32
  })
38
33
  },
39
34
 
package/src/api/index.js CHANGED
@@ -35,6 +35,7 @@ import { buildEventEndpoints } from "./events"
35
35
  import { buildAuditLogsEndpoints } from "./auditLogs"
36
36
  import { buildLogsEndpoints } from "./logs"
37
37
  import { buildMigrationEndpoints } from "./migrations"
38
+ import { buildRowActionEndpoints } from "./rowActions"
38
39
 
39
40
  /**
40
41
  * Random identifier to uniquely identify a session in a tab. This is
@@ -303,5 +304,6 @@ export const createAPIClient = config => {
303
304
  ...buildLogsEndpoints(API),
304
305
  ...buildMigrationEndpoints(API),
305
306
  viewV2: buildViewV2Endpoints(API),
307
+ rowActions: buildRowActionEndpoints(API),
306
308
  }
307
309
  }
@@ -0,0 +1,90 @@
1
+ export const buildRowActionEndpoints = API => ({
2
+ /**
3
+ * Gets the available row actions for a table.
4
+ * @param tableId the ID of the table
5
+ */
6
+ fetch: async tableId => {
7
+ const res = await API.get({
8
+ url: `/api/tables/${tableId}/actions`,
9
+ })
10
+ return res?.actions || {}
11
+ },
12
+
13
+ /**
14
+ * Creates a row action.
15
+ * @param name the name of the row action
16
+ * @param tableId the ID of the table
17
+ */
18
+ create: async ({ name, tableId }) => {
19
+ return await API.post({
20
+ url: `/api/tables/${tableId}/actions`,
21
+ body: {
22
+ name,
23
+ },
24
+ })
25
+ },
26
+
27
+ /**
28
+ * Updates a row action.
29
+ * @param name the new name of the row action
30
+ * @param tableId the ID of the table
31
+ * @param rowActionId the ID of the row action to update
32
+ */
33
+ update: async ({ tableId, rowActionId, name }) => {
34
+ return await API.put({
35
+ url: `/api/tables/${tableId}/actions/${rowActionId}`,
36
+ body: {
37
+ name,
38
+ },
39
+ })
40
+ },
41
+
42
+ /**
43
+ * Deletes a row action.
44
+ * @param tableId the ID of the table
45
+ * @param rowActionId the ID of the row action to delete
46
+ */
47
+ delete: async ({ tableId, rowActionId }) => {
48
+ return await API.delete({
49
+ url: `/api/tables/${tableId}/actions/${rowActionId}`,
50
+ })
51
+ },
52
+
53
+ /**
54
+ * Enables a row action for a certain view
55
+ * @param tableId the ID of the parent table
56
+ * @param rowActionId the ID of the row action
57
+ * @param viewId the ID of the view
58
+ */
59
+ enableView: async ({ tableId, rowActionId, viewId }) => {
60
+ return await API.post({
61
+ url: `/api/tables/${tableId}/actions/${rowActionId}/permissions/${viewId}`,
62
+ })
63
+ },
64
+
65
+ /**
66
+ * Disables a row action for a certain view
67
+ * @param tableId the ID of the parent table
68
+ * @param rowActionId the ID of the row action
69
+ * @param viewId the ID of the view
70
+ */
71
+ disableView: async ({ tableId, rowActionId, viewId }) => {
72
+ return await API.delete({
73
+ url: `/api/tables/${tableId}/actions/${rowActionId}/permissions/${viewId}`,
74
+ })
75
+ },
76
+
77
+ /**
78
+ * Triggers a row action.
79
+ * @param tableId the ID of the table
80
+ * @param rowActionId the ID of the row action to trigger
81
+ */
82
+ trigger: async ({ sourceId, rowActionId, rowId }) => {
83
+ return await API.post({
84
+ url: `/api/tables/${sourceId}/actions/${rowActionId}/trigger`,
85
+ body: {
86
+ rowId,
87
+ },
88
+ })
89
+ },
90
+ })
package/src/api/tables.js CHANGED
@@ -48,7 +48,7 @@ export const buildTableEndpoints = API => ({
48
48
  return await API.post({
49
49
  url: `/api/${tableId}/search`,
50
50
  body: {
51
- query,
51
+ ...(query ? { query } : {}),
52
52
  bookmark,
53
53
  limit,
54
54
  sort,
@@ -6,6 +6,7 @@ export const buildViewV2Endpoints = API => ({
6
6
  fetchDefinition: async viewId => {
7
7
  return await API.get({
8
8
  url: `/api/v2/views/${encodeURIComponent(viewId)}`,
9
+ cache: true,
9
10
  })
10
11
  },
11
12
  /**
@@ -0,0 +1,532 @@
1
+ <script>
2
+ import {
3
+ Body,
4
+ Button,
5
+ Icon,
6
+ Layout,
7
+ Select,
8
+ Helpers,
9
+ ActionButton,
10
+ } from "@budibase/bbui"
11
+ import {
12
+ FieldType,
13
+ UILogicalOperator,
14
+ EmptyFilterOption,
15
+ } from "@budibase/types"
16
+ import { QueryUtils, Constants } from "@budibase/frontend-core"
17
+ import { getContext, createEventDispatcher } from "svelte"
18
+ import FilterField from "./FilterField.svelte"
19
+ import { utils } from "@budibase/shared-core"
20
+
21
+ const dispatch = createEventDispatcher()
22
+ const {
23
+ OperatorOptions,
24
+ DEFAULT_BB_DATASOURCE_ID,
25
+ FilterOperator,
26
+ OnEmptyFilter,
27
+ FilterValueType,
28
+ } = Constants
29
+
30
+ export let schemaFields
31
+ export let filters
32
+ export let tables = []
33
+ export let datasource
34
+ export let behaviourFilters = false
35
+ export let allowBindings = false
36
+
37
+ // Review
38
+ export let bindings
39
+ export let panel
40
+ export let toReadable
41
+ export let toRuntime
42
+
43
+ $: editableFilters = migrateFilters(filters)
44
+ $: {
45
+ if (
46
+ tables.find(
47
+ table =>
48
+ table._id === datasource?.tableId &&
49
+ table.sourceId === DEFAULT_BB_DATASOURCE_ID
50
+ ) &&
51
+ !schemaFields.some(field => field.name === "_id")
52
+ ) {
53
+ schemaFields = [...schemaFields, { name: "_id", type: "string" }]
54
+ }
55
+ }
56
+
57
+ // We still may need to migrate this even though the backend does it automatically now
58
+ // for query definitions. This is because we might be editing saved filter definitions
59
+ // from old screens, which will still be of type LegacyFilter[].
60
+ const migrateFilters = filters => {
61
+ if (Array.isArray(filters)) {
62
+ return utils.processSearchFilters(filters)
63
+ }
64
+ return Helpers.cloneDeep(filters)
65
+ }
66
+
67
+ const filterOperatorOptions = Object.values(FilterOperator).map(entry => {
68
+ return { value: entry, label: Helpers.capitalise(entry) }
69
+ })
70
+
71
+ const onEmptyLabelling = {
72
+ [OnEmptyFilter.RETURN_ALL]: "All rows",
73
+ [OnEmptyFilter.RETURN_NONE]: "No rows",
74
+ }
75
+
76
+ const onEmptyOptions = Object.values(OnEmptyFilter).map(entry => {
77
+ return { value: entry, label: onEmptyLabelling[entry] }
78
+ })
79
+
80
+ const context = getContext("context")
81
+
82
+ $: fieldOptions = (schemaFields || [])
83
+ .filter(field => !field.calculationType)
84
+ .map(field => ({
85
+ label: field.displayName || field.name,
86
+ value: field.name,
87
+ }))
88
+
89
+ const onFieldChange = filter => {
90
+ const previousType = filter.type
91
+ sanitizeTypes(filter)
92
+ sanitizeOperator(filter)
93
+ sanitizeValue(filter, previousType)
94
+ }
95
+
96
+ const onOperatorChange = filter => {
97
+ sanitizeOperator(filter)
98
+ sanitizeValue(filter, filter.type)
99
+ }
100
+
101
+ const getSchema = filter => {
102
+ return schemaFields.find(field => field.name === filter.field)
103
+ }
104
+
105
+ const getValidOperatorsForType = filter => {
106
+ if (!filter?.field && !filter?.name) {
107
+ return []
108
+ }
109
+
110
+ return QueryUtils.getValidOperatorsForType(
111
+ filter,
112
+ filter.field || filter.name,
113
+ datasource
114
+ )
115
+ }
116
+
117
+ const sanitizeTypes = filter => {
118
+ // Update type based on field
119
+ const fieldSchema = schemaFields.find(x => x.name === filter.field)
120
+ filter.type = fieldSchema?.type
121
+ filter.subtype = fieldSchema?.subtype
122
+ filter.formulaType = fieldSchema?.formulaType
123
+ filter.constraints = fieldSchema?.constraints
124
+
125
+ // Update external type based on field
126
+ filter.externalType = getSchema(filter)?.externalType
127
+ }
128
+
129
+ const sanitizeOperator = filter => {
130
+ // Ensure a valid operator is selected
131
+ const operators = getValidOperatorsForType(filter).map(x => x.value)
132
+ if (!operators.includes(filter.operator)) {
133
+ filter.operator = operators[0] ?? OperatorOptions.Equals.value
134
+ }
135
+
136
+ // Update the noValue flag if the operator does not take a value
137
+ const noValueOptions = [
138
+ OperatorOptions.Empty.value,
139
+ OperatorOptions.NotEmpty.value,
140
+ ]
141
+ filter.noValue = noValueOptions.includes(filter.operator)
142
+ }
143
+
144
+ const sanitizeValue = (filter, previousType) => {
145
+ // Check if the operator allows a value at all
146
+ if (filter.noValue) {
147
+ filter.value = null
148
+ return
149
+ }
150
+ // Ensure array values are properly set and cleared
151
+ if (Array.isArray(filter.value)) {
152
+ if (filter.valueType !== "Value" || filter.type !== FieldType.ARRAY) {
153
+ filter.value = null
154
+ }
155
+ } else if (
156
+ filter.type === FieldType.ARRAY &&
157
+ filter.valueType === "Value"
158
+ ) {
159
+ filter.value = []
160
+ } else if (
161
+ previousType !== filter.type &&
162
+ (previousType === FieldType.BB_REFERENCE ||
163
+ filter.type === FieldType.BB_REFERENCE)
164
+ ) {
165
+ filter.value = filter.type === FieldType.ARRAY ? [] : null
166
+ }
167
+ }
168
+
169
+ const getGroupPrefix = groupIdx => {
170
+ if (groupIdx == 0) {
171
+ return "When"
172
+ }
173
+ const operatorMapping = {
174
+ [FilterOperator.ANY]: "or",
175
+ [FilterOperator.ALL]: "and",
176
+ }
177
+ return operatorMapping[editableFilters.logicalOperator]
178
+ }
179
+
180
+ const onFilterFieldUpdate = (filter, groupIdx, filterIdx) => {
181
+ const updated = Helpers.cloneDeep(filter)
182
+
183
+ handleFilterChange({
184
+ groupIdx,
185
+ filterIdx,
186
+ filter: { ...updated },
187
+ })
188
+ }
189
+
190
+ const handleFilterChange = req => {
191
+ const {
192
+ groupIdx,
193
+ filterIdx,
194
+ filter,
195
+ group,
196
+ addFilter,
197
+ addGroup,
198
+ deleteGroup,
199
+ deleteFilter,
200
+ logicalOperator,
201
+ onEmptyFilter,
202
+ } = req
203
+
204
+ let editable = Helpers.cloneDeep(editableFilters)
205
+ let targetGroup = editable?.groups?.[groupIdx]
206
+ let targetFilter = targetGroup?.filters?.[filterIdx]
207
+
208
+ if (targetFilter) {
209
+ if (deleteFilter) {
210
+ targetGroup.filters.splice(filterIdx, 1)
211
+
212
+ // Clear the group entirely if no valid filters remain
213
+ if (targetGroup.filters.length === 0) {
214
+ editable.groups.splice(groupIdx, 1)
215
+ }
216
+ } else if (filter) {
217
+ targetGroup.filters[filterIdx] = filter
218
+ }
219
+ } else if (targetGroup) {
220
+ if (deleteGroup) {
221
+ editable.groups.splice(groupIdx, 1)
222
+ } else if (addFilter) {
223
+ targetGroup.filters.push({
224
+ valueType: FilterValueType.VALUE,
225
+ })
226
+ } else if (group) {
227
+ editable.groups[groupIdx] = {
228
+ ...targetGroup,
229
+ ...group,
230
+ }
231
+ }
232
+ } else if (addGroup) {
233
+ if (!editable?.groups?.length) {
234
+ editable = {
235
+ logicalOperator: UILogicalOperator.ALL,
236
+ onEmptyFilter: EmptyFilterOption.RETURN_NONE,
237
+ groups: [],
238
+ }
239
+ }
240
+ editable.groups.push({
241
+ logicalOperator: Constants.FilterOperator.ANY,
242
+ filters: [
243
+ {
244
+ valueType: FilterValueType.VALUE,
245
+ },
246
+ ],
247
+ })
248
+ } else if (logicalOperator) {
249
+ editable = {
250
+ ...editable,
251
+ logicalOperator,
252
+ }
253
+ } else if (onEmptyFilter) {
254
+ editable = {
255
+ ...editable,
256
+ onEmptyFilter,
257
+ }
258
+ }
259
+
260
+ // Set the request to null if the groups are emptied
261
+ editable = editable.groups.length ? editable : null
262
+
263
+ dispatch("change", editable)
264
+ }
265
+ </script>
266
+
267
+ <div class="container" class:mobile={$context?.device?.mobile}>
268
+ <Layout noPadding>
269
+ {#if fieldOptions?.length}
270
+ <slot name="filtering-hero-content" />
271
+
272
+ {#if editableFilters?.groups?.length}
273
+ <div class="global-filter-header">
274
+ <span>Show data which matches</span>
275
+ <span class="operator-picker">
276
+ <Select
277
+ value={editableFilters?.logicalOperator}
278
+ options={filterOperatorOptions}
279
+ getOptionLabel={opt => opt.label}
280
+ getOptionValue={opt => opt.value}
281
+ on:change={e => {
282
+ handleFilterChange({
283
+ logicalOperator: e.detail,
284
+ })
285
+ }}
286
+ placeholder={false}
287
+ />
288
+ </span>
289
+ <span>of the following filter groups:</span>
290
+ </div>
291
+ {/if}
292
+ {#if editableFilters?.groups?.length}
293
+ <div class="filter-groups">
294
+ {#each editableFilters?.groups as group, groupIdx}
295
+ <div class="group">
296
+ <div class="group-header">
297
+ <div class="group-options">
298
+ <span>
299
+ {getGroupPrefix(groupIdx, editableFilters.logicalOperator)}
300
+ </span>
301
+ <span class="operator-picker">
302
+ <Select
303
+ value={group?.logicalOperator}
304
+ options={filterOperatorOptions}
305
+ getOptionLabel={opt => opt.label}
306
+ getOptionValue={opt => opt.value}
307
+ on:change={e => {
308
+ handleFilterChange({
309
+ groupIdx,
310
+ group: {
311
+ logicalOperator: e.detail,
312
+ },
313
+ })
314
+ }}
315
+ placeholder={false}
316
+ />
317
+ </span>
318
+ <span>of the following filters are matched:</span>
319
+ </div>
320
+ <div class="group-actions">
321
+ <Icon
322
+ name="Add"
323
+ hoverable
324
+ hoverColor="var(--ink)"
325
+ on:click={() => {
326
+ handleFilterChange({
327
+ groupIdx,
328
+ addFilter: true,
329
+ })
330
+ }}
331
+ />
332
+ <Icon
333
+ name="Delete"
334
+ hoverable
335
+ hoverColor="var(--ink)"
336
+ on:click={() => {
337
+ handleFilterChange({
338
+ groupIdx,
339
+ deleteGroup: true,
340
+ })
341
+ }}
342
+ />
343
+ </div>
344
+ </div>
345
+
346
+ <div class="filters">
347
+ {#each group.filters as filter, filterIdx}
348
+ <div class="filter">
349
+ <Select
350
+ value={filter.field}
351
+ options={fieldOptions}
352
+ on:change={e => {
353
+ const updated = { ...filter, field: e.detail }
354
+ onFieldChange(updated)
355
+ onFilterFieldUpdate(updated, groupIdx, filterIdx)
356
+ }}
357
+ placeholder="Column"
358
+ />
359
+
360
+ <Select
361
+ value={filter.operator}
362
+ disabled={!filter.field}
363
+ options={getValidOperatorsForType(filter)}
364
+ on:change={e => {
365
+ const updated = { ...filter, operator: e.detail }
366
+ onOperatorChange(updated)
367
+ onFilterFieldUpdate(updated, groupIdx, filterIdx)
368
+ }}
369
+ placeholder={false}
370
+ />
371
+
372
+ <FilterField
373
+ placeholder="Value"
374
+ {allowBindings}
375
+ {filter}
376
+ {schemaFields}
377
+ {bindings}
378
+ {panel}
379
+ {toReadable}
380
+ {toRuntime}
381
+ on:change={e => {
382
+ onFilterFieldUpdate(
383
+ { ...filter, ...e.detail },
384
+ groupIdx,
385
+ filterIdx
386
+ )
387
+ }}
388
+ />
389
+
390
+ <ActionButton
391
+ size="M"
392
+ icon="Delete"
393
+ on:click={() => {
394
+ handleFilterChange({
395
+ groupIdx,
396
+ filterIdx,
397
+ deleteFilter: true,
398
+ })
399
+ }}
400
+ />
401
+ </div>
402
+ {/each}
403
+ </div>
404
+ </div>
405
+ {/each}
406
+ </div>
407
+ {/if}
408
+
409
+ <div class="filters-footer">
410
+ <Layout noPadding>
411
+ {#if behaviourFilters && editableFilters?.groups?.length}
412
+ <div class="empty-filter">
413
+ <span>Return</span>
414
+ <span class="empty-filter-picker">
415
+ <Select
416
+ value={editableFilters?.onEmptyFilter}
417
+ options={onEmptyOptions}
418
+ getOptionLabel={opt => opt.label}
419
+ getOptionValue={opt => opt.value}
420
+ on:change={e => {
421
+ handleFilterChange({
422
+ onEmptyFilter: e.detail,
423
+ })
424
+ }}
425
+ placeholder={false}
426
+ />
427
+ </span>
428
+ <span>when all filters are empty</span>
429
+ </div>
430
+ {/if}
431
+ <div class="add-group">
432
+ <Button
433
+ icon="AddCircle"
434
+ size="M"
435
+ secondary
436
+ on:click={() => {
437
+ handleFilterChange({
438
+ addGroup: true,
439
+ })
440
+ }}
441
+ >
442
+ Add filter group
443
+ </Button>
444
+ <a
445
+ href="https://docs.budibase.com/docs/searchfilter-data"
446
+ target="_blank"
447
+ >
448
+ <Icon
449
+ name="HelpOutline"
450
+ color="var(--spectrum-global-color-gray-600)"
451
+ />
452
+ </a>
453
+ </div>
454
+ </Layout>
455
+ </div>
456
+ {:else}
457
+ <Body size="S">None of the table column can be used for filtering.</Body>
458
+ {/if}
459
+ </Layout>
460
+ </div>
461
+
462
+ <style>
463
+ .group-actions {
464
+ display: flex;
465
+ gap: var(--spacing-m);
466
+ }
467
+
468
+ .global-filter-header,
469
+ .empty-filter,
470
+ .group-options {
471
+ display: flex;
472
+ gap: var(--spacing-m);
473
+ align-items: center;
474
+ }
475
+
476
+ .group-header {
477
+ display: flex;
478
+ justify-content: space-between;
479
+ align-items: center;
480
+ }
481
+
482
+ .operator-picker {
483
+ width: 72px;
484
+ }
485
+
486
+ .empty-filter-picker {
487
+ width: 92px;
488
+ }
489
+
490
+ .filter-groups {
491
+ display: flex;
492
+ flex-direction: column;
493
+ gap: var(--spacing-xl);
494
+ }
495
+
496
+ .group {
497
+ display: flex;
498
+ flex-direction: column;
499
+ gap: var(--spacing-l);
500
+ border: 1px solid var(--spectrum-global-color-gray-400);
501
+ border-radius: 4px;
502
+ padding: var(--spacing-xl);
503
+ }
504
+
505
+ .filters {
506
+ display: flex;
507
+ flex-direction: column;
508
+ gap: var(--spacing-l);
509
+ }
510
+
511
+ .filter {
512
+ display: grid;
513
+ gap: var(--spacing-l);
514
+ grid-template-columns: minmax(150px, 1fr) 170px minmax(200px, 1fr) 40px 40px;
515
+ }
516
+
517
+ .filters-footer {
518
+ display: flex;
519
+ gap: var(--spacing-xl);
520
+ flex-direction: column;
521
+ }
522
+
523
+ .add-group {
524
+ display: flex;
525
+ gap: var(--spacing-m);
526
+ align-items: center;
527
+ }
528
+
529
+ .container {
530
+ width: 100%;
531
+ }
532
+ </style>