Merge remote-tracking branch 'origin/master' into feature/signature-field-and-component
This commit is contained in:
commit
04d3226185
|
@ -42,7 +42,15 @@
|
|||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"varsIgnorePattern": "^_",
|
||||
"argsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"local-rules/no-budibase-imports": "error"
|
||||
}
|
||||
},
|
||||
|
@ -59,7 +67,15 @@
|
|||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"varsIgnorePattern": "^_",
|
||||
"argsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"local-rules/no-test-com": "error",
|
||||
"local-rules/email-domain-example-com": "error",
|
||||
"no-console": "warn",
|
||||
|
@ -89,7 +105,8 @@
|
|||
{
|
||||
"varsIgnorePattern": "^_",
|
||||
"argsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_"
|
||||
"destructuredArrayIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"import/no-relative-packages": "error",
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 328c84234d11d97d840f0eb2c72665b04ba9e4f8
|
||||
Subproject commit eb7d5da233885c5cffd9c255d3e954d0cd39185e
|
|
@ -8,19 +8,9 @@ import {
|
|||
SearchParams,
|
||||
WithRequired,
|
||||
} from "@budibase/types"
|
||||
import { dataFilters } from "@budibase/shared-core"
|
||||
|
||||
const QUERY_START_REGEX = /\d[0-9]*:/g
|
||||
|
||||
export function removeKeyNumbering(key: any): string {
|
||||
if (typeof key === "string" && key.match(QUERY_START_REGEX) != null) {
|
||||
const parts = key.split(":")
|
||||
// remove the number
|
||||
parts.shift()
|
||||
return parts.join(":")
|
||||
} else {
|
||||
return key
|
||||
}
|
||||
}
|
||||
export const removeKeyNumbering = dataFilters.removeKeyNumbering
|
||||
|
||||
/**
|
||||
* Class to build lucene query URLs.
|
||||
|
|
|
@ -115,7 +115,6 @@ class InMemoryQueue implements Partial<Queue> {
|
|||
* a JSON message as this is required by Bull.
|
||||
* @param repeat serves no purpose for the import queue.
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async add(data: any, opts?: JobOptions) {
|
||||
const jobId = opts?.jobId?.toString()
|
||||
if (jobId && this._queuedJobIds.has(jobId)) {
|
||||
|
@ -166,8 +165,7 @@ class InMemoryQueue implements Partial<Queue> {
|
|||
return []
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async removeJobs(pattern: string) {
|
||||
async removeJobs(_pattern: string) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@ import {
|
|||
ContextUser,
|
||||
CouchFindOptions,
|
||||
DatabaseQueryOpts,
|
||||
SearchQuery,
|
||||
SearchQueryOperators,
|
||||
SearchFilters,
|
||||
SearchFilterOperator,
|
||||
SearchUsersRequest,
|
||||
User,
|
||||
} from "@budibase/types"
|
||||
|
@ -44,11 +44,11 @@ function removeUserPassword(users: User | User[]) {
|
|||
return users
|
||||
}
|
||||
|
||||
export function isSupportedUserSearch(query: SearchQuery) {
|
||||
export function isSupportedUserSearch(query: SearchFilters) {
|
||||
const allowed = [
|
||||
{ op: SearchQueryOperators.STRING, key: "email" },
|
||||
{ op: SearchQueryOperators.EQUAL, key: "_id" },
|
||||
{ op: SearchQueryOperators.ONE_OF, key: "_id" },
|
||||
{ op: SearchFilterOperator.STRING, key: "email" },
|
||||
{ op: SearchFilterOperator.EQUAL, key: "_id" },
|
||||
{ op: SearchFilterOperator.ONE_OF, key: "_id" },
|
||||
]
|
||||
for (let [key, operation] of Object.entries(query)) {
|
||||
if (typeof operation !== "object") {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
notifications,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
DrawerContent,
|
||||
} from "@budibase/bbui"
|
||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
||||
|
@ -37,7 +38,7 @@
|
|||
hbAutocomplete,
|
||||
EditorModes,
|
||||
} from "components/common/CodeEditor"
|
||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||
import { LuceneUtils, Utils } from "@budibase/frontend-core"
|
||||
import {
|
||||
getSchemaForDatasourcePlus,
|
||||
|
@ -442,15 +443,16 @@
|
|||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||
Save
|
||||
</Button>
|
||||
<FilterDrawer
|
||||
slot="body"
|
||||
{filters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
panel={AutomationBindingPanel}
|
||||
on:change={e => (tempFilters = e.detail)}
|
||||
/>
|
||||
<DrawerContent slot="body">
|
||||
<FilterBuilder
|
||||
{filters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
panel={AutomationBindingPanel}
|
||||
on:change={e => (tempFilters = e.detail)}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{:else if value.customType === "password"}
|
||||
<Input
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||
|
||||
export let schema
|
||||
export let filters
|
||||
|
@ -40,7 +40,7 @@
|
|||
onConfirm={() => dispatch("change", tempValue)}
|
||||
>
|
||||
<div class="wrapper">
|
||||
<FilterDrawer
|
||||
<FilterBuilder
|
||||
allowBindings={false}
|
||||
{filters}
|
||||
{schemaFields}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
Layout,
|
||||
AbsTooltip,
|
||||
} from "@budibase/bbui"
|
||||
import { SWITCHABLE_TYPES, ValidColumnNameRegex } from "@budibase/shared-core"
|
||||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { tables, datasources } from "stores/builder"
|
||||
|
@ -20,11 +21,6 @@
|
|||
import {
|
||||
FIELDS,
|
||||
RelationshipType,
|
||||
ALLOWABLE_STRING_OPTIONS,
|
||||
ALLOWABLE_NUMBER_OPTIONS,
|
||||
ALLOWABLE_STRING_TYPES,
|
||||
ALLOWABLE_NUMBER_TYPES,
|
||||
SWITCHABLE_TYPES,
|
||||
PrettyRelationshipDefinitions,
|
||||
DB_TYPE_EXTERNAL,
|
||||
} from "constants/backend"
|
||||
|
@ -33,7 +29,6 @@
|
|||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||
import { getBindings } from "components/backend/DataTable/formula"
|
||||
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
||||
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
||||
import { FieldType, FieldSubtype, SourceName } from "@budibase/types"
|
||||
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||
import { RowUtils } from "@budibase/frontend-core"
|
||||
|
@ -61,8 +56,8 @@
|
|||
let primaryDisplay
|
||||
let indexes = [...($tables.selected.indexes || [])]
|
||||
let isCreating = undefined
|
||||
let relationshipPart1 = PrettyRelationshipDefinitions.Many
|
||||
let relationshipPart2 = PrettyRelationshipDefinitions.One
|
||||
let relationshipPart1 = PrettyRelationshipDefinitions.MANY
|
||||
let relationshipPart2 = PrettyRelationshipDefinitions.ONE
|
||||
let relationshipTableIdPrimary = null
|
||||
let relationshipTableIdSecondary = null
|
||||
let table = $tables.selected
|
||||
|
@ -175,7 +170,7 @@
|
|||
$: typeEnabled =
|
||||
!originalName ||
|
||||
(originalName &&
|
||||
SWITCHABLE_TYPES.indexOf(editableColumn.type) !== -1 &&
|
||||
SWITCHABLE_TYPES[field.type] &&
|
||||
!editableColumn?.autocolumn)
|
||||
|
||||
const fieldDefinitions = Object.values(FIELDS).reduce(
|
||||
|
@ -367,16 +362,15 @@
|
|||
}
|
||||
|
||||
function getAllowedTypes() {
|
||||
if (
|
||||
originalName &&
|
||||
ALLOWABLE_STRING_TYPES.indexOf(editableColumn.type) !== -1
|
||||
) {
|
||||
return ALLOWABLE_STRING_OPTIONS
|
||||
} else if (
|
||||
originalName &&
|
||||
ALLOWABLE_NUMBER_TYPES.indexOf(editableColumn.type) !== -1
|
||||
) {
|
||||
return ALLOWABLE_NUMBER_OPTIONS
|
||||
if (originalName) {
|
||||
const possibleTypes = (
|
||||
SWITCHABLE_TYPES[field.type] || [editableColumn.type]
|
||||
).map(t => t.toLowerCase())
|
||||
return Object.entries(FIELDS)
|
||||
.filter(([fieldType]) =>
|
||||
possibleTypes.includes(fieldType.toLowerCase())
|
||||
)
|
||||
.map(([_, fieldDefinition]) => fieldDefinition)
|
||||
}
|
||||
|
||||
const isUsers =
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
"",
|
||||
requiredValidator
|
||||
)
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [repeatPassword, _, repeatTouched] = createValidationStore(
|
||||
"",
|
||||
requiredValidator
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
<script>
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||
|
||||
import { dataFilters } from "@budibase/shared-core"
|
||||
import { FilterBuilder } from "@budibase/frontend-core"
|
||||
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
|
||||
export let schemaFields
|
||||
export let filters = []
|
||||
export let bindings = []
|
||||
export let panel = ClientBindingPanel
|
||||
export let allowBindings = true
|
||||
export let datasource
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let rawFilters
|
||||
|
||||
$: parseFilters(rawFilters)
|
||||
$: dispatch("change", enrichFilters(rawFilters))
|
||||
|
||||
// Remove field key prefixes and determine which behaviours to use
|
||||
const parseFilters = filters => {
|
||||
rawFilters = (filters || []).map(filter => {
|
||||
const { field } = filter
|
||||
let newFilter = { ...filter }
|
||||
delete newFilter.allOr
|
||||
newFilter.field = dataFilters.removeKeyNumbering(field)
|
||||
return newFilter
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
parseFilters(filters)
|
||||
rawFilters.forEach(filter => {
|
||||
filter.type =
|
||||
schemaFields.find(field => field.name === filter.field)?.type ||
|
||||
filter.type
|
||||
})
|
||||
})
|
||||
|
||||
// Add field key prefixes and a special metadata filter object to indicate
|
||||
// how to handle filter behaviour
|
||||
const enrichFilters = rawFilters => {
|
||||
let count = 1
|
||||
return rawFilters
|
||||
.filter(filter => filter.field)
|
||||
.map(filter => ({
|
||||
...filter,
|
||||
field: `${count++}:${filter.field}`,
|
||||
}))
|
||||
.concat(...rawFilters.filter(filter => !filter.field))
|
||||
}
|
||||
</script>
|
||||
|
||||
<FilterBuilder
|
||||
bind:filters={rawFilters}
|
||||
behaviourFilters={true}
|
||||
{schemaFields}
|
||||
{datasource}
|
||||
{allowBindings}
|
||||
>
|
||||
<div slot="filtering-hero-content" />
|
||||
|
||||
<DrawerBindableInput
|
||||
let:filter
|
||||
slot="binding"
|
||||
disabled={filter.noValue}
|
||||
title={filter.field}
|
||||
value={filter.value}
|
||||
placeholder="Value"
|
||||
{panel}
|
||||
{bindings}
|
||||
on:change={event => {
|
||||
const indexToUpdate = rawFilters.findIndex(f => f.id === filter.id)
|
||||
rawFilters[indexToUpdate] = {
|
||||
...rawFilters[indexToUpdate],
|
||||
value: event.detail,
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FilterBuilder>
|
|
@ -1,8 +1,14 @@
|
|||
<script>
|
||||
import { notifications, ActionButton, Button, Drawer } from "@budibase/bbui"
|
||||
import {
|
||||
notifications,
|
||||
ActionButton,
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
||||
import FilterDrawer from "./FilterDrawer.svelte"
|
||||
import FilterBuilder from "./FilterBuilder.svelte"
|
||||
import { selectedScreen } from "stores/builder"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -40,14 +46,15 @@
|
|||
</div>
|
||||
<Drawer bind:this={drawer} title="Filtering" on:drawerHide on:drawerShow>
|
||||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
||||
<FilterDrawer
|
||||
slot="body"
|
||||
filters={value}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
{datasource}
|
||||
on:change={e => (tempValue = e.detail)}
|
||||
/>
|
||||
<DrawerContent slot="body">
|
||||
<FilterBuilder
|
||||
filters={value}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
{datasource}
|
||||
on:change={e => (tempValue = e.detail)}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -211,26 +211,6 @@ export const PrettyRelationshipDefinitions = {
|
|||
ONE: "One row",
|
||||
}
|
||||
|
||||
export const ALLOWABLE_STRING_OPTIONS = [
|
||||
FIELDS.STRING,
|
||||
FIELDS.OPTIONS,
|
||||
FIELDS.LONGFORM,
|
||||
FIELDS.BARCODEQR,
|
||||
]
|
||||
export const ALLOWABLE_STRING_TYPES = ALLOWABLE_STRING_OPTIONS.map(
|
||||
opt => opt.type
|
||||
)
|
||||
|
||||
export const ALLOWABLE_NUMBER_OPTIONS = [FIELDS.NUMBER, FIELDS.BOOLEAN]
|
||||
export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
|
||||
opt => opt.type
|
||||
)
|
||||
|
||||
export const SWITCHABLE_TYPES = [
|
||||
...ALLOWABLE_STRING_TYPES,
|
||||
...ALLOWABLE_NUMBER_TYPES,
|
||||
]
|
||||
|
||||
export const BUDIBASE_INTERNAL_DB_ID = INTERNAL_TABLE_SOURCE_ID
|
||||
export const DEFAULT_BB_DATASOURCE_ID = "datasource_internal_bb_default"
|
||||
export const BUDIBASE_DATASOURCE_TYPE = "budibase"
|
||||
|
|
|
@ -4,8 +4,6 @@ import {
|
|||
createDatasourceCreationStore,
|
||||
} from "./datasourceCreation"
|
||||
import { get } from "svelte/store"
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { shouldIntegrationFetchTableNames } from "stores/selectors"
|
||||
|
||||
vi.mock("stores/selectors", () => ({
|
||||
shouldIntegrationFetchTableNames: vi.fn(),
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||
import { createOnGoogleAuthStore } from "./onGoogleAuth"
|
||||
import { writable, get } from "svelte/store"
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
// eslint-disable-next-line
|
||||
import { params } from "@roxi/routify"
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
// eslint-disable-next-line
|
||||
import { integrations } from "stores/builder"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { FieldType } from "@budibase/types"
|
||||
import { SWITCHABLE_TYPES } from "@budibase/shared-core"
|
||||
import { get, writable, derived } from "svelte/store"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { API } from "api"
|
||||
import { SWITCHABLE_TYPES } from "constants/backend"
|
||||
|
||||
export function createTablesStore() {
|
||||
const store = writable({
|
||||
|
@ -64,7 +64,7 @@ export function createTablesStore() {
|
|||
if (
|
||||
oldField != null &&
|
||||
oldField?.type !== field.type &&
|
||||
SWITCHABLE_TYPES.indexOf(oldField?.type) === -1
|
||||
!SWITCHABLE_TYPES[oldField?.type]?.includes(field.type)
|
||||
) {
|
||||
updatedTable.schema[key] = oldField
|
||||
}
|
||||
|
@ -148,12 +148,6 @@ export function createTablesStore() {
|
|||
if (indexes) {
|
||||
draft.indexes = indexes
|
||||
}
|
||||
// Add object to indicate if column is being added
|
||||
if (draft.schema[field.name] === undefined) {
|
||||
draft._add = {
|
||||
name: field.name,
|
||||
}
|
||||
}
|
||||
draft.schema = {
|
||||
...draft.schema,
|
||||
[field.name]: cloneDeep(field),
|
||||
|
|
|
@ -105,7 +105,6 @@ export function getAppService(path: string) {
|
|||
}
|
||||
|
||||
export function updateDockerComposeService(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
updateFn: (service: DockerCompose) => void
|
||||
) {
|
||||
const opts = ["docker-compose.yaml", "docker-compose.yml"]
|
||||
|
|
|
@ -1,216 +1,14 @@
|
|||
<script>
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Combobox,
|
||||
DatePicker,
|
||||
Icon,
|
||||
Input,
|
||||
Layout,
|
||||
Select,
|
||||
} from "@budibase/bbui"
|
||||
import { generate } from "shortid"
|
||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
||||
import { getContext } from "svelte"
|
||||
import { FilterBuilder } from "@budibase/frontend-core"
|
||||
|
||||
export let schemaFields
|
||||
export let filters = []
|
||||
export let datasource
|
||||
|
||||
const context = getContext("context")
|
||||
const BannedTypes = ["link", "attachment", "json"]
|
||||
|
||||
$: fieldOptions = (schemaFields ?? [])
|
||||
.filter(
|
||||
field =>
|
||||
!BannedTypes.includes(field.type) ||
|
||||
(field.type === "formula" && field.formulaType === "static")
|
||||
)
|
||||
.map(field => ({
|
||||
label: field.displayName || field.name,
|
||||
value: field.name,
|
||||
}))
|
||||
|
||||
const addFilter = () => {
|
||||
filters = [
|
||||
...filters,
|
||||
{
|
||||
id: generate(),
|
||||
field: null,
|
||||
operator: Constants.OperatorOptions.Equals.value,
|
||||
value: null,
|
||||
valueType: "Value",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const removeFilter = id => {
|
||||
filters = filters.filter(field => field.id !== id)
|
||||
}
|
||||
|
||||
const duplicateFilter = id => {
|
||||
const existingFilter = filters.find(filter => filter.id === id)
|
||||
const duplicate = { ...existingFilter, id: generate() }
|
||||
filters = [...filters, duplicate]
|
||||
}
|
||||
|
||||
const onFieldChange = (expression, field) => {
|
||||
// Update the field type
|
||||
expression.type = schemaFields.find(x => x.name === field)?.type
|
||||
expression.externalType = schemaFields.find(
|
||||
x => x.name === field
|
||||
)?.externalType
|
||||
|
||||
// Ensure a valid operator is set
|
||||
const validOperators = LuceneUtils.getValidOperatorsForType(
|
||||
{ type: expression.type },
|
||||
expression.field,
|
||||
datasource
|
||||
).map(x => x.value)
|
||||
if (!validOperators.includes(expression.operator)) {
|
||||
expression.operator =
|
||||
validOperators[0] ?? Constants.OperatorOptions.Equals.value
|
||||
onOperatorChange(expression, expression.operator)
|
||||
}
|
||||
|
||||
// if changed to an array, change default value to empty array
|
||||
const idx = filters.findIndex(x => x.field === field)
|
||||
if (expression.type === "array") {
|
||||
filters[idx].value = []
|
||||
} else {
|
||||
filters[idx].value = null
|
||||
}
|
||||
}
|
||||
|
||||
const onOperatorChange = (expression, operator) => {
|
||||
const noValueOptions = [
|
||||
Constants.OperatorOptions.Empty.value,
|
||||
Constants.OperatorOptions.NotEmpty.value,
|
||||
]
|
||||
expression.noValue = noValueOptions.includes(operator)
|
||||
if (expression.noValue) {
|
||||
expression.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const getFieldOptions = field => {
|
||||
const schema = schemaFields.find(x => x.name === field)
|
||||
return schema?.constraints?.inclusion || []
|
||||
}
|
||||
|
||||
const getSchema = filter => {
|
||||
return schemaFields.find(field => field.name === filter.field)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container" class:mobile={$context.device.mobile}>
|
||||
<Layout noPadding>
|
||||
<Body size="S">
|
||||
{#if !filters?.length}
|
||||
Add your first filter expression.
|
||||
{:else}
|
||||
Results are filtered to only those which match all of the following
|
||||
constraints.
|
||||
{/if}
|
||||
</Body>
|
||||
{#if filters?.length}
|
||||
<div class="fields">
|
||||
{#each filters as filter}
|
||||
<Select
|
||||
bind:value={filter.field}
|
||||
options={fieldOptions}
|
||||
on:change={e => onFieldChange(filter, e.detail)}
|
||||
placeholder="Column"
|
||||
/>
|
||||
<Select
|
||||
disabled={!filter.field}
|
||||
options={LuceneUtils.getValidOperatorsForType(
|
||||
{ type: filter.type, subtype: filter.subtype },
|
||||
filter.field,
|
||||
datasource
|
||||
)}
|
||||
bind:value={filter.operator}
|
||||
on:change={e => onOperatorChange(filter, e.detail)}
|
||||
placeholder={null}
|
||||
/>
|
||||
{#if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)}
|
||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||
{:else if ["options", "array"].includes(filter.type)}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={getFieldOptions(filter.field)}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "boolean"}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={[
|
||||
{ label: "True", value: "true" },
|
||||
{ label: "False", value: "false" },
|
||||
]}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "datetime"}
|
||||
<DatePicker
|
||||
disabled={filter.noValue}
|
||||
enableTime={!getSchema(filter).dateOnly}
|
||||
timeOnly={getSchema(filter).timeOnly}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else}
|
||||
<Input disabled />
|
||||
{/if}
|
||||
<div class="controls">
|
||||
<Icon
|
||||
name="Duplicate"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => duplicateFilter(filter.id)}
|
||||
/>
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => removeFilter(filter.id)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
||||
Add filter
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.fields {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr 120px 1fr auto auto;
|
||||
}
|
||||
.controls {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.container.mobile .fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.container.mobile .controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: var(--spacing-s) 0;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
<FilterBuilder bind:filters {schemaFields} {datasource} filtersLabel={null}>
|
||||
<div slot="filtering-hero-content">
|
||||
Results are filtered to only those which match all of the following
|
||||
constraints.
|
||||
</div>
|
||||
</FilterBuilder>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"@budibase/types": "0.0.0",
|
||||
"dayjs": "^1.10.8",
|
||||
"lodash": "4.17.21",
|
||||
"shortid": "2.2.15",
|
||||
"socket.io-client": "^4.6.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,33 +4,36 @@
|
|||
Button,
|
||||
Combobox,
|
||||
DatePicker,
|
||||
DrawerContent,
|
||||
Icon,
|
||||
Input,
|
||||
Label,
|
||||
Layout,
|
||||
Multiselect,
|
||||
Select,
|
||||
Label,
|
||||
Multiselect,
|
||||
} from "@budibase/bbui"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||
import { FieldType, SearchFilterOperator } from "@budibase/types"
|
||||
import { generate } from "shortid"
|
||||
import { Constants, LuceneUtils } from "@budibase/frontend-core"
|
||||
import { getFields } from "helpers/searchFields"
|
||||
import { FieldType } from "@budibase/types"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
||||
import { getContext } from "svelte"
|
||||
import FilterUsers from "./FilterUsers.svelte"
|
||||
|
||||
const { OperatorOptions } = Constants
|
||||
|
||||
export let schemaFields
|
||||
export let filters = []
|
||||
export let bindings = []
|
||||
export let panel = ClientBindingPanel
|
||||
export let allowBindings = true
|
||||
export let datasource
|
||||
export let behaviourFilters = false
|
||||
export let allowBindings = false
|
||||
export let filtersLabel = "Filters"
|
||||
|
||||
$: matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
||||
$: onEmptyFilter =
|
||||
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
|
||||
|
||||
$: fieldFilters = filters.filter(
|
||||
filter => filter.operator !== "allOr" && !filter.onEmptyFilter
|
||||
)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const { OperatorOptions } = Constants
|
||||
const KeyedFieldRegex = /\d[0-9]*:/g
|
||||
const behaviourOptions = [
|
||||
{ value: "and", label: "Match all filters" },
|
||||
{ value: "or", label: "Match any filter" },
|
||||
|
@ -40,62 +43,18 @@
|
|||
{ value: "none", label: "Return no rows" },
|
||||
]
|
||||
|
||||
let rawFilters
|
||||
let matchAny = false
|
||||
let onEmptyFilter = "all"
|
||||
const context = getContext("context")
|
||||
|
||||
$: parseFilters(filters)
|
||||
$: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter))
|
||||
$: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true })
|
||||
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
|
||||
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||
|
||||
// Remove field key prefixes and determine which behaviours to use
|
||||
const parseFilters = filters => {
|
||||
matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
||||
onEmptyFilter =
|
||||
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
|
||||
rawFilters = (filters || [])
|
||||
.filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter)
|
||||
.map(filter => {
|
||||
const { field } = filter
|
||||
let newFilter = { ...filter }
|
||||
delete newFilter.allOr
|
||||
if (typeof field === "string" && field.match(KeyedFieldRegex) != null) {
|
||||
const parts = field.split(":")
|
||||
parts.shift()
|
||||
newFilter.field = parts.join(":")
|
||||
}
|
||||
return newFilter
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
parseFilters(filters)
|
||||
rawFilters.forEach(filter => {
|
||||
filter.type =
|
||||
schemaFields.find(field => field.name === filter.field)?.type ||
|
||||
filter.type
|
||||
})
|
||||
})
|
||||
|
||||
// Add field key prefixes and a special metadata filter object to indicate
|
||||
// how to handle filter behaviour
|
||||
const enrichFilters = (rawFilters, matchAny, onEmptyFilter) => {
|
||||
let count = 1
|
||||
return rawFilters
|
||||
.filter(filter => filter.field)
|
||||
.map(filter => ({
|
||||
...filter,
|
||||
field: `${count++}:${filter.field}`,
|
||||
}))
|
||||
.concat(matchAny ? [{ operator: "allOr" }] : [])
|
||||
.concat([{ onEmptyFilter }])
|
||||
}
|
||||
$: fieldOptions = (schemaFields ?? [])
|
||||
.filter(field => getValidOperatorsForType(field).length)
|
||||
.map(field => ({
|
||||
label: field.displayName || field.name,
|
||||
value: field.name,
|
||||
}))
|
||||
|
||||
const addFilter = () => {
|
||||
rawFilters = [
|
||||
...rawFilters,
|
||||
filters = [
|
||||
...(filters || []),
|
||||
{
|
||||
id: generate(),
|
||||
field: null,
|
||||
|
@ -107,22 +66,57 @@
|
|||
}
|
||||
|
||||
const removeFilter = id => {
|
||||
rawFilters = rawFilters.filter(field => field.id !== id)
|
||||
filters = filters.filter(field => field.id !== id)
|
||||
}
|
||||
|
||||
const duplicateFilter = id => {
|
||||
const existingFilter = rawFilters.find(filter => filter.id === id)
|
||||
const existingFilter = filters.find(filter => filter.id === id)
|
||||
const duplicate = { ...existingFilter, id: generate() }
|
||||
rawFilters = [...rawFilters, duplicate]
|
||||
filters = [...filters, duplicate]
|
||||
}
|
||||
|
||||
const onFieldChange = filter => {
|
||||
const previousType = filter.type
|
||||
sanitizeTypes(filter)
|
||||
sanitizeOperator(filter)
|
||||
sanitizeValue(filter, previousType)
|
||||
}
|
||||
|
||||
const onOperatorChange = filter => {
|
||||
sanitizeOperator(filter)
|
||||
sanitizeValue(filter, filter.type)
|
||||
}
|
||||
|
||||
const onValueTypeChange = filter => {
|
||||
sanitizeValue(filter)
|
||||
}
|
||||
|
||||
const getFieldOptions = field => {
|
||||
const schema = schemaFields.find(x => x.name === field)
|
||||
return schema?.constraints?.inclusion || []
|
||||
}
|
||||
|
||||
const getSchema = filter => {
|
||||
return enrichedSchemaFields.find(field => field.name === filter.field)
|
||||
return schemaFields.find(field => field.name === filter.field)
|
||||
}
|
||||
|
||||
const getValidOperatorsForType = filter => {
|
||||
if (!filter?.field && !filter?.name) {
|
||||
return []
|
||||
}
|
||||
|
||||
return LuceneUtils.getValidOperatorsForType(
|
||||
filter,
|
||||
filter.field || filter.name,
|
||||
datasource
|
||||
)
|
||||
}
|
||||
|
||||
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||
|
||||
const sanitizeTypes = filter => {
|
||||
// Update type based on field
|
||||
const fieldSchema = enrichedSchemaFields.find(x => x.name === filter.field)
|
||||
const fieldSchema = schemaFields.find(x => x.name === filter.field)
|
||||
filter.type = fieldSchema?.type
|
||||
filter.subtype = fieldSchema?.subtype
|
||||
|
||||
|
@ -154,88 +148,79 @@
|
|||
|
||||
// Ensure array values are properly set and cleared
|
||||
if (Array.isArray(filter.value)) {
|
||||
if (filter.valueType !== "Value" || filter.type !== "array") {
|
||||
if (filter.valueType !== "Value" || filter.type !== FieldType.ARRAY) {
|
||||
filter.value = null
|
||||
}
|
||||
} else if (filter.type === "array" && filter.valueType === "Value") {
|
||||
} else if (
|
||||
filter.type === FieldType.ARRAY &&
|
||||
filter.valueType === "Value"
|
||||
) {
|
||||
filter.value = []
|
||||
} else if (
|
||||
previousType !== filter.type &&
|
||||
(previousType === FieldType.BB_REFERENCE ||
|
||||
filter.type === FieldType.BB_REFERENCE)
|
||||
) {
|
||||
filter.value = filter.type === "array" ? [] : null
|
||||
filter.value = filter.type === FieldType.ARRAY ? [] : null
|
||||
}
|
||||
}
|
||||
|
||||
const onFieldChange = filter => {
|
||||
const previousType = filter.type
|
||||
sanitizeTypes(filter)
|
||||
sanitizeOperator(filter)
|
||||
sanitizeValue(filter, previousType)
|
||||
}
|
||||
|
||||
const onOperatorChange = filter => {
|
||||
sanitizeOperator(filter)
|
||||
sanitizeValue(filter, filter.type)
|
||||
}
|
||||
|
||||
const onValueTypeChange = filter => {
|
||||
sanitizeValue(filter)
|
||||
}
|
||||
|
||||
const getFieldOptions = field => {
|
||||
const schema = enrichedSchemaFields.find(x => x.name === field)
|
||||
return schema?.constraints?.inclusion || []
|
||||
}
|
||||
|
||||
const getValidOperatorsForType = filter => {
|
||||
if (!filter?.field) {
|
||||
return []
|
||||
function handleAllOr(option) {
|
||||
filters = filters.filter(f => f.operator !== "allOr")
|
||||
if (option === "or") {
|
||||
filters.push({ operator: "allOr" })
|
||||
}
|
||||
}
|
||||
|
||||
return LuceneUtils.getValidOperatorsForType(
|
||||
{ type: filter.type, subtype: filter.subtype },
|
||||
filter.field,
|
||||
datasource
|
||||
)
|
||||
function handleOnEmptyFilter(value) {
|
||||
filters = filters?.filter(filter => !filter.onEmptyFilter)
|
||||
filters.push({ onEmptyFilter: value })
|
||||
}
|
||||
</script>
|
||||
|
||||
<DrawerContent>
|
||||
<div class="container">
|
||||
<Layout noPadding>
|
||||
{#if !rawFilters?.length}
|
||||
<Body size="S">Add your first filter expression.</Body>
|
||||
{:else}
|
||||
<div class="fields">
|
||||
<Select
|
||||
label="Behaviour"
|
||||
value={matchAny ? "or" : "and"}
|
||||
options={behaviourOptions}
|
||||
getOptionLabel={opt => opt.label}
|
||||
getOptionValue={opt => opt.value}
|
||||
on:change={e => (matchAny = e.detail === "or")}
|
||||
placeholder={null}
|
||||
/>
|
||||
{#if datasource?.type === "table"}
|
||||
<Select
|
||||
label="When filter empty"
|
||||
value={onEmptyFilter}
|
||||
options={onEmptyOptions}
|
||||
getOptionLabel={opt => opt.label}
|
||||
getOptionValue={opt => opt.value}
|
||||
on:change={e => (onEmptyFilter = e.detail)}
|
||||
placeholder={null}
|
||||
/>
|
||||
<div class="container" class:mobile={$context?.device?.mobile}>
|
||||
<Layout noPadding>
|
||||
{#if fieldOptions?.length}
|
||||
<Body size="S">
|
||||
{#if !fieldFilters?.length}
|
||||
Add your first filter expression.
|
||||
{:else}
|
||||
<slot name="filtering-hero-content" />
|
||||
{#if behaviourFilters}
|
||||
<div class="behaviour-filters">
|
||||
<Select
|
||||
label="Behaviour"
|
||||
value={matchAny ? "or" : "and"}
|
||||
options={behaviourOptions}
|
||||
getOptionLabel={opt => opt.label}
|
||||
getOptionValue={opt => opt.value}
|
||||
on:change={e => handleAllOr(e.detail)}
|
||||
placeholder={null}
|
||||
/>
|
||||
{#if datasource?.type === "table"}
|
||||
<Select
|
||||
label="When filter empty"
|
||||
value={onEmptyFilter}
|
||||
options={onEmptyOptions}
|
||||
getOptionLabel={opt => opt.label}
|
||||
getOptionValue={opt => opt.value}
|
||||
on:change={e => handleOnEmptyFilter(e.detail)}
|
||||
placeholder={null}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Body>
|
||||
{#if fieldFilters?.length}
|
||||
<div>
|
||||
<div class="filter-label">
|
||||
<Label>Filters</Label>
|
||||
</div>
|
||||
<div class="fields">
|
||||
{#each rawFilters as filter}
|
||||
{#if filtersLabel}
|
||||
<div class="filter-label">
|
||||
<Label>{filtersLabel}</Label>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="fields" class:with-bindings={allowBindings}>
|
||||
{#each fieldFilters as filter}
|
||||
<Select
|
||||
bind:value={filter.field}
|
||||
options={fieldOptions}
|
||||
|
@ -249,38 +234,32 @@
|
|||
on:change={() => onOperatorChange(filter)}
|
||||
placeholder={null}
|
||||
/>
|
||||
<Select
|
||||
disabled={filter.noValue || !filter.field}
|
||||
options={valueTypeOptions}
|
||||
bind:value={filter.valueType}
|
||||
on:change={() => onValueTypeChange(filter)}
|
||||
placeholder={null}
|
||||
/>
|
||||
{#if filter.field && filter.valueType === "Binding"}
|
||||
<DrawerBindableInput
|
||||
disabled={filter.noValue}
|
||||
title={filter.field}
|
||||
value={filter.value}
|
||||
placeholder="Value"
|
||||
{panel}
|
||||
{bindings}
|
||||
on:change={event => (filter.value = event.detail)}
|
||||
{#if allowBindings}
|
||||
<Select
|
||||
disabled={filter.noValue || !filter.field}
|
||||
options={valueTypeOptions}
|
||||
bind:value={filter.valueType}
|
||||
on:change={() => onValueTypeChange(filter)}
|
||||
placeholder={null}
|
||||
/>
|
||||
{:else if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)}
|
||||
{/if}
|
||||
{#if allowBindings && filter.field && filter.valueType === "Binding"}
|
||||
<slot name="binding" {filter} />
|
||||
{:else if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA].includes(filter.type)}
|
||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||
{:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")}
|
||||
{:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchFilterOperator.ONE_OF)}
|
||||
<Multiselect
|
||||
disabled={filter.noValue}
|
||||
options={getFieldOptions(filter.field)}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "options"}
|
||||
{:else if filter.type === FieldType.OPTIONS}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={getFieldOptions(filter.field)}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "boolean"}
|
||||
{:else if filter.type === FieldType.BOOLEAN}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={[
|
||||
|
@ -289,7 +268,7 @@
|
|||
]}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "datetime"}
|
||||
{:else if filter.type === FieldType.DATETIME}
|
||||
<DatePicker
|
||||
disabled={filter.noValue}
|
||||
enableTime={!getSchema(filter)?.dateOnly}
|
||||
|
@ -306,32 +285,36 @@
|
|||
disabled={filter.noValue}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableInput disabled />
|
||||
<Input disabled />
|
||||
{/if}
|
||||
<Icon
|
||||
name="Duplicate"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => duplicateFilter(filter.id)}
|
||||
/>
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => removeFilter(filter.id)}
|
||||
/>
|
||||
<div class="controls">
|
||||
<Icon
|
||||
name="Duplicate"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => duplicateFilter(filter.id)}
|
||||
/>
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => removeFilter(filter.id)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="bottom">
|
||||
<div>
|
||||
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
||||
Add filter
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
{:else}
|
||||
<Body size="S">None of the table column can be used for filtering.</Body>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
|
@ -339,22 +322,42 @@
|
|||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.fields {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr 120px 1fr auto auto;
|
||||
}
|
||||
.fields.with-bindings {
|
||||
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.container.mobile .fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.container.mobile .controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: var(--spacing-s) 0;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.behaviour-filters {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
align-items: center;
|
||||
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
||||
}
|
||||
</style>
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
import { Select, Multiselect } from "@budibase/bbui"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
import { createAPIClient } from "../api"
|
||||
|
||||
import { API } from "api"
|
||||
|
||||
export let API = createAPIClient()
|
||||
export let value = null
|
||||
export let disabled
|
||||
export let multiselect = false
|
|
@ -7,3 +7,4 @@ export { default as UserAvatars } from "./UserAvatars.svelte"
|
|||
export { default as Updating } from "./Updating.svelte"
|
||||
export { Grid } from "./grid"
|
||||
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
|
||||
export { default as FilterBuilder } from "./FilterBuilder.svelte"
|
||||
|
|
|
@ -348,8 +348,7 @@ export default class DataFetch {
|
|||
* Determine the feature flag for this datasource definition
|
||||
* @param definition
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
determineFeatureFlags(definition) {
|
||||
determineFeatureFlags(_definition) {
|
||||
return {
|
||||
supportsSearch: false,
|
||||
supportsSort: false,
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit c68183402b8fb17248572006531d5293ffc8a9ac
|
||||
Subproject commit 06b1064f7e2f7cac5d4bef2ee999796a2a1f0f2c
|
|
@ -31,7 +31,6 @@ export async function save(
|
|||
renaming?: RenameColumn
|
||||
) {
|
||||
const inputs = ctx.request.body
|
||||
const adding = inputs?._add
|
||||
// can't do this right now
|
||||
delete inputs.rows
|
||||
const tableId = ctx.request.body._id
|
||||
|
@ -44,7 +43,7 @@ export async function save(
|
|||
const { datasource, table } = await sdk.tables.external.save(
|
||||
datasourceId!,
|
||||
inputs,
|
||||
{ tableId, renaming, adding }
|
||||
{ tableId, renaming }
|
||||
)
|
||||
builderSocket?.emitDatasourceUpdate(ctx, datasource)
|
||||
return table
|
||||
|
|
|
@ -77,11 +77,6 @@ export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
|
|||
const renaming = ctx.request.body._rename
|
||||
|
||||
const api = pickApi({ table })
|
||||
// do not pass _rename or _add if saving to CouchDB
|
||||
if (api === internal) {
|
||||
delete ctx.request.body._add
|
||||
delete ctx.request.body._rename
|
||||
}
|
||||
let savedTable = await api.save(ctx, renaming)
|
||||
if (!table._id) {
|
||||
savedTable = sdk.tables.enrichViewSchemas(savedTable)
|
||||
|
|
|
@ -16,7 +16,7 @@ export async function save(
|
|||
ctx: UserCtx<SaveTableRequest, SaveTableResponse>,
|
||||
renaming?: RenameColumn
|
||||
) {
|
||||
const { rows, ...rest } = ctx.request.body
|
||||
const { _rename, rows, ...rest } = ctx.request.body
|
||||
let tableToSave: Table = {
|
||||
_id: generateTableID(),
|
||||
...rest,
|
||||
|
|
|
@ -219,9 +219,6 @@ describe.each([
|
|||
|
||||
it("should add a new column for an internal DB table", async () => {
|
||||
const saveTableRequest: SaveTableRequest = {
|
||||
_add: {
|
||||
name: "NEW_COLUMN",
|
||||
},
|
||||
...basicTable(),
|
||||
}
|
||||
|
||||
|
@ -235,7 +232,6 @@ describe.each([
|
|||
updatedAt: expect.stringMatching(ISO_REGEX_PATTERN),
|
||||
views: {},
|
||||
}
|
||||
delete expectedResponse._add
|
||||
expect(response).toEqual(expectedResponse)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
PermissionLevel,
|
||||
QuotaUsageType,
|
||||
SaveTableRequest,
|
||||
SearchQueryOperators,
|
||||
SearchFilterOperator,
|
||||
SortOrder,
|
||||
SortType,
|
||||
StaticQuotaName,
|
||||
|
@ -132,7 +132,7 @@ describe.each([
|
|||
primaryDisplay: generator.word(),
|
||||
query: [
|
||||
{
|
||||
operator: SearchQueryOperators.EQUAL,
|
||||
operator: SearchFilterOperator.EQUAL,
|
||||
field: "field",
|
||||
value: "value",
|
||||
},
|
||||
|
@ -236,7 +236,7 @@ describe.each([
|
|||
...view,
|
||||
query: [
|
||||
{
|
||||
operator: SearchQueryOperators.EQUAL,
|
||||
operator: SearchFilterOperator.EQUAL,
|
||||
field: "newField",
|
||||
value: "thatValue",
|
||||
},
|
||||
|
@ -263,7 +263,7 @@ describe.each([
|
|||
primaryDisplay: generator.word(),
|
||||
query: [
|
||||
{
|
||||
operator: SearchQueryOperators.EQUAL,
|
||||
operator: SearchFilterOperator.EQUAL,
|
||||
field: generator.word(),
|
||||
value: generator.word(),
|
||||
},
|
||||
|
@ -341,7 +341,7 @@ describe.each([
|
|||
tableId: generator.guid(),
|
||||
query: [
|
||||
{
|
||||
operator: SearchQueryOperators.EQUAL,
|
||||
operator: SearchFilterOperator.EQUAL,
|
||||
field: "newField",
|
||||
value: "thatValue",
|
||||
},
|
||||
|
@ -671,7 +671,7 @@ describe.each([
|
|||
name: generator.guid(),
|
||||
query: [
|
||||
{
|
||||
operator: SearchQueryOperators.EQUAL,
|
||||
operator: SearchFilterOperator.EQUAL,
|
||||
field: "two",
|
||||
value: "bar2",
|
||||
},
|
||||
|
|
|
@ -62,7 +62,6 @@ export const definition: AutomationStepSchema = {
|
|||
}
|
||||
|
||||
export async function run({ inputs }: AutomationStepInput) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { automationId, ...fieldParams } = inputs.automation
|
||||
|
||||
if (await features.isTriggerAutomationRunEnabled()) {
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import { features } from "@budibase/backend-core"
|
||||
import env from "./environment"
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
enum AppFeature {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
API = "api",
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
AUTOMATIONS = "automations",
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
getDatasource,
|
||||
rawQuery,
|
||||
} from "../integrations/tests/utils"
|
||||
import { builderSocket } from "../websockets"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
// @ts-ignore
|
||||
fetch.mockSearch()
|
||||
|
@ -233,72 +232,6 @@ describe("mysql integrations", () => {
|
|||
})
|
||||
|
||||
describe("POST /api/tables/", () => {
|
||||
const emitDatasourceUpdateMock = jest.fn()
|
||||
|
||||
it("will emit the datasource entity schema with externalType to the front-end when adding a new column", async () => {
|
||||
const addColumnToTable: TableRequest = {
|
||||
type: "table",
|
||||
sourceType: TableSourceType.EXTERNAL,
|
||||
name: uniqueTableName(),
|
||||
sourceId: datasource._id!,
|
||||
primary: ["id"],
|
||||
schema: {
|
||||
id: {
|
||||
type: FieldType.AUTO,
|
||||
name: "id",
|
||||
autocolumn: true,
|
||||
},
|
||||
new_column: {
|
||||
type: FieldType.NUMBER,
|
||||
name: "new_column",
|
||||
},
|
||||
},
|
||||
_add: {
|
||||
name: "new_column",
|
||||
},
|
||||
}
|
||||
|
||||
jest
|
||||
.spyOn(builderSocket!, "emitDatasourceUpdate")
|
||||
.mockImplementation(emitDatasourceUpdateMock)
|
||||
|
||||
await makeRequest("post", "/api/tables/", addColumnToTable)
|
||||
|
||||
const expectedTable: TableRequest = {
|
||||
...addColumnToTable,
|
||||
schema: {
|
||||
id: {
|
||||
type: FieldType.NUMBER,
|
||||
name: "id",
|
||||
autocolumn: true,
|
||||
constraints: {
|
||||
presence: false,
|
||||
},
|
||||
externalType: "int unsigned",
|
||||
},
|
||||
new_column: {
|
||||
type: FieldType.NUMBER,
|
||||
name: "new_column",
|
||||
autocolumn: false,
|
||||
constraints: {
|
||||
presence: false,
|
||||
},
|
||||
externalType: "float(8,2)",
|
||||
},
|
||||
},
|
||||
created: true,
|
||||
_id: `${datasource._id}__${addColumnToTable.name}`,
|
||||
}
|
||||
delete expectedTable._add
|
||||
|
||||
expect(emitDatasourceUpdateMock).toHaveBeenCalledTimes(1)
|
||||
const emittedDatasource: Datasource =
|
||||
emitDatasourceUpdateMock.mock.calls[0][1]
|
||||
expect(emittedDatasource.entities![expectedTable.name]).toEqual(
|
||||
expectedTable
|
||||
)
|
||||
})
|
||||
|
||||
it("will rename a column", async () => {
|
||||
await makeRequest("post", "/api/tables/", primaryMySqlTable)
|
||||
|
||||
|
|
|
@ -168,8 +168,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
|||
return ""
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getStringConcat(parts: string[]) {
|
||||
getStringConcat(_parts: string[]) {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from "@budibase/types"
|
||||
import { DocumentType, SEPARATOR } from "../db/utils"
|
||||
import { InvalidColumns, DEFAULT_BB_DATASOURCE_ID } from "../constants"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
import { SWITCHABLE_TYPES, helpers } from "@budibase/shared-core"
|
||||
import env from "../environment"
|
||||
import { Knex } from "knex"
|
||||
|
||||
|
@ -284,8 +284,8 @@ export function isIsoDateString(str: string) {
|
|||
* @param column The column to check, to see if it is a valid relationship.
|
||||
* @param tableIds The IDs of the tables which currently exist.
|
||||
*/
|
||||
export function shouldCopyRelationship(
|
||||
column: { type: string; tableId?: string },
|
||||
function shouldCopyRelationship(
|
||||
column: { type: FieldType.LINK; tableId?: string },
|
||||
tableIds: string[]
|
||||
) {
|
||||
return (
|
||||
|
@ -303,28 +303,18 @@ export function shouldCopyRelationship(
|
|||
* @param column The column to check for options or boolean type.
|
||||
* @param fetchedColumn The fetched column to check for the type in the external database.
|
||||
*/
|
||||
export function shouldCopySpecialColumn(
|
||||
column: { type: string },
|
||||
fetchedColumn: { type: string } | undefined
|
||||
function shouldCopySpecialColumn(
|
||||
column: { type: FieldType },
|
||||
fetchedColumn: { type: FieldType } | undefined
|
||||
) {
|
||||
const isFormula = column.type === FieldType.FORMULA
|
||||
const specialTypes = [
|
||||
FieldType.OPTIONS,
|
||||
FieldType.LONGFORM,
|
||||
FieldType.ARRAY,
|
||||
FieldType.FORMULA,
|
||||
FieldType.BB_REFERENCE,
|
||||
]
|
||||
// column has been deleted, remove - formulas will never exist, always copy
|
||||
if (!isFormula && column && !fetchedColumn) {
|
||||
return false
|
||||
}
|
||||
const fetchedIsNumber =
|
||||
!fetchedColumn || fetchedColumn.type === FieldType.NUMBER
|
||||
return (
|
||||
specialTypes.indexOf(column.type as FieldType) !== -1 ||
|
||||
(fetchedIsNumber && column.type === FieldType.BOOLEAN)
|
||||
)
|
||||
return fetchedIsNumber && column.type === FieldType.BOOLEAN
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -357,11 +347,44 @@ function copyExistingPropsOver(
|
|||
continue
|
||||
}
|
||||
const column = existingTableSchema[key]
|
||||
|
||||
const existingColumnType = column?.type
|
||||
const updatedColumnType = table.schema[key]?.type
|
||||
|
||||
// If the db column type changed to a non-compatible one, we want to re-fetch it
|
||||
if (
|
||||
shouldCopyRelationship(column, tableIds) ||
|
||||
shouldCopySpecialColumn(column, table.schema[key])
|
||||
updatedColumnType !== existingColumnType &&
|
||||
!SWITCHABLE_TYPES[updatedColumnType]?.includes(existingColumnType)
|
||||
) {
|
||||
table.schema[key] = existingTableSchema[key]
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
column.type === FieldType.LINK &&
|
||||
!shouldCopyRelationship(column, tableIds)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const specialTypes = [
|
||||
FieldType.OPTIONS,
|
||||
FieldType.LONGFORM,
|
||||
FieldType.ARRAY,
|
||||
FieldType.FORMULA,
|
||||
FieldType.BB_REFERENCE,
|
||||
]
|
||||
if (
|
||||
specialTypes.includes(column.type) &&
|
||||
!shouldCopySpecialColumn(column, table.schema[key])
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
table.schema[key] = {
|
||||
...existingTableSchema[key],
|
||||
externalType:
|
||||
existingTableSchema[key].externalType ||
|
||||
table.schema[key].externalType,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ export function init() {
|
|||
// Because we can't pass functions into an Isolate, we remove them from
|
||||
// the passed context and rely on the withHelpers() method to add them
|
||||
// back in.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { helpers, snippets, ...rest } = ctx
|
||||
return vm.withContext(rest, () => vm.execute(js))
|
||||
} catch (error: any) {
|
||||
|
|
|
@ -348,8 +348,7 @@ const preSaveAction: Partial<Record<SourceName, any>> = {
|
|||
* Make sure all datasource entities have a display name selected
|
||||
*/
|
||||
export function setDefaultDisplayColumns(datasource: Datasource) {
|
||||
//
|
||||
for (let entity of Object.values(datasource.entities || {})) {
|
||||
for (const entity of Object.values(datasource.entities || {})) {
|
||||
if (entity.primaryDisplay) {
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -160,7 +160,7 @@ describe("internal search", () => {
|
|||
const response = await search.paginatedSearch(
|
||||
{
|
||||
contains: {
|
||||
column: "a",
|
||||
column: ["a"],
|
||||
colArr: [1, 2, 3],
|
||||
},
|
||||
},
|
||||
|
@ -168,7 +168,7 @@ describe("internal search", () => {
|
|||
)
|
||||
checkLucene(
|
||||
response,
|
||||
`(*:* AND column:a AND colArr:(1 AND 2 AND 3))`,
|
||||
`(*:* AND column:(a) AND colArr:(1 AND 2 AND 3))`,
|
||||
PARAMS
|
||||
)
|
||||
})
|
|
@ -3,7 +3,6 @@ import {
|
|||
Operation,
|
||||
RelationshipType,
|
||||
RenameColumn,
|
||||
AddColumn,
|
||||
Table,
|
||||
TableRequest,
|
||||
ViewV2,
|
||||
|
@ -33,7 +32,7 @@ import * as viewSdk from "../../views"
|
|||
export async function save(
|
||||
datasourceId: string,
|
||||
update: Table,
|
||||
opts?: { tableId?: string; renaming?: RenameColumn; adding?: AddColumn }
|
||||
opts?: { tableId?: string; renaming?: RenameColumn }
|
||||
) {
|
||||
let tableToSave: TableRequest = {
|
||||
...update,
|
||||
|
@ -179,14 +178,7 @@ export async function save(
|
|||
// remove the rename prop
|
||||
delete tableToSave._rename
|
||||
|
||||
// if adding a new column, we need to rebuild the schema for that table to get the 'externalType' of the column
|
||||
if (opts?.adding) {
|
||||
datasource.entities[tableToSave.name] = (
|
||||
await datasourceSdk.buildFilteredSchema(datasource, [tableToSave.name])
|
||||
).tables[tableToSave.name]
|
||||
} else {
|
||||
datasource.entities[tableToSave.name] = tableToSave
|
||||
}
|
||||
datasource.entities[tableToSave.name] = tableToSave
|
||||
|
||||
// store it into couch now for budibase reference
|
||||
await db.put(populateExternalTableSchemas(datasource))
|
||||
|
|
|
@ -351,7 +351,6 @@ describe("table sdk", () => {
|
|||
const view: ViewV2 = {
|
||||
...basicView,
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { name, description, ...newTableSchema } = basicTable.schema
|
||||
|
||||
const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined)
|
||||
|
@ -365,7 +364,6 @@ describe("table sdk", () => {
|
|||
const view: ViewV2 = {
|
||||
...basicView,
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { description, ...newTableSchema } = {
|
||||
...basicTable.schema,
|
||||
updatedDescription: {
|
||||
|
@ -450,7 +448,6 @@ describe("table sdk", () => {
|
|||
hiddenField: { visible: false },
|
||||
},
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { name, description, ...newTableSchema } = basicTable.schema
|
||||
|
||||
const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined)
|
||||
|
@ -474,7 +471,6 @@ describe("table sdk", () => {
|
|||
hiddenField: { visible: false },
|
||||
},
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { name, description, ...newTableSchema } = {
|
||||
...basicTable.schema,
|
||||
newField1: {
|
||||
|
@ -506,7 +502,6 @@ describe("table sdk", () => {
|
|||
hiddenField: { visible: false },
|
||||
},
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { description, ...newTableSchema } = {
|
||||
...basicTable.schema,
|
||||
updatedDescription: {
|
||||
|
|
|
@ -374,11 +374,13 @@ class Orchestrator {
|
|||
for (let [innerObject, innerValue] of Object.entries(
|
||||
originalStepInput[key][innerKey]
|
||||
)) {
|
||||
originalStepInput[key][innerKey][innerObject] =
|
||||
automationUtils.substituteLoopStep(
|
||||
innerValue as string,
|
||||
`steps.${loopStepNumber}`
|
||||
)
|
||||
if (typeof innerValue === "string") {
|
||||
originalStepInput[key][innerKey][innerObject] =
|
||||
automationUtils.substituteLoopStep(
|
||||
innerValue,
|
||||
`steps.${loopStepNumber}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -458,7 +460,6 @@ class Orchestrator {
|
|||
inputs,
|
||||
step.schema.inputs
|
||||
)
|
||||
|
||||
try {
|
||||
// appId is always passed
|
||||
const outputs = await stepFn({
|
||||
|
|
|
@ -262,13 +262,11 @@ export class BaseSocket {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async onConnect(socket: Socket) {
|
||||
async onConnect(_socket: Socket) {
|
||||
// Override
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async onDisconnect(socket: Socket) {
|
||||
async onDisconnect(_socket: Socket) {
|
||||
// Override
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { FieldType } from "@budibase/types"
|
||||
|
||||
type SwitchableTypes = Partial<{
|
||||
[K in FieldType]: [K, ...FieldType[]]
|
||||
}>
|
||||
|
||||
export const SWITCHABLE_TYPES: SwitchableTypes = {
|
||||
[FieldType.STRING]: [
|
||||
FieldType.STRING,
|
||||
FieldType.OPTIONS,
|
||||
FieldType.LONGFORM,
|
||||
FieldType.BARCODEQR,
|
||||
],
|
||||
[FieldType.OPTIONS]: [
|
||||
FieldType.OPTIONS,
|
||||
FieldType.STRING,
|
||||
FieldType.LONGFORM,
|
||||
FieldType.BARCODEQR,
|
||||
],
|
||||
[FieldType.LONGFORM]: [
|
||||
FieldType.LONGFORM,
|
||||
FieldType.STRING,
|
||||
FieldType.OPTIONS,
|
||||
FieldType.BARCODEQR,
|
||||
],
|
||||
[FieldType.BARCODEQR]: [
|
||||
FieldType.BARCODEQR,
|
||||
FieldType.STRING,
|
||||
FieldType.OPTIONS,
|
||||
FieldType.LONGFORM,
|
||||
],
|
||||
[FieldType.NUMBER]: [FieldType.NUMBER, FieldType.BOOLEAN],
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
export * from "./api"
|
||||
export * from "./fields"
|
||||
|
||||
export const OperatorOptions = {
|
||||
Equals: {
|
||||
|
|
|
@ -2,10 +2,11 @@ import {
|
|||
Datasource,
|
||||
FieldSubtype,
|
||||
FieldType,
|
||||
FormulaType,
|
||||
SearchFilter,
|
||||
SearchQuery,
|
||||
SearchFilters,
|
||||
SearchQueryFields,
|
||||
SearchQueryOperators,
|
||||
SearchFilterOperator,
|
||||
SortDirection,
|
||||
SortType,
|
||||
} from "@budibase/types"
|
||||
|
@ -19,9 +20,13 @@ const HBS_REGEX = /{{([^{].*?)}}/g
|
|||
* Returns the valid operator options for a certain data type
|
||||
*/
|
||||
export const getValidOperatorsForType = (
|
||||
fieldType: { type: FieldType; subtype?: FieldSubtype },
|
||||
fieldType: {
|
||||
type: FieldType
|
||||
subtype?: FieldSubtype
|
||||
formulaType?: FormulaType
|
||||
},
|
||||
field: string,
|
||||
datasource: Datasource & { tableId: any } // TODO: is this table id ever populated?
|
||||
datasource: Datasource & { tableId: any }
|
||||
) => {
|
||||
const Op = OperatorOptions
|
||||
const stringOps = [
|
||||
|
@ -46,7 +51,7 @@ export const getValidOperatorsForType = (
|
|||
value: string
|
||||
label: string
|
||||
}[] = []
|
||||
const { type, subtype } = fieldType
|
||||
const { type, subtype, formulaType } = fieldType
|
||||
if (type === FieldType.STRING) {
|
||||
ops = stringOps
|
||||
} else if (type === FieldType.NUMBER || type === FieldType.BIGINT) {
|
||||
|
@ -61,7 +66,7 @@ export const getValidOperatorsForType = (
|
|||
ops = stringOps
|
||||
} else if (type === FieldType.DATETIME) {
|
||||
ops = numOps
|
||||
} else if (type === FieldType.FORMULA) {
|
||||
} else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) {
|
||||
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
||||
} else if (type === FieldType.BB_REFERENCE && subtype == FieldSubtype.USER) {
|
||||
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
|
||||
|
@ -94,18 +99,19 @@ export const NoEmptyFilterStrings = [
|
|||
* Removes any fields that contain empty strings that would cause inconsistent
|
||||
* behaviour with how backend tables are filtered (no value means no filter).
|
||||
*/
|
||||
const cleanupQuery = (query: SearchQuery) => {
|
||||
const cleanupQuery = (query: SearchFilters) => {
|
||||
if (!query) {
|
||||
return query
|
||||
}
|
||||
for (let filterField of NoEmptyFilterStrings) {
|
||||
if (!query[filterField]) {
|
||||
const operator = filterField as SearchFilterOperator
|
||||
if (!query[operator]) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (let [key, value] of Object.entries(query[filterField]!)) {
|
||||
for (let [key, value] of Object.entries(query[operator]!)) {
|
||||
if (value == null || value === "") {
|
||||
delete query[filterField]![key]
|
||||
delete query[operator]![key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -115,9 +121,10 @@ const cleanupQuery = (query: SearchQuery) => {
|
|||
/**
|
||||
* Removes a numeric prefix on field names designed to give fields uniqueness
|
||||
*/
|
||||
const removeKeyNumbering = (key: string) => {
|
||||
export const removeKeyNumbering = (key: string): string => {
|
||||
if (typeof key === "string" && key.match(/\d[0-9]*:/g) != null) {
|
||||
const parts = key.split(":")
|
||||
// remove the number
|
||||
parts.shift()
|
||||
return parts.join(":")
|
||||
} else {
|
||||
|
@ -130,7 +137,7 @@ const removeKeyNumbering = (key: string) => {
|
|||
* @param filter the builder filter structure
|
||||
*/
|
||||
export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
||||
let query: SearchQuery = {
|
||||
let query: SearchFilters = {
|
||||
string: {},
|
||||
fuzzy: {},
|
||||
range: {},
|
||||
|
@ -151,6 +158,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
|||
filter.forEach(expression => {
|
||||
let { operator, field, type, value, externalType, onEmptyFilter } =
|
||||
expression
|
||||
const queryOperator = operator as SearchFilterOperator
|
||||
const isHbs =
|
||||
typeof value === "string" && (value.match(HBS_REGEX) || []).length > 0
|
||||
// Parse all values into correct types
|
||||
|
@ -165,8 +173,8 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
|||
if (
|
||||
type === "datetime" &&
|
||||
!isHbs &&
|
||||
operator !== "empty" &&
|
||||
operator !== "notEmpty"
|
||||
queryOperator !== "empty" &&
|
||||
queryOperator !== "notEmpty"
|
||||
) {
|
||||
// Ensure date value is a valid date and parse into correct format
|
||||
if (!value) {
|
||||
|
@ -179,7 +187,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
|||
}
|
||||
}
|
||||
if (type === "number" && typeof value === "string" && !isHbs) {
|
||||
if (operator === "oneOf") {
|
||||
if (queryOperator === "oneOf") {
|
||||
value = value.split(",").map(item => parseFloat(item))
|
||||
} else {
|
||||
value = parseFloat(value)
|
||||
|
@ -219,24 +227,24 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
|||
) {
|
||||
query.range[field].high = value
|
||||
}
|
||||
} else if (query[operator] && operator !== "onEmptyFilter") {
|
||||
} else if (query[queryOperator] && operator !== "onEmptyFilter") {
|
||||
if (type === "boolean") {
|
||||
// Transform boolean filters to cope with null.
|
||||
// "equals false" needs to be "not equals true"
|
||||
// "not equals false" needs to be "equals true"
|
||||
if (operator === "equal" && value === false) {
|
||||
if (queryOperator === "equal" && value === false) {
|
||||
query.notEqual = query.notEqual || {}
|
||||
query.notEqual[field] = true
|
||||
} else if (operator === "notEqual" && value === false) {
|
||||
} else if (queryOperator === "notEqual" && value === false) {
|
||||
query.equal = query.equal || {}
|
||||
query.equal[field] = true
|
||||
} else {
|
||||
query[operator] = query[operator] || {}
|
||||
query[operator]![field] = value
|
||||
query[queryOperator] = query[queryOperator] || {}
|
||||
query[queryOperator]![field] = value
|
||||
}
|
||||
} else {
|
||||
query[operator] = query[operator] || {}
|
||||
query[operator]![field] = value
|
||||
query[queryOperator] = query[queryOperator] || {}
|
||||
query[queryOperator]![field] = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -249,7 +257,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
|||
* @param docs the data
|
||||
* @param query the JSON lucene query
|
||||
*/
|
||||
export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||
export const runLuceneQuery = (docs: any[], query?: SearchFilters) => {
|
||||
if (!docs || !Array.isArray(docs)) {
|
||||
return []
|
||||
}
|
||||
|
@ -263,7 +271,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
|||
// Iterates over a set of filters and evaluates a fail function against a doc
|
||||
const match =
|
||||
(
|
||||
type: keyof SearchQueryFields,
|
||||
type: SearchFilterOperator,
|
||||
failFn: (docValue: any, testValue: any) => boolean
|
||||
) =>
|
||||
(doc: any) => {
|
||||
|
@ -280,7 +288,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
|||
|
||||
// Process a string match (fails if the value does not start with the string)
|
||||
const stringMatch = match(
|
||||
SearchQueryOperators.STRING,
|
||||
SearchFilterOperator.STRING,
|
||||
(docValue: string, testValue: string) => {
|
||||
return (
|
||||
!docValue ||
|
||||
|
@ -291,7 +299,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
|||
|
||||
// Process a fuzzy match (treat the same as starts with when running locally)
|
||||
const fuzzyMatch = match(
|
||||
SearchQueryOperators.FUZZY,
|
||||
SearchFilterOperator.FUZZY,
|
||||
(docValue: string, testValue: string) => {
|
||||
return (
|
||||
!docValue ||
|
||||
|
@ -302,7 +310,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
|||
|
||||
// Process a range match
|
||||
const rangeMatch = match(
|
||||
SearchQueryOperators.RANGE,
|
||||
SearchFilterOperator.RANGE,
|
||||
(
|
||||
docValue: string | number | null,
|
||||
testValue: { low: number; high: number }
|
||||
|
@ -325,7 +333,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
|||
|
||||
// Process an equal match (fails if the value is different)
|
||||
const equalMatch = match(
|
||||
SearchQueryOperators.EQUAL,
|
||||
SearchFilterOperator.EQUAL,
|
||||
(docValue: any, testValue: string | null) => {
|
||||
return testValue != null && testValue !== "" && docValue !== testValue
|
||||
}
|
||||
|
@ -333,7 +341,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
|||
|
||||
// Process a not-equal match (fails if the value is the same)
|
||||
const notEqualMatch = match(
|
||||
SearchQueryOperators.NOT_EQUAL,
|
||||
SearchFilterOperator.NOT_EQUAL,
|
||||
(docValue: any, testValue: string | null) => {
|
||||
return testValue != null && testValue !== "" && docValue === testValue
|
||||
}
|
||||
|
@ -341,7 +349,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
|||
|
||||
// Process an empty match (fails if the value is not empty)
|
||||
const emptyMatch = match(
|
||||
SearchQueryOperators.EMPTY,
|
||||
SearchFilterOperator.EMPTY,
|
||||
(docValue: string | null) => {
|
||||
return docValue != null && docValue !== ""
|
||||
}
|
||||
|
@ -349,7 +357,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
|||
|
||||
// Process a not-empty match (fails is the value is empty)
|
||||
const notEmptyMatch = match(
|
||||
SearchQueryOperators.NOT_EMPTY,
|
||||
SearchFilterOperator.NOT_EMPTY,
|
||||
(docValue: string | null) => {
|
||||
return docValue == null || docValue === ""
|
||||
}
|
||||
|
@ -357,7 +365,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
|||
|
||||
// Process an includes match (fails if the value is not included)
|
||||
const oneOf = match(
|
||||
SearchQueryOperators.ONE_OF,
|
||||
SearchFilterOperator.ONE_OF,
|
||||
(docValue: any, testValue: any) => {
|
||||
if (typeof testValue === "string") {
|
||||
testValue = testValue.split(",")
|
||||
|
@ -370,28 +378,28 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
|||
)
|
||||
|
||||
const containsAny = match(
|
||||
SearchQueryOperators.CONTAINS_ANY,
|
||||
SearchFilterOperator.CONTAINS_ANY,
|
||||
(docValue: any, testValue: any) => {
|
||||
return !docValue?.includes(...testValue)
|
||||
}
|
||||
)
|
||||
|
||||
const contains = match(
|
||||
SearchQueryOperators.CONTAINS,
|
||||
SearchFilterOperator.CONTAINS,
|
||||
(docValue: string | any[], testValue: any[]) => {
|
||||
return !testValue?.every((item: any) => docValue?.includes(item))
|
||||
}
|
||||
)
|
||||
|
||||
const notContains = match(
|
||||
SearchQueryOperators.NOT_CONTAINS,
|
||||
SearchFilterOperator.NOT_CONTAINS,
|
||||
(docValue: string | any[], testValue: any[]) => {
|
||||
return testValue?.every((item: any) => docValue?.includes(item))
|
||||
}
|
||||
)
|
||||
|
||||
const docMatch = (doc: any) => {
|
||||
const filterFunctions: Record<SearchQueryOperators, (doc: any) => boolean> =
|
||||
const filterFunctions: Record<SearchFilterOperator, (doc: any) => boolean> =
|
||||
{
|
||||
string: stringMatch,
|
||||
fuzzy: fuzzyMatch,
|
||||
|
@ -406,7 +414,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
|||
notContains: notContains,
|
||||
}
|
||||
|
||||
const activeFilterKeys: SearchQueryOperators[] = Object.entries(query || {})
|
||||
const activeFilterKeys: SearchFilterOperator[] = Object.entries(query || {})
|
||||
.filter(
|
||||
([key, value]: [string, any]) =>
|
||||
!["allOr", "onEmptyFilter"].includes(key) &&
|
||||
|
@ -474,7 +482,7 @@ export const luceneLimit = (docs: any[], limit: string) => {
|
|||
return docs.slice(0, numLimit)
|
||||
}
|
||||
|
||||
export const hasFilters = (query?: SearchQuery) => {
|
||||
export const hasFilters = (query?: SearchFilters) => {
|
||||
if (!query) {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
SearchQuery,
|
||||
SearchQueryOperators,
|
||||
SearchFilters,
|
||||
SearchFilterOperator,
|
||||
FieldType,
|
||||
SearchFilter,
|
||||
} from "@budibase/types"
|
||||
|
@ -46,8 +46,8 @@ describe("runLuceneQuery", () => {
|
|||
},
|
||||
]
|
||||
|
||||
function buildQuery(filters: { [filterKey: string]: any }): SearchQuery {
|
||||
const query: SearchQuery = {
|
||||
function buildQuery(filters: { [filterKey: string]: any }): SearchFilters {
|
||||
const query: SearchFilters = {
|
||||
string: {},
|
||||
fuzzy: {},
|
||||
range: {},
|
||||
|
@ -63,7 +63,7 @@ describe("runLuceneQuery", () => {
|
|||
}
|
||||
|
||||
for (const filterKey in filters) {
|
||||
query[filterKey as SearchQueryOperators] = filters[filterKey]
|
||||
query[filterKey as SearchFilterOperator] = filters[filterKey]
|
||||
}
|
||||
|
||||
return query
|
||||
|
@ -265,13 +265,13 @@ describe("buildLuceneQuery", () => {
|
|||
it("should parseFloat if the type is a number, but the value is a numeric string", () => {
|
||||
const filter: SearchFilter[] = [
|
||||
{
|
||||
operator: SearchQueryOperators.EQUAL,
|
||||
operator: SearchFilterOperator.EQUAL,
|
||||
field: "customer_id",
|
||||
type: FieldType.NUMBER,
|
||||
value: "1212",
|
||||
},
|
||||
{
|
||||
operator: SearchQueryOperators.ONE_OF,
|
||||
operator: SearchFilterOperator.ONE_OF,
|
||||
field: "customer_id",
|
||||
type: FieldType.NUMBER,
|
||||
value: "1000,1212,3400",
|
||||
|
@ -299,13 +299,13 @@ describe("buildLuceneQuery", () => {
|
|||
it("should not parseFloat if the type is a number, but the value is a handlebars binding string", () => {
|
||||
const filter: SearchFilter[] = [
|
||||
{
|
||||
operator: SearchQueryOperators.EQUAL,
|
||||
operator: SearchFilterOperator.EQUAL,
|
||||
field: "customer_id",
|
||||
type: FieldType.NUMBER,
|
||||
value: "{{ customer_id }}",
|
||||
},
|
||||
{
|
||||
operator: SearchQueryOperators.ONE_OF,
|
||||
operator: SearchFilterOperator.ONE_OF,
|
||||
field: "customer_id",
|
||||
type: FieldType.NUMBER,
|
||||
value: "{{ list_of_customer_ids }}",
|
||||
|
@ -333,19 +333,19 @@ describe("buildLuceneQuery", () => {
|
|||
it("should cast string to boolean if the type is boolean", () => {
|
||||
const filter: SearchFilter[] = [
|
||||
{
|
||||
operator: SearchQueryOperators.EQUAL,
|
||||
operator: SearchFilterOperator.EQUAL,
|
||||
field: "a",
|
||||
type: FieldType.BOOLEAN,
|
||||
value: "not_true",
|
||||
},
|
||||
{
|
||||
operator: SearchQueryOperators.NOT_EQUAL,
|
||||
operator: SearchFilterOperator.NOT_EQUAL,
|
||||
field: "b",
|
||||
type: FieldType.BOOLEAN,
|
||||
value: "not_true",
|
||||
},
|
||||
{
|
||||
operator: SearchQueryOperators.EQUAL,
|
||||
operator: SearchFilterOperator.EQUAL,
|
||||
field: "c",
|
||||
type: FieldType.BOOLEAN,
|
||||
value: "true",
|
||||
|
@ -374,19 +374,19 @@ describe("buildLuceneQuery", () => {
|
|||
it("should split the string for contains operators", () => {
|
||||
const filter: SearchFilter[] = [
|
||||
{
|
||||
operator: SearchQueryOperators.CONTAINS,
|
||||
operator: SearchFilterOperator.CONTAINS,
|
||||
field: "description",
|
||||
type: FieldType.ARRAY,
|
||||
value: "Large box,Heavy box,Small box",
|
||||
},
|
||||
{
|
||||
operator: SearchQueryOperators.NOT_CONTAINS,
|
||||
operator: SearchFilterOperator.NOT_CONTAINS,
|
||||
field: "description",
|
||||
type: FieldType.ARRAY,
|
||||
value: "Large box,Heavy box,Small box",
|
||||
},
|
||||
{
|
||||
operator: SearchQueryOperators.CONTAINS_ANY,
|
||||
operator: SearchFilterOperator.CONTAINS_ANY,
|
||||
field: "description",
|
||||
type: FieldType.ARRAY,
|
||||
value: "Large box,Heavy box,Small box",
|
||||
|
|
|
@ -4,7 +4,6 @@ export const PostProcessorNames = {
|
|||
CONVERT_LITERALS: "convert-literals",
|
||||
}
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
class Postprocessor {
|
||||
name: string
|
||||
private fn: any
|
||||
|
|
|
@ -9,7 +9,6 @@ export const PreprocessorNames = {
|
|||
FINALISE: "finalise",
|
||||
}
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
class Preprocessor {
|
||||
name: string
|
||||
private fn: any
|
||||
|
|
|
@ -1,68 +1,11 @@
|
|||
import { FieldType } from "../../documents"
|
||||
import { EmptyFilterOption } from "../../sdk"
|
||||
import { EmptyFilterOption, SearchFilters } from "../../sdk"
|
||||
|
||||
export type SearchFilter = {
|
||||
operator: keyof SearchQuery
|
||||
operator: keyof SearchFilters | "rangeLow" | "rangeHigh"
|
||||
onEmptyFilter?: EmptyFilterOption
|
||||
field: string
|
||||
type?: FieldType
|
||||
value: any
|
||||
externalType?: string
|
||||
}
|
||||
|
||||
export enum SearchQueryOperators {
|
||||
STRING = "string",
|
||||
FUZZY = "fuzzy",
|
||||
RANGE = "range",
|
||||
EQUAL = "equal",
|
||||
NOT_EQUAL = "notEqual",
|
||||
EMPTY = "empty",
|
||||
NOT_EMPTY = "notEmpty",
|
||||
ONE_OF = "oneOf",
|
||||
CONTAINS = "contains",
|
||||
NOT_CONTAINS = "notContains",
|
||||
CONTAINS_ANY = "containsAny",
|
||||
}
|
||||
|
||||
export type SearchQuery = {
|
||||
allOr?: boolean
|
||||
onEmptyFilter?: EmptyFilterOption
|
||||
[SearchQueryOperators.STRING]?: {
|
||||
[key: string]: string
|
||||
}
|
||||
[SearchQueryOperators.FUZZY]?: {
|
||||
[key: string]: string
|
||||
}
|
||||
[SearchQueryOperators.RANGE]?: {
|
||||
[key: string]: {
|
||||
high: number | string
|
||||
low: number | string
|
||||
}
|
||||
}
|
||||
[SearchQueryOperators.EQUAL]?: {
|
||||
[key: string]: any
|
||||
}
|
||||
[SearchQueryOperators.NOT_EQUAL]?: {
|
||||
[key: string]: any
|
||||
}
|
||||
[SearchQueryOperators.EMPTY]?: {
|
||||
[key: string]: any
|
||||
}
|
||||
[SearchQueryOperators.NOT_EMPTY]?: {
|
||||
[key: string]: any
|
||||
}
|
||||
[SearchQueryOperators.ONE_OF]?: {
|
||||
[key: string]: any[]
|
||||
}
|
||||
[SearchQueryOperators.CONTAINS]?: {
|
||||
[key: string]: any[]
|
||||
}
|
||||
[SearchQueryOperators.NOT_CONTAINS]?: {
|
||||
[key: string]: any[]
|
||||
}
|
||||
[SearchQueryOperators.CONTAINS_ANY]?: {
|
||||
[key: string]: any[]
|
||||
}
|
||||
}
|
||||
|
||||
export type SearchQueryFields = Omit<SearchQuery, "allOr" | "onEmptyFilter">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { User } from "../../documents"
|
||||
import { SearchQuery } from "./searchFilter"
|
||||
import { SearchFilters } from "../../sdk"
|
||||
|
||||
export interface SaveUserResponse {
|
||||
_id: string
|
||||
|
@ -55,7 +55,7 @@ export interface InviteUsersResponse {
|
|||
|
||||
export interface SearchUsersRequest {
|
||||
bookmark?: string
|
||||
query?: SearchQuery
|
||||
query?: SearchFilters
|
||||
appId?: string
|
||||
limit?: number
|
||||
paginate?: boolean
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Document } from "../../document"
|
||||
import { View, ViewV2 } from "../view"
|
||||
import { AddColumn, RenameColumn } from "../../../sdk"
|
||||
import { RenameColumn } from "../../../sdk"
|
||||
import { TableSchema } from "./schema"
|
||||
|
||||
export const INTERNAL_TABLE_SOURCE_ID = "bb_internal"
|
||||
|
@ -30,6 +30,5 @@ export interface Table extends Document {
|
|||
|
||||
export interface TableRequest extends Table {
|
||||
_rename?: RenameColumn
|
||||
_add?: AddColumn
|
||||
created?: boolean
|
||||
}
|
||||
|
|
|
@ -3,47 +3,63 @@ import { Row, Table } from "../documents"
|
|||
import { SortType } from "../api"
|
||||
import { Knex } from "knex"
|
||||
|
||||
export enum SearchFilterOperator {
|
||||
STRING = "string",
|
||||
FUZZY = "fuzzy",
|
||||
RANGE = "range",
|
||||
EQUAL = "equal",
|
||||
NOT_EQUAL = "notEqual",
|
||||
EMPTY = "empty",
|
||||
NOT_EMPTY = "notEmpty",
|
||||
ONE_OF = "oneOf",
|
||||
CONTAINS = "contains",
|
||||
NOT_CONTAINS = "notContains",
|
||||
CONTAINS_ANY = "containsAny",
|
||||
}
|
||||
|
||||
export interface SearchFilters {
|
||||
allOr?: boolean
|
||||
onEmptyFilter?: EmptyFilterOption
|
||||
string?: {
|
||||
[SearchFilterOperator.STRING]?: {
|
||||
[key: string]: string
|
||||
}
|
||||
fuzzy?: {
|
||||
[SearchFilterOperator.FUZZY]?: {
|
||||
[key: string]: string
|
||||
}
|
||||
range?: {
|
||||
[SearchFilterOperator.RANGE]?: {
|
||||
[key: string]: {
|
||||
high: number | string
|
||||
low: number | string
|
||||
}
|
||||
}
|
||||
equal?: {
|
||||
[SearchFilterOperator.EQUAL]?: {
|
||||
[key: string]: any
|
||||
}
|
||||
notEqual?: {
|
||||
[SearchFilterOperator.NOT_EQUAL]?: {
|
||||
[key: string]: any
|
||||
}
|
||||
empty?: {
|
||||
[SearchFilterOperator.EMPTY]?: {
|
||||
[key: string]: any
|
||||
}
|
||||
notEmpty?: {
|
||||
[SearchFilterOperator.NOT_EMPTY]?: {
|
||||
[key: string]: any
|
||||
}
|
||||
oneOf?: {
|
||||
[SearchFilterOperator.ONE_OF]?: {
|
||||
[key: string]: any[]
|
||||
}
|
||||
contains?: {
|
||||
[key: string]: any[] | any
|
||||
}
|
||||
notContains?: {
|
||||
[SearchFilterOperator.CONTAINS]?: {
|
||||
[key: string]: any[]
|
||||
}
|
||||
containsAny?: {
|
||||
[SearchFilterOperator.NOT_CONTAINS]?: {
|
||||
[key: string]: any[]
|
||||
}
|
||||
[SearchFilterOperator.CONTAINS_ANY]?: {
|
||||
[key: string]: any[]
|
||||
}
|
||||
}
|
||||
|
||||
export type SearchQueryFields = Omit<SearchFilters, "allOr" | "onEmptyFilter">
|
||||
|
||||
export interface SortJson {
|
||||
[key: string]: {
|
||||
direction: SortDirection
|
||||
|
@ -61,10 +77,6 @@ export interface RenameColumn {
|
|||
updated: string
|
||||
}
|
||||
|
||||
export interface AddColumn {
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface RelationshipsJson {
|
||||
through?: string
|
||||
from?: string
|
||||
|
|
|
@ -704,7 +704,6 @@ describe("scim", () => {
|
|||
expect(response).toEqual({
|
||||
Resources: expect.arrayContaining(
|
||||
groups.map(g => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { members, ...groupData } = g
|
||||
return groupData
|
||||
})
|
||||
|
@ -724,7 +723,6 @@ describe("scim", () => {
|
|||
expect(response).toEqual({
|
||||
Resources: expect.arrayContaining(
|
||||
groups.map(g => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { members, displayName, ...groupData } = g
|
||||
return groupData
|
||||
})
|
||||
|
@ -874,7 +872,6 @@ describe("scim", () => {
|
|||
qs: "excludedAttributes=members",
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { members, ...expectedResponse } = group
|
||||
|
||||
expect(response).toEqual(expectedResponse)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { features } from "@budibase/backend-core"
|
||||
import env from "./environment"
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
enum WorkerFeature {}
|
||||
|
||||
const featureList: WorkerFeature[] = features.processFeatureEnvVar(
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
InviteUsersRequest,
|
||||
User,
|
||||
CreateAdminUserRequest,
|
||||
SearchQuery,
|
||||
SearchFilters,
|
||||
InviteUsersResponse,
|
||||
} from "@budibase/types"
|
||||
import structures from "../structures"
|
||||
|
@ -150,7 +150,7 @@ export class UserAPI extends TestAPI {
|
|||
}
|
||||
|
||||
searchUsers = (
|
||||
{ query }: { query?: SearchQuery },
|
||||
{ query }: { query?: SearchFilters },
|
||||
opts?: { status?: number; noHeaders?: boolean }
|
||||
) => {
|
||||
const req = this.request
|
||||
|
|
Loading…
Reference in New Issue