Merge branch 'BUDI-9127/oauth2-config-validation' into BUDI-9127/edit-delete-configs-frontend
This commit is contained in:
commit
adb4e8e9ca
|
@ -89,7 +89,7 @@
|
||||||
/* Selection is only meant for standalone list items (non stacked) so we just set a fixed border radius */
|
/* Selection is only meant for standalone list items (non stacked) so we just set a fixed border radius */
|
||||||
.list-item.selected {
|
.list-item.selected {
|
||||||
background-color: var(--spectrum-global-color-blue-100);
|
background-color: var(--spectrum-global-color-blue-100);
|
||||||
border-color: var(--spectrum-global-color-blue-100);
|
border: none;
|
||||||
}
|
}
|
||||||
.list-item.selected:after {
|
.list-item.selected:after {
|
||||||
content: "";
|
content: "";
|
||||||
|
@ -100,7 +100,7 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
border-radius: 4px;
|
border-radius: inherit;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||||
|
|
||||||
export let title = ""
|
export let title: string = ""
|
||||||
export let body = ""
|
export let body: string = ""
|
||||||
export let okText = "Confirm"
|
export let okText: string = "Confirm"
|
||||||
export let cancelText = "Cancel"
|
export let cancelText: string = "Cancel"
|
||||||
export let onOk = undefined
|
export let size: "S" | "M" | "L" | "XL" | undefined = undefined
|
||||||
export let onCancel = undefined
|
export let onOk: (() => void) | undefined = undefined
|
||||||
export let warning = true
|
export let onCancel: (() => void) | undefined = undefined
|
||||||
export let disabled = false
|
export let onClose: (() => void) | undefined = undefined
|
||||||
|
export let warning: boolean = true
|
||||||
|
export let disabled: boolean = false
|
||||||
|
|
||||||
let modal
|
let modal: Modal
|
||||||
|
|
||||||
export const show = () => {
|
export const show = () => {
|
||||||
modal.show()
|
modal.show()
|
||||||
|
@ -20,14 +22,16 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={modal} on:hide={onCancel}>
|
<Modal bind:this={modal} on:hide={onClose ?? onCancel}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
onConfirm={onOk}
|
onConfirm={onOk}
|
||||||
|
{onCancel}
|
||||||
{title}
|
{title}
|
||||||
confirmText={okText}
|
confirmText={okText}
|
||||||
{cancelText}
|
{cancelText}
|
||||||
{warning}
|
{warning}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
{size}
|
||||||
>
|
>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
{body}
|
{body}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, params } from "@roxi/routify"
|
import { beforeUrlChange, goto, params } from "@roxi/routify"
|
||||||
import { datasources, flags, integrations, queries } from "@/stores/builder"
|
import { datasources, flags, integrations, queries } from "@/stores/builder"
|
||||||
import { environment } from "@/stores/portal"
|
import { environment } from "@/stores/portal"
|
||||||
import {
|
import {
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
EditorModes,
|
EditorModes,
|
||||||
} from "@/components/common/CodeMirrorEditor.svelte"
|
} from "@/components/common/CodeMirrorEditor.svelte"
|
||||||
import RestBodyInput from "./RestBodyInput.svelte"
|
import RestBodyInput from "./RestBodyInput.svelte"
|
||||||
import { capitalise } from "@/helpers"
|
import { capitalise, confirm } from "@/helpers"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import restUtils from "@/helpers/data/utils"
|
import restUtils from "@/helpers/data/utils"
|
||||||
import {
|
import {
|
||||||
|
@ -50,6 +50,7 @@
|
||||||
toBindingsArray,
|
toBindingsArray,
|
||||||
} from "@/dataBinding"
|
} from "@/dataBinding"
|
||||||
import ConnectedQueryScreens from "./ConnectedQueryScreens.svelte"
|
import ConnectedQueryScreens from "./ConnectedQueryScreens.svelte"
|
||||||
|
import AuthPicker from "./rest/AuthPicker.svelte"
|
||||||
|
|
||||||
export let queryId
|
export let queryId
|
||||||
|
|
||||||
|
@ -63,6 +64,7 @@
|
||||||
let nestedSchemaFields = {}
|
let nestedSchemaFields = {}
|
||||||
let saving
|
let saving
|
||||||
let queryNameLabel
|
let queryNameLabel
|
||||||
|
let mounted = false
|
||||||
|
|
||||||
$: staticVariables = datasource?.config?.staticVariables || {}
|
$: staticVariables = datasource?.config?.staticVariables || {}
|
||||||
|
|
||||||
|
@ -104,8 +106,10 @@
|
||||||
|
|
||||||
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
|
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
|
||||||
|
|
||||||
$: originalQuery = originalQuery ?? cloneDeep(query)
|
|
||||||
$: builtQuery = buildQuery(query, runtimeUrlQueries, requestBindings)
|
$: builtQuery = buildQuery(query, runtimeUrlQueries, requestBindings)
|
||||||
|
$: originalQuery = mounted
|
||||||
|
? originalQuery ?? cloneDeep(builtQuery)
|
||||||
|
: undefined
|
||||||
$: isModified = JSON.stringify(originalQuery) !== JSON.stringify(builtQuery)
|
$: isModified = JSON.stringify(originalQuery) !== JSON.stringify(builtQuery)
|
||||||
|
|
||||||
function getSelectedQuery() {
|
function getSelectedQuery() {
|
||||||
|
@ -208,11 +212,14 @@
|
||||||
originalQuery = null
|
originalQuery = null
|
||||||
|
|
||||||
queryNameLabel.disableEditingState()
|
queryNameLabel.disableEditingState()
|
||||||
|
return { ok: true }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error saving query`)
|
notifications.error(`Error saving query`)
|
||||||
} finally {
|
} finally {
|
||||||
saving = false
|
saving = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { ok: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateQuery = async () => {
|
const validateQuery = async () => {
|
||||||
|
@ -474,6 +481,38 @@
|
||||||
staticVariables,
|
staticVariables,
|
||||||
restBindings
|
restBindings
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mounted = true
|
||||||
|
})
|
||||||
|
|
||||||
|
$beforeUrlChange(async () => {
|
||||||
|
if (!isModified) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return await confirm({
|
||||||
|
title: "Some updates are not saved",
|
||||||
|
body: "Some of your changes are not yet saved. Do you want to save them before leaving?",
|
||||||
|
okText: "Save and continue",
|
||||||
|
cancelText: "Discard and continue",
|
||||||
|
size: "M",
|
||||||
|
onConfirm: async () => {
|
||||||
|
const saveResult = await saveQuery()
|
||||||
|
if (!saveResult.ok) {
|
||||||
|
// We can't leave as the query was not properly saved
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
// Leave without saving anything
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -642,16 +681,13 @@
|
||||||
<div class="auth-container">
|
<div class="auth-container">
|
||||||
<div />
|
<div />
|
||||||
<!-- spacer -->
|
<!-- spacer -->
|
||||||
<div class="auth-select">
|
|
||||||
<Select
|
<AuthPicker
|
||||||
label="Auth"
|
bind:authConfigId={query.fields.authConfigId}
|
||||||
labelPosition="left"
|
{authConfigs}
|
||||||
placeholder="None"
|
datasourceId={datasource._id}
|
||||||
bind:value={query.fields.authConfigId}
|
|
||||||
options={authConfigs}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
@ -853,10 +889,6 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-select {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
ActionButton,
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
PopoverAlignment,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { appStore } from "@/stores/builder"
|
||||||
|
import DetailPopover from "@/components/common/DetailPopover.svelte"
|
||||||
|
|
||||||
|
export let authConfigId: string | undefined
|
||||||
|
export let authConfigs: { label: string; value: string }[]
|
||||||
|
export let datasourceId: string
|
||||||
|
|
||||||
|
let popover: DetailPopover
|
||||||
|
|
||||||
|
$: authConfig = authConfigs.find(c => c.value === authConfigId)
|
||||||
|
|
||||||
|
function addBasicConfiguration() {
|
||||||
|
$goto(
|
||||||
|
`/builder/app/${$appStore.appId}/data/datasource/${datasourceId}?&tab=Authentication`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectConfiguration(id: string) {
|
||||||
|
if (authConfigId === id) {
|
||||||
|
authConfigId = undefined
|
||||||
|
} else {
|
||||||
|
authConfigId = id
|
||||||
|
}
|
||||||
|
popover.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
$: title = !authConfig ? "Authentication" : `Auth: ${authConfig.label}`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DetailPopover bind:this={popover} {title} align={PopoverAlignment.Right}>
|
||||||
|
<div slot="anchor">
|
||||||
|
<ActionButton icon="LockClosed" quiet selected>
|
||||||
|
{#if !authConfig}
|
||||||
|
Authentication
|
||||||
|
{:else}
|
||||||
|
Auth: {authConfig.label}
|
||||||
|
{/if}
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Body size="S" color="var(--spectrum-global-color-gray-700)">
|
||||||
|
Basic & Bearer Authentication
|
||||||
|
</Body>
|
||||||
|
|
||||||
|
{#if authConfigs.length}
|
||||||
|
<List>
|
||||||
|
{#each authConfigs as config}
|
||||||
|
<ListItem
|
||||||
|
title={config.label}
|
||||||
|
on:click={() => selectConfiguration(config.value)}
|
||||||
|
selected={config.value === authConfigId}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</List>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<Button secondary icon="Add" on:click={addBasicConfiguration}
|
||||||
|
>Add config</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</DetailPopover>
|
|
@ -0,0 +1,41 @@
|
||||||
|
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
|
export enum ConfirmOutput {}
|
||||||
|
|
||||||
|
export async function confirm(props: {
|
||||||
|
title: string
|
||||||
|
body?: string
|
||||||
|
okText?: string
|
||||||
|
cancelText?: string
|
||||||
|
size?: "S" | "M" | "L" | "XL"
|
||||||
|
onConfirm?: () => void
|
||||||
|
onCancel?: () => void
|
||||||
|
onClose?: () => void
|
||||||
|
}) {
|
||||||
|
return await new Promise(resolve => {
|
||||||
|
const dialog = new ConfirmDialog({
|
||||||
|
target: document.body,
|
||||||
|
props: {
|
||||||
|
title: props.title,
|
||||||
|
body: props.body,
|
||||||
|
okText: props.okText,
|
||||||
|
cancelText: props.cancelText,
|
||||||
|
size: props.size,
|
||||||
|
warning: false,
|
||||||
|
onOk: () => {
|
||||||
|
dialog.$destroy()
|
||||||
|
resolve(props.onConfirm?.() || true)
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
dialog.$destroy()
|
||||||
|
resolve(props.onCancel?.() || false)
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
dialog.$destroy()
|
||||||
|
resolve(props.onClose?.() || false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
dialog.show()
|
||||||
|
})
|
||||||
|
}
|
|
@ -11,3 +11,4 @@ export {
|
||||||
} from "./helpers"
|
} from "./helpers"
|
||||||
export * as featureFlag from "./featureFlags"
|
export * as featureFlag from "./featureFlags"
|
||||||
export * as bindings from "./bindings"
|
export * as bindings from "./bindings"
|
||||||
|
export * from "./confirm"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { params } from "@roxi/routify"
|
||||||
import { Tabs, Tab, Heading, Body, Layout } from "@budibase/bbui"
|
import { Tabs, Tab, Heading, Body, Layout } from "@budibase/bbui"
|
||||||
import { datasources, integrations } from "@/stores/builder"
|
import { datasources, integrations } from "@/stores/builder"
|
||||||
import ICONS from "@/components/backend/DatasourceNavigator/icons"
|
import ICONS from "@/components/backend/DatasourceNavigator/icons"
|
||||||
|
@ -15,7 +16,7 @@
|
||||||
import { admin } from "@/stores/portal"
|
import { admin } from "@/stores/portal"
|
||||||
import { IntegrationTypes } from "@/constants/backend"
|
import { IntegrationTypes } from "@/constants/backend"
|
||||||
|
|
||||||
let selectedPanel = null
|
let selectedPanel = $params.tab ?? null
|
||||||
let panelOptions = []
|
let panelOptions = []
|
||||||
|
|
||||||
$: datasource = $datasources.selected
|
$: datasource = $datasources.selected
|
||||||
|
|
|
@ -79,6 +79,7 @@
|
||||||
<Heading size="M">Reset your password</Heading>
|
<Heading size="M">Reset your password</Heading>
|
||||||
<Body size="M">Must contain at least 12 characters</Body>
|
<Body size="M">Must contain at least 12 characters</Body>
|
||||||
<PasswordRepeatInput
|
<PasswordRepeatInput
|
||||||
|
bind:passwordForm={form}
|
||||||
bind:password
|
bind:password
|
||||||
bind:error={passwordError}
|
bind:error={passwordError}
|
||||||
minLength={$admin.passwordMinLength || 12}
|
minLength={$admin.passwordMinLength || 12}
|
||||||
|
|
|
@ -41,4 +41,15 @@
|
||||||
div :global(img) {
|
div :global(img) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
div :global(.editor-preview-full) {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
div :global(h1),
|
||||||
|
div :global(h2),
|
||||||
|
div :global(h3),
|
||||||
|
div :global(h4),
|
||||||
|
div :global(h5),
|
||||||
|
div :global(h6) {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,17 +1,27 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
type ValueType = string | string[]
|
||||||
|
type BasicRelatedRow = { _id: string; primaryDisplay: string }
|
||||||
|
type OptionsMap = Record<string, BasicRelatedRow>
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
||||||
import { BasicOperator, FieldType, InternalTable } from "@budibase/types"
|
import {
|
||||||
|
BasicOperator,
|
||||||
|
EmptyFilterOption,
|
||||||
|
FieldType,
|
||||||
|
InternalTable,
|
||||||
|
UILogicalOperator,
|
||||||
|
type LegacyFilter,
|
||||||
|
type SearchFilterGroup,
|
||||||
|
type UISearchFilter,
|
||||||
|
} from "@budibase/types"
|
||||||
import { fetchData, Utils } from "@budibase/frontend-core"
|
import { fetchData, Utils } from "@budibase/frontend-core"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
import type {
|
import type { RelationshipFieldMetadata, Row } from "@budibase/types"
|
||||||
SearchFilter,
|
|
||||||
RelationshipFieldMetadata,
|
|
||||||
Row,
|
|
||||||
} from "@budibase/types"
|
|
||||||
import type { FieldApi, FieldState, FieldValidation } from "@/types"
|
import type { FieldApi, FieldState, FieldValidation } from "@/types"
|
||||||
|
import { utils } from "@budibase/shared-core"
|
||||||
type ValueType = string | string[]
|
|
||||||
|
|
||||||
export let field: string | undefined = undefined
|
export let field: string | undefined = undefined
|
||||||
export let label: string | undefined = undefined
|
export let label: string | undefined = undefined
|
||||||
|
@ -22,7 +32,7 @@
|
||||||
export let autocomplete: boolean = true
|
export let autocomplete: boolean = true
|
||||||
export let defaultValue: ValueType | undefined = undefined
|
export let defaultValue: ValueType | undefined = undefined
|
||||||
export let onChange: (_props: { value: ValueType }) => void
|
export let onChange: (_props: { value: ValueType }) => void
|
||||||
export let filter: SearchFilter[]
|
export let filter: UISearchFilter | LegacyFilter[] | undefined = undefined
|
||||||
export let datasourceType: "table" | "user" = "table"
|
export let datasourceType: "table" | "user" = "table"
|
||||||
export let primaryDisplay: string | undefined = undefined
|
export let primaryDisplay: string | undefined = undefined
|
||||||
export let span: number | undefined = undefined
|
export let span: number | undefined = undefined
|
||||||
|
@ -32,14 +42,10 @@
|
||||||
| FieldType.BB_REFERENCE
|
| FieldType.BB_REFERENCE
|
||||||
| FieldType.BB_REFERENCE_SINGLE = FieldType.LINK
|
| FieldType.BB_REFERENCE_SINGLE = FieldType.LINK
|
||||||
|
|
||||||
type BasicRelatedRow = { _id: string; primaryDisplay: string }
|
|
||||||
type OptionsMap = Record<string, BasicRelatedRow>
|
|
||||||
|
|
||||||
const { API } = getContext("sdk")
|
const { API } = getContext("sdk")
|
||||||
|
|
||||||
// Field state
|
// Field state
|
||||||
let fieldState: FieldState<string | string[]> | undefined
|
let fieldState: FieldState<string | string[]> | undefined
|
||||||
|
|
||||||
let fieldApi: FieldApi
|
let fieldApi: FieldApi
|
||||||
let fieldSchema: RelationshipFieldMetadata | undefined
|
let fieldSchema: RelationshipFieldMetadata | undefined
|
||||||
|
|
||||||
|
@ -52,6 +58,9 @@
|
||||||
let optionsMap: OptionsMap = {}
|
let optionsMap: OptionsMap = {}
|
||||||
let loadingMissingOptions: boolean = false
|
let loadingMissingOptions: boolean = false
|
||||||
|
|
||||||
|
// Reset the available options when our base filter changes
|
||||||
|
$: filter, (optionsMap = {})
|
||||||
|
|
||||||
// Determine if we can select multiple rows or not
|
// Determine if we can select multiple rows or not
|
||||||
$: multiselect =
|
$: multiselect =
|
||||||
[FieldType.LINK, FieldType.BB_REFERENCE].includes(type) &&
|
[FieldType.LINK, FieldType.BB_REFERENCE].includes(type) &&
|
||||||
|
@ -65,7 +74,13 @@
|
||||||
// If writable, we use a fetch to load options
|
// If writable, we use a fetch to load options
|
||||||
$: linkedTableId = fieldSchema?.tableId
|
$: linkedTableId = fieldSchema?.tableId
|
||||||
$: writable = !disabled && !readonly
|
$: writable = !disabled && !readonly
|
||||||
$: fetch = createFetch(writable, datasourceType, filter, linkedTableId)
|
$: migratedFilter = migrateFilter(filter)
|
||||||
|
$: fetch = createFetch(
|
||||||
|
writable,
|
||||||
|
datasourceType,
|
||||||
|
migratedFilter,
|
||||||
|
linkedTableId
|
||||||
|
)
|
||||||
|
|
||||||
// Attempt to determine the primary display field to use
|
// Attempt to determine the primary display field to use
|
||||||
$: tableDefinition = $fetch?.definition
|
$: tableDefinition = $fetch?.definition
|
||||||
|
@ -90,8 +105,8 @@
|
||||||
// Ensure backwards compatibility
|
// Ensure backwards compatibility
|
||||||
$: enrichedDefaultValue = enrichDefaultValue(defaultValue)
|
$: enrichedDefaultValue = enrichDefaultValue(defaultValue)
|
||||||
|
|
||||||
$: emptyValue = multiselect ? [] : undefined
|
|
||||||
// We need to cast value to pass it down, as those components aren't typed
|
// We need to cast value to pass it down, as those components aren't typed
|
||||||
|
$: emptyValue = multiselect ? [] : undefined
|
||||||
$: displayValue = (missingIDs.length ? emptyValue : selectedValue) as any
|
$: displayValue = (missingIDs.length ? emptyValue : selectedValue) as any
|
||||||
|
|
||||||
// Ensures that we flatten any objects so that only the IDs of the selected
|
// Ensures that we flatten any objects so that only the IDs of the selected
|
||||||
|
@ -107,7 +122,7 @@
|
||||||
const createFetch = (
|
const createFetch = (
|
||||||
writable: boolean,
|
writable: boolean,
|
||||||
dsType: typeof datasourceType,
|
dsType: typeof datasourceType,
|
||||||
filter: SearchFilter[],
|
filter: UISearchFilter | undefined,
|
||||||
linkedTableId?: string
|
linkedTableId?: string
|
||||||
) => {
|
) => {
|
||||||
const datasource =
|
const datasource =
|
||||||
|
@ -176,9 +191,16 @@
|
||||||
option: string | BasicRelatedRow | Row,
|
option: string | BasicRelatedRow | Row,
|
||||||
primaryDisplay?: string
|
primaryDisplay?: string
|
||||||
): BasicRelatedRow | null => {
|
): BasicRelatedRow | null => {
|
||||||
|
// For plain strings, check if we already have this option available
|
||||||
|
if (typeof option === "string" && optionsMap[option]) {
|
||||||
|
return optionsMap[option]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise ensure we have a valid option object
|
||||||
if (!option || typeof option !== "object" || !option?._id) {
|
if (!option || typeof option !== "object" || !option?._id) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is a basic related row shape (_id and PD only) then just use
|
// If this is a basic related row shape (_id and PD only) then just use
|
||||||
// that
|
// that
|
||||||
if (Object.keys(option).length === 2 && "primaryDisplay" in option) {
|
if (Object.keys(option).length === 2 && "primaryDisplay" in option) {
|
||||||
|
@ -300,24 +322,54 @@
|
||||||
return val.includes(",") ? val.split(",") : val
|
return val.includes(",") ? val.split(",") : val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We may need to migrate the filter structure, in the case of this being
|
||||||
|
// an old app with LegacyFilter[] saved
|
||||||
|
const migrateFilter = (
|
||||||
|
filter: UISearchFilter | LegacyFilter[] | undefined
|
||||||
|
): UISearchFilter | undefined => {
|
||||||
|
if (Array.isArray(filter)) {
|
||||||
|
return utils.processSearchFilters(filter)
|
||||||
|
}
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
// Searches for new options matching the given term
|
// Searches for new options matching the given term
|
||||||
async function searchOptions(searchTerm: string, primaryDisplay?: string) {
|
async function searchOptions(searchTerm: string, primaryDisplay?: string) {
|
||||||
if (!primaryDisplay) {
|
if (!primaryDisplay) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let newFilter: UISearchFilter | undefined = undefined
|
||||||
// Ensure we match all filters, rather than any
|
let searchFilter: SearchFilterGroup = {
|
||||||
let newFilter = filter
|
logicalOperator: UILogicalOperator.ALL,
|
||||||
if (searchTerm) {
|
filters: [
|
||||||
// @ts-expect-error this doesn't fit types, but don't want to change it yet
|
{
|
||||||
newFilter = (newFilter || []).filter(x => x.operator !== "allOr")
|
field: primaryDisplay,
|
||||||
newFilter.push({
|
|
||||||
// Use a big numeric prefix to avoid clashing with an existing filter
|
|
||||||
field: `999:${primaryDisplay}`,
|
|
||||||
operator: BasicOperator.STRING,
|
operator: BasicOperator.STRING,
|
||||||
value: searchTerm,
|
value: searchTerm,
|
||||||
})
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine the new filter to apply to the fetch
|
||||||
|
if (searchTerm && migratedFilter) {
|
||||||
|
// If we have both a search term and existing filter, filter by both
|
||||||
|
newFilter = {
|
||||||
|
logicalOperator: UILogicalOperator.ALL,
|
||||||
|
groups: [searchFilter, migratedFilter],
|
||||||
|
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
|
||||||
|
}
|
||||||
|
} else if (searchTerm) {
|
||||||
|
// If we just have a search term them use that
|
||||||
|
newFilter = {
|
||||||
|
logicalOperator: UILogicalOperator.ALL,
|
||||||
|
groups: [searchFilter],
|
||||||
|
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Otherwise use the supplied filter untouched
|
||||||
|
newFilter = migratedFilter
|
||||||
|
}
|
||||||
|
|
||||||
await fetch?.update({
|
await fetch?.update({
|
||||||
filter: newFilter,
|
filter: newFilter,
|
||||||
})
|
})
|
||||||
|
@ -389,7 +441,6 @@
|
||||||
bind:searchTerm
|
bind:searchTerm
|
||||||
bind:open
|
bind:open
|
||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
on:loadMore={() => fetch?.nextPage()}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { FancyForm, FancyInput } from "@budibase/bbui"
|
import { FancyForm, FancyInput } from "@budibase/bbui"
|
||||||
import { createValidationStore, requiredValidator } from "../utils/validation"
|
import { createValidationStore, requiredValidator } from "../utils/validation"
|
||||||
|
|
||||||
export let password
|
export let passwordForm: FancyForm | undefined = undefined
|
||||||
export let error
|
export let password: string
|
||||||
|
export let error: string
|
||||||
export let minLength = "12"
|
export let minLength = "12"
|
||||||
|
|
||||||
const validatePassword = value => {
|
const validatePassword = (value: string | undefined) => {
|
||||||
if (!value || value.length < minLength) {
|
if (!value || value.length < parseInt(minLength)) {
|
||||||
return `Please enter at least ${minLength} characters. We recommend using machine generated or random passwords.`
|
return `Please enter at least ${minLength} characters. We recommend using machine generated or random passwords.`
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
@ -35,7 +36,7 @@
|
||||||
firstPasswordError
|
firstPasswordError
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FancyForm>
|
<FancyForm bind:this={passwordForm}>
|
||||||
<FancyInput
|
<FancyInput
|
||||||
label="Password"
|
label="Password"
|
||||||
type="password"
|
type="password"
|
||||||
|
|
|
@ -465,7 +465,7 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.header-cell.searching .name {
|
.header-cell.searching .name {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
@ -219,7 +219,7 @@
|
||||||
--grid-background-alt: var(--spectrum-global-color-gray-100);
|
--grid-background-alt: var(--spectrum-global-color-gray-100);
|
||||||
--header-cell-background: var(
|
--header-cell-background: var(
|
||||||
--custom-header-cell-background,
|
--custom-header-cell-background,
|
||||||
var(--grid-background-alt)
|
var(--spectrum-global-color-gray-100)
|
||||||
);
|
);
|
||||||
--cell-background: var(--grid-background);
|
--cell-background: var(--grid-background);
|
||||||
--cell-background-hover: var(--grid-background-alt);
|
--cell-background-hover: var(--grid-background-alt);
|
||||||
|
|
|
@ -397,14 +397,19 @@ export function parseFilter(filter: UISearchFilter) {
|
||||||
|
|
||||||
const update = cloneDeep(filter)
|
const update = cloneDeep(filter)
|
||||||
|
|
||||||
|
if (update.groups) {
|
||||||
update.groups = update.groups
|
update.groups = update.groups
|
||||||
?.map(group => {
|
.map(group => {
|
||||||
group.filters = group.filters?.filter((filter: any) => {
|
if (group.filters) {
|
||||||
|
group.filters = group.filters.filter((filter: any) => {
|
||||||
return filter.field && filter.operator
|
return filter.field && filter.operator
|
||||||
})
|
})
|
||||||
return group.filters?.length ? group : null
|
return group.filters?.length ? group : null
|
||||||
|
}
|
||||||
|
return group
|
||||||
})
|
})
|
||||||
.filter((group): group is SearchFilterGroup => !!group)
|
.filter((group): group is SearchFilterGroup => !!group)
|
||||||
|
}
|
||||||
|
|
||||||
return update
|
return update
|
||||||
}
|
}
|
||||||
|
|
|
@ -358,8 +358,8 @@ async function performAppCreate(
|
||||||
},
|
},
|
||||||
theme: DefaultAppTheme,
|
theme: DefaultAppTheme,
|
||||||
customTheme: {
|
customTheme: {
|
||||||
primaryColor: "var(--spectrum-global-color-static-blue-1200)",
|
primaryColor: "var(--spectrum-global-color-blue-700)",
|
||||||
primaryColorHover: "var(--spectrum-global-color-static-blue-800)",
|
primaryColorHover: "var(--spectrum-global-color-blue-600)",
|
||||||
buttonBorderRadius: "16px",
|
buttonBorderRadius: "16px",
|
||||||
},
|
},
|
||||||
features: {
|
features: {
|
||||||
|
|
|
@ -381,12 +381,19 @@ export class RestIntegration implements IntegrationBase {
|
||||||
authConfigId?: string,
|
authConfigId?: string,
|
||||||
authConfigType?: RestAuthType
|
authConfigType?: RestAuthType
|
||||||
): Promise<{ [key: string]: any }> {
|
): Promise<{ [key: string]: any }> {
|
||||||
let headers: any = {}
|
if (!authConfigId) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
if (authConfigId) {
|
|
||||||
if (authConfigType === RestAuthType.OAUTH2) {
|
if (authConfigType === RestAuthType.OAUTH2) {
|
||||||
headers.Authorization = await sdk.oauth2.generateToken(authConfigId)
|
return { Authorization: await sdk.oauth2.generateToken(authConfigId) }
|
||||||
} else if (this.config.authConfigs) {
|
}
|
||||||
|
|
||||||
|
if (!this.config.authConfigs) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers: any = {}
|
||||||
const authConfig = this.config.authConfigs.filter(
|
const authConfig = this.config.authConfigs.filter(
|
||||||
c => c._id === authConfigId
|
c => c._id === authConfigId
|
||||||
)[0]
|
)[0]
|
||||||
|
@ -407,8 +414,6 @@ export class RestIntegration implements IntegrationBase {
|
||||||
throw utils.unreachable(type)
|
throw utils.unreachable(type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
import {
|
import {
|
||||||
Datasource,
|
ArrayOperator,
|
||||||
|
BasicOperator,
|
||||||
BBReferenceFieldSubType,
|
BBReferenceFieldSubType,
|
||||||
|
Datasource,
|
||||||
|
EmptyFilterOption,
|
||||||
|
FieldConstraints,
|
||||||
FieldType,
|
FieldType,
|
||||||
FormulaType,
|
FormulaType,
|
||||||
|
isArraySearchOperator,
|
||||||
|
isBasicSearchOperator,
|
||||||
|
isLogicalSearchOperator,
|
||||||
|
isRangeSearchOperator,
|
||||||
LegacyFilter,
|
LegacyFilter,
|
||||||
|
LogicalOperator,
|
||||||
|
RangeOperator,
|
||||||
|
RowSearchParams,
|
||||||
|
SearchFilter,
|
||||||
|
SearchFilterOperator,
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
SearchQueryFields,
|
SearchQueryFields,
|
||||||
ArrayOperator,
|
|
||||||
SearchFilterOperator,
|
|
||||||
SortType,
|
|
||||||
FieldConstraints,
|
|
||||||
SortOrder,
|
|
||||||
RowSearchParams,
|
|
||||||
EmptyFilterOption,
|
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
|
SortOrder,
|
||||||
|
SortType,
|
||||||
Table,
|
Table,
|
||||||
BasicOperator,
|
|
||||||
RangeOperator,
|
|
||||||
LogicalOperator,
|
|
||||||
isLogicalSearchOperator,
|
|
||||||
UISearchFilter,
|
|
||||||
UILogicalOperator,
|
UILogicalOperator,
|
||||||
isBasicSearchOperator,
|
UISearchFilter,
|
||||||
isArraySearchOperator,
|
|
||||||
isRangeSearchOperator,
|
|
||||||
SearchFilter,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
|
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
|
||||||
|
@ -444,6 +444,7 @@ export function buildQuery(
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migrate legacy filters if required
|
||||||
if (Array.isArray(filter)) {
|
if (Array.isArray(filter)) {
|
||||||
filter = processSearchFilters(filter)
|
filter = processSearchFilters(filter)
|
||||||
if (!filter) {
|
if (!filter) {
|
||||||
|
@ -451,10 +452,7 @@ export function buildQuery(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const operator = logicalOperatorFromUI(
|
// Determine top level empty filter behaviour
|
||||||
filter.logicalOperator || UILogicalOperator.ALL
|
|
||||||
)
|
|
||||||
|
|
||||||
const query: SearchFilters = {}
|
const query: SearchFilters = {}
|
||||||
if (filter.onEmptyFilter) {
|
if (filter.onEmptyFilter) {
|
||||||
query.onEmptyFilter = filter.onEmptyFilter
|
query.onEmptyFilter = filter.onEmptyFilter
|
||||||
|
@ -462,8 +460,24 @@ export function buildQuery(
|
||||||
query.onEmptyFilter = EmptyFilterOption.RETURN_ALL
|
query.onEmptyFilter = EmptyFilterOption.RETURN_ALL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default to matching all groups/filters
|
||||||
|
const operator = logicalOperatorFromUI(
|
||||||
|
filter.logicalOperator || UILogicalOperator.ALL
|
||||||
|
)
|
||||||
|
|
||||||
query[operator] = {
|
query[operator] = {
|
||||||
conditions: (filter.groups || []).map(group => {
|
conditions: (filter.groups || []).map(group => {
|
||||||
|
// Check if we contain more groups
|
||||||
|
if (group.groups) {
|
||||||
|
const searchFilter = buildQuery(group)
|
||||||
|
|
||||||
|
// We don't define this properly in the types, but certain fields should
|
||||||
|
// not be present in these nested search filters
|
||||||
|
delete searchFilter.onEmptyFilter
|
||||||
|
return searchFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise handle filters
|
||||||
const { allOr, onEmptyFilter, filters } = splitFiltersArray(
|
const { allOr, onEmptyFilter, filters } = splitFiltersArray(
|
||||||
group.filters || []
|
group.filters || []
|
||||||
)
|
)
|
||||||
|
@ -471,7 +485,7 @@ export function buildQuery(
|
||||||
query.onEmptyFilter = onEmptyFilter
|
query.onEmptyFilter = onEmptyFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
// logicalOperator takes precendence over allOr
|
// logicalOperator takes precedence over allOr
|
||||||
let operator = allOr ? LogicalOperator.OR : LogicalOperator.AND
|
let operator = allOr ? LogicalOperator.OR : LogicalOperator.AND
|
||||||
if (group.logicalOperator) {
|
if (group.logicalOperator) {
|
||||||
operator = logicalOperatorFromUI(group.logicalOperator)
|
operator = logicalOperatorFromUI(group.logicalOperator)
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
import { buildQuery } from "../filters"
|
||||||
|
import {
|
||||||
|
BasicOperator,
|
||||||
|
EmptyFilterOption,
|
||||||
|
FieldType,
|
||||||
|
UILogicalOperator,
|
||||||
|
UISearchFilter,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
describe("filter to query conversion", () => {
|
||||||
|
it("handles a filter with 1 group", () => {
|
||||||
|
const filter: UISearchFilter = {
|
||||||
|
logicalOperator: UILogicalOperator.ALL,
|
||||||
|
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
logicalOperator: UILogicalOperator.ALL,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: "city",
|
||||||
|
operator: BasicOperator.STRING,
|
||||||
|
value: "lon",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const query = buildQuery(filter)
|
||||||
|
expect(query).toEqual({
|
||||||
|
onEmptyFilter: "none",
|
||||||
|
$and: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
$and: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
string: {
|
||||||
|
city: "lon",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles an empty filter", () => {
|
||||||
|
const filter = undefined
|
||||||
|
const query = buildQuery(filter)
|
||||||
|
expect(query).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles legacy filters", () => {
|
||||||
|
const filter = [
|
||||||
|
{
|
||||||
|
field: "city",
|
||||||
|
operator: BasicOperator.STRING,
|
||||||
|
value: "lon",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const query = buildQuery(filter)
|
||||||
|
expect(query).toEqual({
|
||||||
|
onEmptyFilter: "all",
|
||||||
|
$and: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
$and: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
string: {
|
||||||
|
city: "lon",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles nested groups", () => {
|
||||||
|
const filter: UISearchFilter = {
|
||||||
|
logicalOperator: UILogicalOperator.ALL,
|
||||||
|
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
logicalOperator: UILogicalOperator.ALL,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: "city",
|
||||||
|
operator: BasicOperator.STRING,
|
||||||
|
value: "lon",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
logicalOperator: UILogicalOperator.ALL,
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
logicalOperator: UILogicalOperator.ANY,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
valueType: "Binding",
|
||||||
|
field: "country.country_name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
operator: BasicOperator.EQUAL,
|
||||||
|
noValue: false,
|
||||||
|
value: "England",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const query = buildQuery(filter)
|
||||||
|
expect(query).toEqual({
|
||||||
|
onEmptyFilter: "none",
|
||||||
|
$and: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
$and: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
string: {
|
||||||
|
city: "lon",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$and: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
$or: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
equal: {
|
||||||
|
"country.country_name": "England",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -38,11 +38,19 @@ export type SearchFilter = {
|
||||||
// involved. We convert this to a SearchFilters before use with the search SDK.
|
// involved. We convert this to a SearchFilters before use with the search SDK.
|
||||||
export type LegacyFilter = AllOr | OnEmptyFilter | SearchFilter
|
export type LegacyFilter = AllOr | OnEmptyFilter | SearchFilter
|
||||||
|
|
||||||
|
// A search filter group should either contain groups or filters, but not both
|
||||||
export type SearchFilterGroup = {
|
export type SearchFilterGroup = {
|
||||||
logicalOperator?: UILogicalOperator
|
logicalOperator?: UILogicalOperator
|
||||||
groups?: SearchFilterGroup[]
|
} & (
|
||||||
filters?: LegacyFilter[]
|
| {
|
||||||
|
groups?: (SearchFilterGroup | UISearchFilter)[]
|
||||||
|
filters?: never
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
filters?: LegacyFilter[]
|
||||||
|
groups?: never
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// As of v3, this is the format that the frontend always sends when search
|
// As of v3, this is the format that the frontend always sends when search
|
||||||
// filters are involved. We convert this to SearchFilters before use with the
|
// filters are involved. We convert this to SearchFilters before use with the
|
||||||
|
|
Loading…
Reference in New Issue