@budibase/frontend-core 2.23.6 → 2.23.9
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 +6 -5
- package/src/api/attachments.js +0 -28
- package/src/components/FilterBuilder.svelte +363 -0
- package/src/components/FilterUsers.svelte +34 -0
- package/src/components/grid/cells/AttachmentCell.svelte +0 -9
- package/src/components/index.js +1 -0
- package/src/fetch/DataFetch.js +1 -2
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/frontend-core",
|
|
3
|
-
"version": "2.23.
|
|
3
|
+
"version": "2.23.9",
|
|
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.23.
|
|
10
|
-
"@budibase/shared-core": "2.23.
|
|
11
|
-
"@budibase/types": "2.23.
|
|
9
|
+
"@budibase/bbui": "2.23.9",
|
|
10
|
+
"@budibase/shared-core": "2.23.9",
|
|
11
|
+
"@budibase/types": "2.23.9",
|
|
12
12
|
"dayjs": "^1.10.8",
|
|
13
13
|
"lodash": "4.17.21",
|
|
14
|
+
"shortid": "2.2.15",
|
|
14
15
|
"socket.io-client": "^4.6.1"
|
|
15
16
|
},
|
|
16
|
-
"gitHead": "
|
|
17
|
+
"gitHead": "90609c97ad49b770b3b5b8092bb724041339e28e"
|
|
17
18
|
}
|
package/src/api/attachments.js
CHANGED
|
@@ -61,34 +61,6 @@ export const buildAttachmentEndpoints = API => {
|
|
|
61
61
|
})
|
|
62
62
|
return { publicUrl }
|
|
63
63
|
},
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Deletes attachments from the bucket.
|
|
67
|
-
* @param keys the attachments to delete
|
|
68
|
-
* @param tableId the associated table ID
|
|
69
|
-
*/
|
|
70
|
-
deleteAttachments: async ({ keys, tableId }) => {
|
|
71
|
-
return await API.post({
|
|
72
|
-
url: `/api/attachments/${tableId}/delete`,
|
|
73
|
-
body: {
|
|
74
|
-
keys,
|
|
75
|
-
},
|
|
76
|
-
})
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Deletes attachments from the builder bucket.
|
|
81
|
-
* @param keys the attachments to delete
|
|
82
|
-
*/
|
|
83
|
-
deleteBuilderAttachments: async keys => {
|
|
84
|
-
return await API.post({
|
|
85
|
-
url: `/api/attachments/delete`,
|
|
86
|
-
body: {
|
|
87
|
-
keys,
|
|
88
|
-
},
|
|
89
|
-
})
|
|
90
|
-
},
|
|
91
|
-
|
|
92
64
|
/**
|
|
93
65
|
* Download an attachment from a row given its column name.
|
|
94
66
|
* @param datasourceId the ID of the datasource to download from
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import {
|
|
3
|
+
Body,
|
|
4
|
+
Button,
|
|
5
|
+
Combobox,
|
|
6
|
+
DatePicker,
|
|
7
|
+
Icon,
|
|
8
|
+
Input,
|
|
9
|
+
Layout,
|
|
10
|
+
Select,
|
|
11
|
+
Label,
|
|
12
|
+
Multiselect,
|
|
13
|
+
} from "@budibase/bbui"
|
|
14
|
+
import { FieldType, SearchFilterOperator } from "@budibase/types"
|
|
15
|
+
import { generate } from "shortid"
|
|
16
|
+
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
|
17
|
+
import { getContext } from "svelte"
|
|
18
|
+
import FilterUsers from "./FilterUsers.svelte"
|
|
19
|
+
|
|
20
|
+
const { OperatorOptions } = Constants
|
|
21
|
+
|
|
22
|
+
export let schemaFields
|
|
23
|
+
export let filters = []
|
|
24
|
+
export let datasource
|
|
25
|
+
export let behaviourFilters = false
|
|
26
|
+
export let allowBindings = false
|
|
27
|
+
export let filtersLabel = "Filters"
|
|
28
|
+
|
|
29
|
+
$: matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
|
30
|
+
$: onEmptyFilter =
|
|
31
|
+
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
|
|
32
|
+
|
|
33
|
+
$: fieldFilters = filters.filter(
|
|
34
|
+
filter => filter.operator !== "allOr" && !filter.onEmptyFilter
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const behaviourOptions = [
|
|
38
|
+
{ value: "and", label: "Match all filters" },
|
|
39
|
+
{ value: "or", label: "Match any filter" },
|
|
40
|
+
]
|
|
41
|
+
const onEmptyOptions = [
|
|
42
|
+
{ value: "all", label: "Return all table rows" },
|
|
43
|
+
{ value: "none", label: "Return no rows" },
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const context = getContext("context")
|
|
47
|
+
|
|
48
|
+
$: fieldOptions = (schemaFields ?? [])
|
|
49
|
+
.filter(field => getValidOperatorsForType(field).length)
|
|
50
|
+
.map(field => ({
|
|
51
|
+
label: field.displayName || field.name,
|
|
52
|
+
value: field.name,
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
const addFilter = () => {
|
|
56
|
+
filters = [
|
|
57
|
+
...(filters || []),
|
|
58
|
+
{
|
|
59
|
+
id: generate(),
|
|
60
|
+
field: null,
|
|
61
|
+
operator: OperatorOptions.Equals.value,
|
|
62
|
+
value: null,
|
|
63
|
+
valueType: "Value",
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const removeFilter = id => {
|
|
69
|
+
filters = filters.filter(field => field.id !== id)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const duplicateFilter = id => {
|
|
73
|
+
const existingFilter = filters.find(filter => filter.id === id)
|
|
74
|
+
const duplicate = { ...existingFilter, id: generate() }
|
|
75
|
+
filters = [...filters, duplicate]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const onFieldChange = filter => {
|
|
79
|
+
const previousType = filter.type
|
|
80
|
+
sanitizeTypes(filter)
|
|
81
|
+
sanitizeOperator(filter)
|
|
82
|
+
sanitizeValue(filter, previousType)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const onOperatorChange = filter => {
|
|
86
|
+
sanitizeOperator(filter)
|
|
87
|
+
sanitizeValue(filter, filter.type)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const onValueTypeChange = filter => {
|
|
91
|
+
sanitizeValue(filter)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const getFieldOptions = field => {
|
|
95
|
+
const schema = schemaFields.find(x => x.name === field)
|
|
96
|
+
return schema?.constraints?.inclusion || []
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const getSchema = filter => {
|
|
100
|
+
return schemaFields.find(field => field.name === filter.field)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const getValidOperatorsForType = filter => {
|
|
104
|
+
if (!filter?.field && !filter?.name) {
|
|
105
|
+
return []
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return LuceneUtils.getValidOperatorsForType(
|
|
109
|
+
filter,
|
|
110
|
+
filter.field || filter.name,
|
|
111
|
+
datasource
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
|
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
|
+
|
|
123
|
+
// Update external type based on field
|
|
124
|
+
filter.externalType = getSchema(filter)?.externalType
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const sanitizeOperator = filter => {
|
|
128
|
+
// Ensure a valid operator is selected
|
|
129
|
+
const operators = getValidOperatorsForType(filter).map(x => x.value)
|
|
130
|
+
if (!operators.includes(filter.operator)) {
|
|
131
|
+
filter.operator = operators[0] ?? OperatorOptions.Equals.value
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Update the noValue flag if the operator does not take a value
|
|
135
|
+
const noValueOptions = [
|
|
136
|
+
OperatorOptions.Empty.value,
|
|
137
|
+
OperatorOptions.NotEmpty.value,
|
|
138
|
+
]
|
|
139
|
+
filter.noValue = noValueOptions.includes(filter.operator)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const sanitizeValue = (filter, previousType) => {
|
|
143
|
+
// Check if the operator allows a value at all
|
|
144
|
+
if (filter.noValue) {
|
|
145
|
+
filter.value = null
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Ensure array values are properly set and cleared
|
|
150
|
+
if (Array.isArray(filter.value)) {
|
|
151
|
+
if (filter.valueType !== "Value" || filter.type !== FieldType.ARRAY) {
|
|
152
|
+
filter.value = null
|
|
153
|
+
}
|
|
154
|
+
} else if (
|
|
155
|
+
filter.type === FieldType.ARRAY &&
|
|
156
|
+
filter.valueType === "Value"
|
|
157
|
+
) {
|
|
158
|
+
filter.value = []
|
|
159
|
+
} else if (
|
|
160
|
+
previousType !== filter.type &&
|
|
161
|
+
(previousType === FieldType.BB_REFERENCE ||
|
|
162
|
+
filter.type === FieldType.BB_REFERENCE)
|
|
163
|
+
) {
|
|
164
|
+
filter.value = filter.type === FieldType.ARRAY ? [] : null
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handleAllOr(option) {
|
|
169
|
+
filters = filters.filter(f => f.operator !== "allOr")
|
|
170
|
+
if (option === "or") {
|
|
171
|
+
filters.push({ operator: "allOr" })
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function handleOnEmptyFilter(value) {
|
|
176
|
+
filters = filters?.filter(filter => !filter.onEmptyFilter)
|
|
177
|
+
filters.push({ onEmptyFilter: value })
|
|
178
|
+
}
|
|
179
|
+
</script>
|
|
180
|
+
|
|
181
|
+
<div class="container" class:mobile={$context?.device?.mobile}>
|
|
182
|
+
<Layout noPadding>
|
|
183
|
+
{#if fieldOptions?.length}
|
|
184
|
+
<Body size="S">
|
|
185
|
+
{#if !fieldFilters?.length}
|
|
186
|
+
Add your first filter expression.
|
|
187
|
+
{:else}
|
|
188
|
+
<slot name="filtering-hero-content" />
|
|
189
|
+
{#if behaviourFilters}
|
|
190
|
+
<div class="behaviour-filters">
|
|
191
|
+
<Select
|
|
192
|
+
label="Behaviour"
|
|
193
|
+
value={matchAny ? "or" : "and"}
|
|
194
|
+
options={behaviourOptions}
|
|
195
|
+
getOptionLabel={opt => opt.label}
|
|
196
|
+
getOptionValue={opt => opt.value}
|
|
197
|
+
on:change={e => handleAllOr(e.detail)}
|
|
198
|
+
placeholder={null}
|
|
199
|
+
/>
|
|
200
|
+
{#if datasource?.type === "table"}
|
|
201
|
+
<Select
|
|
202
|
+
label="When filter empty"
|
|
203
|
+
value={onEmptyFilter}
|
|
204
|
+
options={onEmptyOptions}
|
|
205
|
+
getOptionLabel={opt => opt.label}
|
|
206
|
+
getOptionValue={opt => opt.value}
|
|
207
|
+
on:change={e => handleOnEmptyFilter(e.detail)}
|
|
208
|
+
placeholder={null}
|
|
209
|
+
/>
|
|
210
|
+
{/if}
|
|
211
|
+
</div>
|
|
212
|
+
{/if}
|
|
213
|
+
{/if}
|
|
214
|
+
</Body>
|
|
215
|
+
{#if fieldFilters?.length}
|
|
216
|
+
<div>
|
|
217
|
+
{#if filtersLabel}
|
|
218
|
+
<div class="filter-label">
|
|
219
|
+
<Label>{filtersLabel}</Label>
|
|
220
|
+
</div>
|
|
221
|
+
{/if}
|
|
222
|
+
<div class="fields" class:with-bindings={allowBindings}>
|
|
223
|
+
{#each fieldFilters as filter}
|
|
224
|
+
<Select
|
|
225
|
+
bind:value={filter.field}
|
|
226
|
+
options={fieldOptions}
|
|
227
|
+
on:change={() => onFieldChange(filter)}
|
|
228
|
+
placeholder="Column"
|
|
229
|
+
/>
|
|
230
|
+
<Select
|
|
231
|
+
disabled={!filter.field}
|
|
232
|
+
options={getValidOperatorsForType(filter)}
|
|
233
|
+
bind:value={filter.operator}
|
|
234
|
+
on:change={() => onOperatorChange(filter)}
|
|
235
|
+
placeholder={null}
|
|
236
|
+
/>
|
|
237
|
+
{#if allowBindings}
|
|
238
|
+
<Select
|
|
239
|
+
disabled={filter.noValue || !filter.field}
|
|
240
|
+
options={valueTypeOptions}
|
|
241
|
+
bind:value={filter.valueType}
|
|
242
|
+
on:change={() => onValueTypeChange(filter)}
|
|
243
|
+
placeholder={null}
|
|
244
|
+
/>
|
|
245
|
+
{/if}
|
|
246
|
+
{#if allowBindings && filter.field && filter.valueType === "Binding"}
|
|
247
|
+
<slot name="binding" {filter} />
|
|
248
|
+
{:else if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA].includes(filter.type)}
|
|
249
|
+
<Input disabled={filter.noValue} bind:value={filter.value} />
|
|
250
|
+
{:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchFilterOperator.ONE_OF)}
|
|
251
|
+
<Multiselect
|
|
252
|
+
disabled={filter.noValue}
|
|
253
|
+
options={getFieldOptions(filter.field)}
|
|
254
|
+
bind:value={filter.value}
|
|
255
|
+
/>
|
|
256
|
+
{:else if filter.type === FieldType.OPTIONS}
|
|
257
|
+
<Combobox
|
|
258
|
+
disabled={filter.noValue}
|
|
259
|
+
options={getFieldOptions(filter.field)}
|
|
260
|
+
bind:value={filter.value}
|
|
261
|
+
/>
|
|
262
|
+
{:else if filter.type === FieldType.BOOLEAN}
|
|
263
|
+
<Combobox
|
|
264
|
+
disabled={filter.noValue}
|
|
265
|
+
options={[
|
|
266
|
+
{ label: "True", value: "true" },
|
|
267
|
+
{ label: "False", value: "false" },
|
|
268
|
+
]}
|
|
269
|
+
bind:value={filter.value}
|
|
270
|
+
/>
|
|
271
|
+
{:else if filter.type === FieldType.DATETIME}
|
|
272
|
+
<DatePicker
|
|
273
|
+
disabled={filter.noValue}
|
|
274
|
+
enableTime={!getSchema(filter)?.dateOnly}
|
|
275
|
+
timeOnly={getSchema(filter)?.timeOnly}
|
|
276
|
+
bind:value={filter.value}
|
|
277
|
+
/>
|
|
278
|
+
{:else if filter.type === FieldType.BB_REFERENCE}
|
|
279
|
+
<FilterUsers
|
|
280
|
+
bind:value={filter.value}
|
|
281
|
+
multiselect={[
|
|
282
|
+
OperatorOptions.In.value,
|
|
283
|
+
OperatorOptions.ContainsAny.value,
|
|
284
|
+
].includes(filter.operator)}
|
|
285
|
+
disabled={filter.noValue}
|
|
286
|
+
/>
|
|
287
|
+
{:else}
|
|
288
|
+
<Input disabled />
|
|
289
|
+
{/if}
|
|
290
|
+
<div class="controls">
|
|
291
|
+
<Icon
|
|
292
|
+
name="Duplicate"
|
|
293
|
+
hoverable
|
|
294
|
+
size="S"
|
|
295
|
+
on:click={() => duplicateFilter(filter.id)}
|
|
296
|
+
/>
|
|
297
|
+
<Icon
|
|
298
|
+
name="Close"
|
|
299
|
+
hoverable
|
|
300
|
+
size="S"
|
|
301
|
+
on:click={() => removeFilter(filter.id)}
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
{/each}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
{/if}
|
|
308
|
+
<div>
|
|
309
|
+
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
|
310
|
+
Add filter
|
|
311
|
+
</Button>
|
|
312
|
+
</div>
|
|
313
|
+
{:else}
|
|
314
|
+
<Body size="S">None of the table column can be used for filtering.</Body>
|
|
315
|
+
{/if}
|
|
316
|
+
</Layout>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<style>
|
|
320
|
+
.container {
|
|
321
|
+
width: 100%;
|
|
322
|
+
max-width: 1000px;
|
|
323
|
+
margin: 0 auto;
|
|
324
|
+
}
|
|
325
|
+
.fields {
|
|
326
|
+
display: grid;
|
|
327
|
+
column-gap: var(--spacing-l);
|
|
328
|
+
row-gap: var(--spacing-s);
|
|
329
|
+
align-items: center;
|
|
330
|
+
grid-template-columns: 1fr 120px 1fr auto auto;
|
|
331
|
+
}
|
|
332
|
+
.fields.with-bindings {
|
|
333
|
+
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.controls {
|
|
337
|
+
display: contents;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.container.mobile .fields {
|
|
341
|
+
grid-template-columns: 1fr;
|
|
342
|
+
}
|
|
343
|
+
.container.mobile .controls {
|
|
344
|
+
display: flex;
|
|
345
|
+
flex-direction: row;
|
|
346
|
+
justify-content: flex-start;
|
|
347
|
+
align-items: center;
|
|
348
|
+
padding: var(--spacing-s) 0;
|
|
349
|
+
gap: var(--spacing-s);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.filter-label {
|
|
353
|
+
margin-bottom: var(--spacing-s);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.behaviour-filters {
|
|
357
|
+
display: grid;
|
|
358
|
+
column-gap: var(--spacing-l);
|
|
359
|
+
row-gap: var(--spacing-s);
|
|
360
|
+
align-items: center;
|
|
361
|
+
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
|
362
|
+
}
|
|
363
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { Select, Multiselect } from "@budibase/bbui"
|
|
3
|
+
import { fetchData } from "@budibase/frontend-core"
|
|
4
|
+
import { createAPIClient } from "../api"
|
|
5
|
+
|
|
6
|
+
export let API = createAPIClient()
|
|
7
|
+
export let value = null
|
|
8
|
+
export let disabled
|
|
9
|
+
export let multiselect = false
|
|
10
|
+
|
|
11
|
+
$: fetch = fetchData({
|
|
12
|
+
API,
|
|
13
|
+
datasource: {
|
|
14
|
+
type: "user",
|
|
15
|
+
},
|
|
16
|
+
options: {
|
|
17
|
+
limit: 100,
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
$: options = $fetch.rows
|
|
22
|
+
|
|
23
|
+
$: component = multiselect ? Multiselect : Select
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<svelte:component
|
|
27
|
+
this={component}
|
|
28
|
+
bind:value
|
|
29
|
+
autocomplete
|
|
30
|
+
{options}
|
|
31
|
+
getOptionLabel={option => option.email}
|
|
32
|
+
getOptionValue={option => option._id}
|
|
33
|
+
{disabled}
|
|
34
|
+
/>
|
|
@@ -61,14 +61,6 @@
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const deleteAttachments = async fileList => {
|
|
65
|
-
try {
|
|
66
|
-
return await API.deleteBuilderAttachments(fileList)
|
|
67
|
-
} catch (error) {
|
|
68
|
-
return []
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
64
|
onMount(() => {
|
|
73
65
|
api = {
|
|
74
66
|
focus: () => open(),
|
|
@@ -101,7 +93,6 @@
|
|
|
101
93
|
on:change={e => onChange(e.detail)}
|
|
102
94
|
maximum={maximum || schema.constraints?.length?.maximum}
|
|
103
95
|
{processFiles}
|
|
104
|
-
{deleteAttachments}
|
|
105
96
|
{handleFileTooLarge}
|
|
106
97
|
/>
|
|
107
98
|
</div>
|
package/src/components/index.js
CHANGED
|
@@ -6,3 +6,4 @@ export { default as UserAvatars } from "./UserAvatars.svelte"
|
|
|
6
6
|
export { default as Updating } from "./Updating.svelte"
|
|
7
7
|
export { Grid } from "./grid"
|
|
8
8
|
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
|
|
9
|
+
export { default as FilterBuilder } from "./FilterBuilder.svelte"
|
package/src/fetch/DataFetch.js
CHANGED
|
@@ -348,8 +348,7 @@ export default class DataFetch {
|
|
|
348
348
|
* Determine the feature flag for this datasource definition
|
|
349
349
|
* @param definition
|
|
350
350
|
*/
|
|
351
|
-
|
|
352
|
-
determineFeatureFlags(definition) {
|
|
351
|
+
determineFeatureFlags(_definition) {
|
|
353
352
|
return {
|
|
354
353
|
supportsSearch: false,
|
|
355
354
|
supportsSort: false,
|