@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.
- package/package.json +5 -5
- package/src/api/automations.js +2 -7
- package/src/api/index.js +2 -0
- package/src/api/rowActions.js +90 -0
- package/src/api/tables.js +1 -1
- package/src/api/viewsV2.js +1 -0
- package/src/components/CoreFilterBuilder.svelte +532 -0
- package/src/components/FilterField.svelte +319 -0
- package/src/components/FilterUsers.svelte +2 -1
- package/src/components/grid/cells/AICell.svelte +99 -0
- package/src/components/grid/cells/DataCell.svelte +1 -1
- package/src/components/grid/cells/GridCell.svelte +6 -1
- package/src/components/grid/cells/HeaderCell.svelte +20 -11
- package/src/components/grid/cells/NumberCell.svelte +23 -1
- package/src/components/grid/cells/RelationshipCell.svelte +1 -3
- package/src/components/grid/cells/RoleCell.svelte +45 -0
- package/src/components/grid/cells/TextCell.svelte +3 -1
- package/src/components/grid/layout/ButtonColumn.svelte +67 -36
- package/src/components/grid/layout/Grid.svelte +22 -27
- package/src/components/grid/lib/constants.js +2 -0
- package/src/components/grid/lib/renderers.js +9 -0
- package/src/components/grid/lib/utils.js +13 -31
- package/src/components/grid/lib/websocket.js +9 -0
- package/src/components/grid/overlays/GridPopover.svelte +4 -2
- package/src/components/grid/overlays/MenuOverlay.svelte +19 -0
- package/src/components/grid/stores/columns.js +2 -1
- package/src/components/grid/stores/config.js +12 -3
- package/src/components/grid/stores/datasource.js +27 -10
- package/src/components/grid/stores/datasources/nonPlus.js +3 -2
- package/src/components/grid/stores/datasources/table.js +3 -2
- package/src/components/grid/stores/datasources/viewV2.js +58 -27
- package/src/components/grid/stores/filter.js +25 -7
- package/src/components/grid/stores/rows.js +11 -6
- package/src/components/grid/stores/sort.js +7 -3
- package/src/components/index.js +1 -1
- package/src/constants.js +17 -30
- package/src/fetch/DataFetch.js +17 -9
- package/src/fetch/TableFetch.js +2 -1
- package/src/fetch/UserFetch.js +7 -8
- package/src/fetch/ViewV2Fetch.js +18 -13
- package/src/utils/index.js +1 -1
- package/src/utils/relatedColumns.js +6 -10
- package/src/utils/roles.js +0 -13
- package/src/utils/schema.js +24 -0
- package/src/utils/table.js +6 -6
- package/src/utils/utils.js +1 -1
- package/src/components/FilterBuilder.svelte +0 -379
- package/src/components/grid/controls/ColumnsSettingButton.svelte +0 -41
- package/src/components/grid/controls/ColumnsSettingContent.svelte +0 -270
- package/src/components/grid/controls/SizeButton.svelte +0 -136
- package/src/components/grid/controls/SortButton.svelte +0 -96
- package/src/components/grid/controls/ToggleActionButtonGroup.svelte +0 -41
- 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": "
|
|
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": "
|
|
10
|
-
"@budibase/shared-core": "
|
|
11
|
-
"@budibase/types": "
|
|
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": "
|
|
17
|
+
"gitHead": "6528c1be8865a2cfee8bb80453fa314764d965f3"
|
|
18
18
|
}
|
package/src/api/automations.js
CHANGED
|
@@ -26,14 +26,9 @@ export const buildAutomationEndpoints = API => ({
|
|
|
26
26
|
/**
|
|
27
27
|
* Gets a list of all automations.
|
|
28
28
|
*/
|
|
29
|
-
getAutomations: async (
|
|
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:
|
|
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
package/src/api/viewsV2.js
CHANGED
|
@@ -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>
|