Merge remote-tracking branch 'origin/v3-ui' into feature/automation-branching-ux
This commit is contained in:
commit
6d0d4a9b59
|
@ -1,3 +1,3 @@
|
||||||
nodejs 20.10.0
|
nodejs 20.10.0
|
||||||
python 3.10.0
|
python 3.10.0
|
||||||
yarn 1.22.19
|
yarn 1.22.22
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "2.32.9",
|
"version": "2.32.10",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
export let width
|
||||||
|
export let height
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
{width}
|
||||||
|
{height}
|
||||||
|
viewBox="0 0 13 12"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M9.4179 4.13222C9.4179 3.73121 9.26166 3.35428 8.97913 3.07175C8.41342 2.50538 7.4239 2.50408 6.85753 3.07175L5.64342 4.28586C5.6291 4.30018 5.61543 4.3158 5.60305 4.33143C5.58678 4.3438 5.5718 4.35747 5.55683 4.37244L0.491426 9.43785C0.208245 9.72103 0.052002 10.098 0.052002 10.4983C0.052002 10.8987 0.208245 11.2756 0.491426 11.5588C0.774607 11.842 1.15153 11.9982 1.5519 11.9982C1.95227 11.9982 2.32919 11.842 2.61238 11.5588L8.97848 5.1927C9.26166 4.90952 9.4179 4.53259 9.4179 4.13222ZM1.90539 10.8518C1.7166 11.0406 1.3872 11.0406 1.1984 10.8518C1.10401 10.7574 1.05193 10.6318 1.05193 10.4983C1.05193 10.3649 1.104 10.2392 1.1984 10.1448L5.99821 5.34503L6.70845 6.04875L1.90539 10.8518ZM8.2715 4.48571L7.41544 5.34178L6.7052 4.63805L7.56452 3.77873C7.7533 3.58995 8.08271 3.58929 8.2715 3.77939C8.36589 3.87313 8.41798 3.99877 8.41798 4.13223C8.41798 4.26569 8.3659 4.39132 8.2715 4.48571Z"
|
||||||
|
fill="#C8C8C8"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11.8552 6.55146L11.0144 6.21913L10.879 5.32449C10.8356 5.03919 10.3737 4.98776 10.2686 5.255L9.93606 6.09642L9.04143 6.23085C8.89951 6.25216 8.78884 6.36658 8.77257 6.50947C8.75629 6.65253 8.83783 6.78826 8.97193 6.84148L9.81335 7.17464L9.94794 8.06862C9.9691 8.21053 10.0835 8.32121 10.2266 8.33748C10.3695 8.35375 10.5052 8.27221 10.5586 8.13811L10.8914 7.29751L11.7855 7.1621C11.9283 7.1403 12.0381 7.02637 12.0544 6.88348C12.0707 6.74058 11.9887 6.60403 11.8552 6.55146Z"
|
||||||
|
fill="#F9634C"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8.94215 1.76145L9.78356 2.0946L9.91815 2.9885C9.93931 3.13049 10.0539 3.24117 10.1968 3.25744C10.3398 3.27371 10.4756 3.19218 10.5288 3.05807L10.8618 2.21739L11.7559 2.08207C11.8985 2.06034 12.0085 1.94633 12.0248 1.80344C12.0411 1.66054 11.959 1.524 11.8254 1.47143L10.9847 1.13909L10.8494 0.244456C10.806 -0.0409246 10.3439 -0.0922745 10.2388 0.174881L9.90643 1.0163L9.0118 1.15089C8.86972 1.17213 8.75905 1.28654 8.74278 1.42952C8.72651 1.57249 8.80804 1.70823 8.94215 1.76145Z"
|
||||||
|
fill="#8488FD"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.2379 2.46066L3.92063 2.73091L4.02984 3.45637C4.04709 3.57151 4.14002 3.66135 4.25606 3.67453C4.37194 3.6878 4.48212 3.62163 4.52541 3.51276L4.79557 2.83059L5.52094 2.72074C5.63682 2.70316 5.72601 2.61072 5.73936 2.49468C5.75254 2.37864 5.68597 2.26797 5.57758 2.22533L4.89533 1.95565L4.78548 1.22963C4.75016 0.998038 4.37535 0.956375 4.29007 1.17315L4.0204 1.85597L3.29437 1.96517C3.17915 1.98235 3.08931 2.07527 3.07613 2.19131C3.06294 2.30727 3.12902 2.41737 3.2379 2.46066Z"
|
||||||
|
fill="#F7D804"
|
||||||
|
/>
|
||||||
|
</svg>
|
|
@ -67,6 +67,7 @@
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
"@zerodevx/svelte-json-view": "^1.0.7",
|
"@zerodevx/svelte-json-view": "^1.0.7",
|
||||||
"codemirror": "^5.65.16",
|
"codemirror": "^5.65.16",
|
||||||
|
"cron-parser": "^4.9.0",
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
"downloadjs": "1.4.7",
|
"downloadjs": "1.4.7",
|
||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
|
|
|
@ -871,7 +871,7 @@
|
||||||
{:else if value.customType === "cron"}
|
{:else if value.customType === "cron"}
|
||||||
<CronBuilder
|
<CronBuilder
|
||||||
on:change={e => onChange({ [key]: e.detail })}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
value={inputData[key]}
|
cronExpression={inputData[key]}
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "automationFields"}
|
{:else if value.customType === "automationFields"}
|
||||||
<AutomationSelector
|
<AutomationSelector
|
||||||
|
|
|
@ -1,41 +1,70 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Select, Input, Label } from "@budibase/bbui"
|
import {
|
||||||
|
Select,
|
||||||
|
InlineAlert,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Layout,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { onMount, createEventDispatcher } from "svelte"
|
import { onMount, createEventDispatcher } from "svelte"
|
||||||
import { flags } from "stores/builder"
|
import { flags } from "stores/builder"
|
||||||
|
import { licensing } from "stores/portal"
|
||||||
|
import { API } from "api"
|
||||||
|
import MagicWand from "../../../../assets/MagicWand.svelte"
|
||||||
|
|
||||||
import { helpers, REBOOT_CRON } from "@budibase/shared-core"
|
import { helpers, REBOOT_CRON } from "@budibase/shared-core"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value
|
export let cronExpression
|
||||||
|
|
||||||
let error
|
let error
|
||||||
|
let nextExecutions
|
||||||
|
|
||||||
|
// AI prompt
|
||||||
|
let aiCronPrompt = ""
|
||||||
|
let loadingAICronExpression = false
|
||||||
|
|
||||||
|
$: aiEnabled =
|
||||||
|
$licensing.customAIConfigsEnabled || $licensing.budibaseAIEnabled
|
||||||
$: {
|
$: {
|
||||||
const exists = CRON_EXPRESSIONS.some(cron => cron.value === value)
|
if (cronExpression) {
|
||||||
const customIndex = CRON_EXPRESSIONS.findIndex(
|
try {
|
||||||
cron => cron.label === "Custom"
|
nextExecutions = helpers.cron
|
||||||
)
|
.getNextExecutionDates(cronExpression)
|
||||||
|
.join("\n")
|
||||||
if (!exists && customIndex === -1) {
|
} catch (err) {
|
||||||
CRON_EXPRESSIONS[0] = { label: "Custom", value: value }
|
nextExecutions = null
|
||||||
} else if (exists && customIndex !== -1) {
|
}
|
||||||
CRON_EXPRESSIONS.splice(customIndex, 1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
if (value !== REBOOT_CRON) {
|
if (e.detail !== REBOOT_CRON) {
|
||||||
error = helpers.cron.validate(e.detail).err
|
error = helpers.cron.validate(e.detail).err
|
||||||
}
|
}
|
||||||
if (e.detail === value || error) {
|
if (e.detail === cronExpression || error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
value = e.detail
|
cronExpression = e.detail
|
||||||
dispatch("change", e.detail)
|
dispatch("change", e.detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatePreset = e => {
|
||||||
|
aiCronPrompt = ""
|
||||||
|
onChange(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCronExpression = e => {
|
||||||
|
aiCronPrompt = ""
|
||||||
|
cronExpression = null
|
||||||
|
nextExecutions = null
|
||||||
|
onChange(e)
|
||||||
|
}
|
||||||
|
|
||||||
let touched = false
|
let touched = false
|
||||||
let presets = false
|
|
||||||
|
|
||||||
const CRON_EXPRESSIONS = [
|
const CRON_EXPRESSIONS = [
|
||||||
{
|
{
|
||||||
|
@ -64,45 +93,130 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function generateAICronExpression() {
|
||||||
|
loadingAICronExpression = true
|
||||||
|
try {
|
||||||
|
const response = await API.generateCronExpression({
|
||||||
|
prompt: aiCronPrompt,
|
||||||
|
})
|
||||||
|
cronExpression = response.message
|
||||||
|
dispatch("change", response.message)
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(err.message)
|
||||||
|
} finally {
|
||||||
|
loadingAICronExpression = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="block-field">
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<Select
|
||||||
|
on:change={updatePreset}
|
||||||
|
value={cronExpression || "Custom"}
|
||||||
|
secondary
|
||||||
|
extraThin
|
||||||
|
label="Use a Preset (Optional)"
|
||||||
|
options={CRON_EXPRESSIONS}
|
||||||
|
/>
|
||||||
|
{#if aiEnabled}
|
||||||
|
<div class="cron-ai-generator">
|
||||||
<Input
|
<Input
|
||||||
|
bind:value={aiCronPrompt}
|
||||||
|
label="Generate Cron Expression with AI"
|
||||||
|
size="S"
|
||||||
|
placeholder="Run every hour between 1pm to 4pm everyday of the week"
|
||||||
|
/>
|
||||||
|
{#if aiCronPrompt}
|
||||||
|
<div
|
||||||
|
class="icon"
|
||||||
|
class:pulsing-text={loadingAICronExpression}
|
||||||
|
on:click={generateAICronExpression}
|
||||||
|
>
|
||||||
|
<MagicWand height="17" width="17" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<Input
|
||||||
|
label="Cron Expression"
|
||||||
{error}
|
{error}
|
||||||
on:change={onChange}
|
on:change={updateCronExpression}
|
||||||
{value}
|
value={cronExpression}
|
||||||
on:blur={() => (touched = true)}
|
on:blur={() => (touched = true)}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
{#if touched && !value}
|
{#if touched && !cronExpression}
|
||||||
<Label><div class="error">Please specify a CRON expression</div></Label>
|
<Label><div class="error">Please specify a CRON expression</div></Label>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="presets">
|
{#if nextExecutions}
|
||||||
<Button on:click={() => (presets = !presets)}
|
<InlineAlert
|
||||||
>{presets ? "Hide" : "Show"} Presets</Button
|
type="info"
|
||||||
>
|
header="Next Executions"
|
||||||
{#if presets}
|
message={nextExecutions}
|
||||||
<Select
|
|
||||||
on:change={onChange}
|
|
||||||
value={value || "Custom"}
|
|
||||||
secondary
|
|
||||||
extraThin
|
|
||||||
label="Presets"
|
|
||||||
options={CRON_EXPRESSIONS}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</Layout>
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.presets {
|
.cron-ai-generator {
|
||||||
margin-top: var(--spacing-m);
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.block-field {
|
.icon {
|
||||||
padding-top: var(--spacing-s);
|
right: 1px;
|
||||||
|
bottom: 1px;
|
||||||
|
position: absolute;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-left: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
width: 31px;
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
background-color: var(--spectrum-global-color-gray-75);
|
||||||
|
transition: background-color
|
||||||
|
var(--spectrum-global-animation-duration-100, 130ms),
|
||||||
|
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
|
||||||
|
border-color var(--spectrum-global-animation-duration-100, 130ms);
|
||||||
|
height: calc(var(--spectrum-alias-item-height-m) - 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--spectrum-alias-text-color-hover);
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
border-color: var(--spectrum-alias-border-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
padding-top: var(--spacing-xs);
|
padding-top: var(--spacing-xs);
|
||||||
color: var(--spectrum-global-color-red-500);
|
color: var(--spectrum-global-color-red-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pulsing-text {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
PROTECTED_EXTERNAL_COLUMNS,
|
PROTECTED_EXTERNAL_COLUMNS,
|
||||||
canHaveDefaultColumn,
|
canHaveDefaultColumn,
|
||||||
} from "@budibase/shared-core"
|
} from "@budibase/shared-core"
|
||||||
|
import { makePropSafe } from "@budibase/string-templates"
|
||||||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { tables, datasources } from "stores/builder"
|
import { tables, datasources } from "stores/builder"
|
||||||
|
@ -46,6 +47,7 @@
|
||||||
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
||||||
import OptionsEditor from "./OptionsEditor.svelte"
|
import OptionsEditor from "./OptionsEditor.svelte"
|
||||||
import { isEnabled } from "helpers/featureFlags"
|
import { isEnabled } from "helpers/featureFlags"
|
||||||
|
import { getUserBindings } from "dataBinding"
|
||||||
|
|
||||||
const AUTO_TYPE = FieldType.AUTO
|
const AUTO_TYPE = FieldType.AUTO
|
||||||
const FORMULA_TYPE = FieldType.FORMULA
|
const FORMULA_TYPE = FieldType.FORMULA
|
||||||
|
@ -191,6 +193,19 @@
|
||||||
fieldId: makeFieldId(t.type, t.subtype),
|
fieldId: makeFieldId(t.type, t.subtype),
|
||||||
...t,
|
...t,
|
||||||
}))
|
}))
|
||||||
|
$: defaultValueBindings = [
|
||||||
|
{
|
||||||
|
type: "context",
|
||||||
|
runtimeBinding: `${makePropSafe("now")}`,
|
||||||
|
readableBinding: `Date`,
|
||||||
|
category: "Date",
|
||||||
|
icon: "Date",
|
||||||
|
display: {
|
||||||
|
name: "Server date",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...getUserBindings(),
|
||||||
|
]
|
||||||
|
|
||||||
const fieldDefinitions = Object.values(FIELDS).reduce(
|
const fieldDefinitions = Object.values(FIELDS).reduce(
|
||||||
// Storing the fields by complex field id
|
// Storing the fields by complex field id
|
||||||
|
@ -781,9 +796,8 @@
|
||||||
setRequired(false)
|
setRequired(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
bindings={getBindings({ table })}
|
bindings={defaultValueBindings}
|
||||||
allowJS
|
allowJS
|
||||||
context={rowGoldenSample}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -2,7 +2,12 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte"
|
import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte"
|
||||||
|
|
||||||
const { datasource } = getContext("grid")
|
const { datasource, rows } = getContext("grid")
|
||||||
|
|
||||||
|
const onUpdate = async () => {
|
||||||
|
await datasource.actions.refreshDefinition()
|
||||||
|
await rows.actions.refreshData()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CreateEditColumn on:updatecolumns={datasource.actions.refreshDefinition} />
|
<CreateEditColumn on:updatecolumns={onUpdate} />
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
export const buildAIEndpoints = API => ({
|
||||||
|
/**
|
||||||
|
* Generates a cron expression from a prompt
|
||||||
|
*/
|
||||||
|
generateCronExpression: async ({ prompt }) => {
|
||||||
|
return await API.post({
|
||||||
|
url: "/api/ai/cron",
|
||||||
|
body: { prompt },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
|
@ -2,6 +2,7 @@ import { Helpers } from "@budibase/bbui"
|
||||||
import { Header } from "@budibase/shared-core"
|
import { Header } from "@budibase/shared-core"
|
||||||
import { ApiVersion } from "../constants"
|
import { ApiVersion } from "../constants"
|
||||||
import { buildAnalyticsEndpoints } from "./analytics"
|
import { buildAnalyticsEndpoints } from "./analytics"
|
||||||
|
import { buildAIEndpoints } from "./ai"
|
||||||
import { buildAppEndpoints } from "./app"
|
import { buildAppEndpoints } from "./app"
|
||||||
import { buildAttachmentEndpoints } from "./attachments"
|
import { buildAttachmentEndpoints } from "./attachments"
|
||||||
import { buildAuthEndpoints } from "./auth"
|
import { buildAuthEndpoints } from "./auth"
|
||||||
|
@ -269,6 +270,7 @@ export const createAPIClient = config => {
|
||||||
// Attach all endpoints
|
// Attach all endpoints
|
||||||
return {
|
return {
|
||||||
...API,
|
...API,
|
||||||
|
...buildAIEndpoints(API),
|
||||||
...buildAnalyticsEndpoints(API),
|
...buildAnalyticsEndpoints(API),
|
||||||
...buildAppEndpoints(API),
|
...buildAppEndpoints(API),
|
||||||
...buildAttachmentEndpoints(API),
|
...buildAttachmentEndpoints(API),
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit e2fe0f9cc856b4ee1a97df96d623b2d87d4e8733
|
Subproject commit aca9828117bb97f54f40ee359f1a3f6e259174e7
|
|
@ -3,15 +3,9 @@ import {
|
||||||
ViewV2,
|
ViewV2,
|
||||||
SearchRowResponse,
|
SearchRowResponse,
|
||||||
SearchViewRowRequest,
|
SearchViewRowRequest,
|
||||||
SearchFilterKey,
|
|
||||||
LogicalOperator,
|
|
||||||
SearchFilter,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { db, context, features } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import { enrichSearchContext } from "./utils"
|
|
||||||
import { isExternalTableID } from "../../../integrations/utils"
|
|
||||||
|
|
||||||
export async function searchView(
|
export async function searchView(
|
||||||
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
|
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
|
||||||
|
@ -28,70 +22,23 @@ export async function searchView(
|
||||||
|
|
||||||
const { body } = ctx.request
|
const { body } = ctx.request
|
||||||
|
|
||||||
const sqsEnabled = await features.flags.isEnabled("SQS")
|
|
||||||
const supportsLogicalOperators = isExternalTableID(view.tableId) || sqsEnabled
|
|
||||||
|
|
||||||
// Enrich saved query with ephemeral query params.
|
|
||||||
// We prevent searching on any fields that are saved as part of the query, as
|
|
||||||
// that could let users find rows they should not be allowed to access.
|
|
||||||
let query = supportsLogicalOperators
|
|
||||||
? dataFilters.buildQuery(view.query)
|
|
||||||
: dataFilters.buildQueryLegacy(view.query)
|
|
||||||
|
|
||||||
delete query?.onEmptyFilter
|
|
||||||
|
|
||||||
if (body.query) {
|
|
||||||
// Delete extraneous search params that cannot be overridden
|
|
||||||
delete body.query.onEmptyFilter
|
|
||||||
|
|
||||||
if (!supportsLogicalOperators) {
|
|
||||||
// In the unlikely event that a Grouped Filter is in a non-SQS environment
|
|
||||||
// It needs to be ignored entirely
|
|
||||||
let queryFilters: SearchFilter[] = Array.isArray(view.query)
|
|
||||||
? view.query
|
|
||||||
: []
|
|
||||||
|
|
||||||
// Extract existing fields
|
|
||||||
const existingFields =
|
|
||||||
queryFilters
|
|
||||||
?.filter(filter => filter.field)
|
|
||||||
.map(filter => db.removeKeyNumbering(filter.field)) || []
|
|
||||||
|
|
||||||
// Carry over filters for unused fields
|
|
||||||
Object.keys(body.query).forEach(key => {
|
|
||||||
const operator = key as Exclude<SearchFilterKey, LogicalOperator>
|
|
||||||
Object.keys(body.query[operator] || {}).forEach(field => {
|
|
||||||
if (query && !existingFields.includes(db.removeKeyNumbering(field))) {
|
|
||||||
query[operator]![field] = body.query[operator]![field]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const conditions = query ? [query] : []
|
|
||||||
query = {
|
|
||||||
$and: {
|
|
||||||
conditions: [...conditions, body.query],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.ensureSnippetContext(true)
|
await context.ensureSnippetContext(true)
|
||||||
|
|
||||||
const enrichedQuery = await enrichSearchContext(query || {}, {
|
const result = await sdk.rows.search(
|
||||||
user: sdk.users.getUserContextBindings(ctx.user),
|
{
|
||||||
})
|
|
||||||
|
|
||||||
const result = await sdk.rows.search({
|
|
||||||
viewId: view.id,
|
viewId: view.id,
|
||||||
tableId: view.tableId,
|
tableId: view.tableId,
|
||||||
query: enrichedQuery,
|
query: body.query,
|
||||||
...getSortOptions(body, view),
|
...getSortOptions(body, view),
|
||||||
limit: body.limit,
|
limit: body.limit,
|
||||||
bookmark: body.bookmark,
|
bookmark: body.bookmark,
|
||||||
paginate: body.paginate,
|
paginate: body.paginate,
|
||||||
countRows: body.countRows,
|
countRows: body.countRows,
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
user: sdk.users.getUserContextBindings(ctx.user),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
result.rows.forEach(r => (r._viewId = view.id))
|
result.rows.forEach(r => (r._viewId = view.id))
|
||||||
ctx.body = result
|
ctx.body = result
|
||||||
|
|
|
@ -33,6 +33,7 @@ import rowActionRoutes from "./rowAction"
|
||||||
export { default as staticRoutes } from "./static"
|
export { default as staticRoutes } from "./static"
|
||||||
export { default as publicRoutes } from "./public"
|
export { default as publicRoutes } from "./public"
|
||||||
|
|
||||||
|
const aiRoutes = pro.ai
|
||||||
const appBackupRoutes = pro.appBackups
|
const appBackupRoutes = pro.appBackups
|
||||||
const environmentVariableRoutes = pro.environmentVariables
|
const environmentVariableRoutes = pro.environmentVariables
|
||||||
|
|
||||||
|
@ -67,6 +68,7 @@ export const mainRoutes: Router[] = [
|
||||||
debugRoutes,
|
debugRoutes,
|
||||||
environmentVariableRoutes,
|
environmentVariableRoutes,
|
||||||
rowActionRoutes,
|
rowActionRoutes,
|
||||||
|
aiRoutes,
|
||||||
// these need to be handled last as they still use /api/:tableId
|
// these need to be handled last as they still use /api/:tableId
|
||||||
// this could be breaking as koa may recognise other routes as this
|
// this could be breaking as koa may recognise other routes as this
|
||||||
tableRoutes,
|
tableRoutes,
|
||||||
|
|
|
@ -1738,6 +1738,40 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("views filters are respected even if the column is hidden", async () => {
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
one: "foo",
|
||||||
|
two: "bar",
|
||||||
|
})
|
||||||
|
const two = await config.api.row.save(table._id!, {
|
||||||
|
one: "foo2",
|
||||||
|
two: "bar2",
|
||||||
|
})
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
query: [
|
||||||
|
{
|
||||||
|
operator: BasicOperator.EQUAL,
|
||||||
|
field: "two",
|
||||||
|
value: "bar2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
id: { visible: true },
|
||||||
|
one: { visible: false },
|
||||||
|
two: { visible: false },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await config.api.viewV2.search(view.id)
|
||||||
|
expect(response.rows).toHaveLength(1)
|
||||||
|
expect(response.rows).toEqual([
|
||||||
|
expect.objectContaining({ _id: two._id }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it("views without data can be returned", async () => {
|
it("views without data can be returned", async () => {
|
||||||
const response = await config.api.viewV2.search(view.id)
|
const response = await config.api.viewV2.search(view.id)
|
||||||
expect(response.rows).toHaveLength(0)
|
expect(response.rows).toHaveLength(0)
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import {
|
import {
|
||||||
EmptyFilterOption,
|
EmptyFilterOption,
|
||||||
|
LogicalOperator,
|
||||||
Row,
|
Row,
|
||||||
RowSearchParams,
|
RowSearchParams,
|
||||||
|
SearchFilterKey,
|
||||||
|
SearchFilters,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
Table,
|
Table,
|
||||||
|
@ -14,9 +17,10 @@ import { ExportRowsParams, ExportRowsResult } from "./search/types"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
import sdk from "../../index"
|
import sdk from "../../index"
|
||||||
import { searchInputMapping } from "./search/utils"
|
import { searchInputMapping } from "./search/utils"
|
||||||
import { features } from "@budibase/backend-core"
|
import { db, features } from "@budibase/backend-core"
|
||||||
import tracer from "dd-trace"
|
import tracer from "dd-trace"
|
||||||
import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
|
import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
|
||||||
|
import { enrichSearchContext } from "../../../api/controllers/row/utils"
|
||||||
|
|
||||||
export { isValidFilter } from "../../../integrations/utils"
|
export { isValidFilter } from "../../../integrations/utils"
|
||||||
|
|
||||||
|
@ -34,7 +38,8 @@ function pickApi(tableId: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function search(
|
export async function search(
|
||||||
options: RowSearchParams
|
options: RowSearchParams,
|
||||||
|
context?: Record<string, any>
|
||||||
): Promise<SearchResponse<Row>> {
|
): Promise<SearchResponse<Row>> {
|
||||||
return await tracer.trace("search", async span => {
|
return await tracer.trace("search", async span => {
|
||||||
span?.addTags({
|
span?.addTags({
|
||||||
|
@ -51,7 +56,73 @@ export async function search(
|
||||||
countRows: options.countRows,
|
countRows: options.countRows,
|
||||||
})
|
})
|
||||||
|
|
||||||
options.query = dataFilters.cleanupQuery(options.query || {})
|
let source: Table | ViewV2
|
||||||
|
let table: Table
|
||||||
|
if (options.viewId) {
|
||||||
|
source = await sdk.views.get(options.viewId)
|
||||||
|
table = await sdk.views.getTable(source)
|
||||||
|
options = searchInputMapping(table, options)
|
||||||
|
} else if (options.tableId) {
|
||||||
|
source = await sdk.tables.getTable(options.tableId)
|
||||||
|
table = source
|
||||||
|
} else {
|
||||||
|
throw new Error(`Must supply either a view ID or a table ID`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExternalTable = isExternalTableID(table._id!)
|
||||||
|
|
||||||
|
if (options.query) {
|
||||||
|
const visibleFields = (
|
||||||
|
options.fields || Object.keys(table.schema)
|
||||||
|
).filter(field => table.schema[field].visible !== false)
|
||||||
|
|
||||||
|
const queryableFields = await getQueryableFields(table, visibleFields)
|
||||||
|
options.query = removeInvalidFilters(options.query, queryableFields)
|
||||||
|
} else {
|
||||||
|
options.query = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.viewId) {
|
||||||
|
const view = await sdk.views.get(options.viewId)
|
||||||
|
// Enrich saved query with ephemeral query params.
|
||||||
|
// We prevent searching on any fields that are saved as part of the query, as
|
||||||
|
// that could let users find rows they should not be allowed to access.
|
||||||
|
let viewQuery = dataFilters.buildQuery(view.query || [])
|
||||||
|
|
||||||
|
if (!isExternalTable && !(await features.flags.isEnabled("SQS"))) {
|
||||||
|
// Lucene does not accept conditional filters, so we need to keep the old logic
|
||||||
|
const query: SearchFilters = viewQuery
|
||||||
|
|
||||||
|
// Extract existing fields
|
||||||
|
const existingFields =
|
||||||
|
view.query
|
||||||
|
?.filter(filter => filter.field)
|
||||||
|
.map(filter => db.removeKeyNumbering(filter.field)) || []
|
||||||
|
|
||||||
|
// Carry over filters for unused fields
|
||||||
|
Object.keys(options.query || {}).forEach(key => {
|
||||||
|
const operator = key as Exclude<SearchFilterKey, LogicalOperator>
|
||||||
|
Object.keys(options.query[operator] || {}).forEach(field => {
|
||||||
|
if (!existingFields.includes(db.removeKeyNumbering(field))) {
|
||||||
|
query[operator]![field] = options.query[operator]![field]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
options.query = query
|
||||||
|
} else {
|
||||||
|
options.query = {
|
||||||
|
$and: {
|
||||||
|
conditions: [viewQuery, options.query],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context) {
|
||||||
|
options.query = await enrichSearchContext(options.query, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
options.query = dataFilters.cleanupQuery(options.query)
|
||||||
options.query = dataFilters.fixupFilterArrays(options.query)
|
options.query = dataFilters.fixupFilterArrays(options.query)
|
||||||
|
|
||||||
span.addTags({
|
span.addTags({
|
||||||
|
@ -72,30 +143,8 @@ export async function search(
|
||||||
options.sortOrder = options.sortOrder.toLowerCase() as SortOrder
|
options.sortOrder = options.sortOrder.toLowerCase() as SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
let source: Table | ViewV2
|
|
||||||
let table: Table
|
|
||||||
if (options.viewId) {
|
|
||||||
source = await sdk.views.get(options.viewId)
|
|
||||||
table = await sdk.views.getTable(source)
|
|
||||||
options = searchInputMapping(table, options)
|
options = searchInputMapping(table, options)
|
||||||
} else if (options.tableId) {
|
|
||||||
source = await sdk.tables.getTable(options.tableId)
|
|
||||||
table = source
|
|
||||||
options = searchInputMapping(table, options)
|
|
||||||
} else {
|
|
||||||
throw new Error(`Must supply either a view ID or a table ID`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.query) {
|
|
||||||
const visibleFields = (
|
|
||||||
options.fields || Object.keys(table.schema)
|
|
||||||
).filter(field => table.schema[field].visible !== false)
|
|
||||||
|
|
||||||
const queryableFields = await getQueryableFields(table, visibleFields)
|
|
||||||
options.query = removeInvalidFilters(options.query, queryableFields)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isExternalTable = isExternalTableID(table._id!)
|
|
||||||
let result: SearchResponse<Row>
|
let result: SearchResponse<Row>
|
||||||
if (isExternalTable) {
|
if (isExternalTable) {
|
||||||
span?.addTags({ searchType: "external" })
|
span?.addTags({ searchType: "external" })
|
||||||
|
|
|
@ -130,6 +130,26 @@ export function getUserContextBindings(user: ContextUser) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
// Current user context for bindable search
|
// Current user context for bindable search
|
||||||
const { _id, _rev, firstName, lastName, email, status, roleId } = user
|
const {
|
||||||
return { _id, _rev, firstName, lastName, email, status, roleId }
|
_id,
|
||||||
|
_rev,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
status,
|
||||||
|
roleId,
|
||||||
|
globalId,
|
||||||
|
userId,
|
||||||
|
} = user
|
||||||
|
return {
|
||||||
|
_id,
|
||||||
|
_rev,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
status,
|
||||||
|
roleId,
|
||||||
|
globalId,
|
||||||
|
userId,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import cronValidate from "cron-validate"
|
import cronValidate from "cron-validate"
|
||||||
|
import cronParser from "cron-parser"
|
||||||
|
|
||||||
const INPUT_CRON_START = "(Input cron: "
|
const INPUT_CRON_START = "(Input cron: "
|
||||||
const ERROR_SWAPS = {
|
const ERROR_SWAPS = {
|
||||||
|
@ -30,6 +31,19 @@ function improveErrors(errors: string[]): string[] {
|
||||||
return finalErrors
|
return finalErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNextExecutionDates(
|
||||||
|
cronExpression: string,
|
||||||
|
limit: number = 4
|
||||||
|
): string[] {
|
||||||
|
const parsed = cronParser.parseExpression(cronExpression)
|
||||||
|
const nextRuns = []
|
||||||
|
for (let i = 0; i < limit; i++) {
|
||||||
|
nextRuns.push(parsed.next().toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextRuns
|
||||||
|
}
|
||||||
|
|
||||||
export function validate(
|
export function validate(
|
||||||
cronExpression: string
|
cronExpression: string
|
||||||
): { valid: false; err: string[] } | { valid: true } {
|
): { valid: false; err: string[] } | { valid: true } {
|
||||||
|
|
31
yarn.lock
31
yarn.lock
|
@ -817,23 +817,23 @@
|
||||||
tslib "^2.2.0"
|
tslib "^2.2.0"
|
||||||
|
|
||||||
"@azure/msal-browser@^3.11.1":
|
"@azure/msal-browser@^3.11.1":
|
||||||
version "3.23.0"
|
version "3.24.0"
|
||||||
resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.23.0.tgz#446aaf268247e5943f464f007d3aa3a04abfe95b"
|
resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.24.0.tgz#3208047672d0b0c943b0bef5f995d510d6582ae4"
|
||||||
integrity sha512-+QgdMvaeEpdtgRTD7AHHq9aw8uga7mXVHV1KshO1RQ2uI5B55xJ4aEpGlg/ga3H+0arEVcRfT4ZVmX7QLXiCVw==
|
integrity sha512-JGNV9hTYAa7lsum9IMIibn2kKczAojNihGo1hi7pG0kNrcKej530Fl6jxwM05A44/6I079CSn6WxYxbVhKUmWg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@azure/msal-common" "14.14.2"
|
"@azure/msal-common" "14.15.0"
|
||||||
|
|
||||||
"@azure/msal-common@14.14.2":
|
"@azure/msal-common@14.15.0":
|
||||||
version "14.14.2"
|
version "14.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.14.2.tgz#583b4ac9c089953718d7a5e2f3b8df2d4dbb17f4"
|
resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.15.0.tgz#0e27ac0bb88fe100f4f8d1605b64d5c268636a55"
|
||||||
integrity sha512-XV0P5kSNwDwCA/SjIxTe9mEAsKB0NqGNSuaVrkCCE2lAyBr/D6YtD80Vkdp4tjWnPFwjzkwldjr1xU/facOJog==
|
integrity sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ==
|
||||||
|
|
||||||
"@azure/msal-node@^2.9.2":
|
"@azure/msal-node@^2.9.2":
|
||||||
version "2.13.1"
|
version "2.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.13.1.tgz#f144371275b7c3cbe564762b84772a9732457a47"
|
resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.14.0.tgz#7881895d41b03d8b9b38a29550ba3bbb15f73b3c"
|
||||||
integrity sha512-sijfzPNorKt6+9g1/miHwhj6Iapff4mPQx1azmmZExgzUROqWTM1o3ACyxDja0g47VpowFy/sxTM/WsuCyXTiw==
|
integrity sha512-rrfzIpG3Q1rHjVYZmHAEDidWAZZ2cgkxlIcMQ8dHebRISaZ2KCV33Q8Vs+uaV6lxweROabNxKFlR2lIKagZqYg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@azure/msal-common" "14.14.2"
|
"@azure/msal-common" "14.15.0"
|
||||||
jsonwebtoken "^9.0.0"
|
jsonwebtoken "^9.0.0"
|
||||||
uuid "^8.3.0"
|
uuid "^8.3.0"
|
||||||
|
|
||||||
|
@ -9167,6 +9167,13 @@ cron-parser@^4.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
luxon "^3.2.1"
|
luxon "^3.2.1"
|
||||||
|
|
||||||
|
cron-parser@^4.9.0:
|
||||||
|
version "4.9.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5"
|
||||||
|
integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==
|
||||||
|
dependencies:
|
||||||
|
luxon "^3.2.1"
|
||||||
|
|
||||||
cron-validate@1.4.5:
|
cron-validate@1.4.5:
|
||||||
version "1.4.5"
|
version "1.4.5"
|
||||||
resolved "https://registry.yarnpkg.com/cron-validate/-/cron-validate-1.4.5.tgz#eceb221f7558e6302e5f84c7b3a454fdf4d064c3"
|
resolved "https://registry.yarnpkg.com/cron-validate/-/cron-validate-1.4.5.tgz#eceb221f7558e6302e5f84c7b3a454fdf4d064c3"
|
||||||
|
|
Loading…
Reference in New Issue