@budibase/frontend-core 2.23.6 → 2.23.7

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 CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "2.23.6",
3
+ "version": "2.23.7",
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.6",
10
- "@budibase/shared-core": "2.23.6",
11
- "@budibase/types": "2.23.6",
9
+ "@budibase/bbui": "2.23.7",
10
+ "@budibase/shared-core": "2.23.7",
11
+ "@budibase/types": "2.23.7",
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": "c19bcab71d54cb614911afbb6229e9c42bde16aa"
17
+ "gitHead": "c57dcbb5ee45152210670041dc18eae00460b159"
17
18
  }
@@ -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>
@@ -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"
@@ -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
- // eslint-disable-next-line no-unused-vars
352
- determineFeatureFlags(definition) {
351
+ determineFeatureFlags(_definition) {
353
352
  return {
354
353
  supportsSearch: false,
355
354
  supportsSort: false,