diff --git a/.github/workflows/deploy-featurebranch.yml b/.github/workflows/deploy-featurebranch.yml index d86d301507..0e19f0649f 100644 --- a/.github/workflows/deploy-featurebranch.yml +++ b/.github/workflows/deploy-featurebranch.yml @@ -3,26 +3,50 @@ name: deploy-featurebranch on: pull_request: types: [ - labeled, - # default types below (https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request) - opened, - synchronize, - reopened, - ] + labeled, + # default types below (https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request) + opened, + synchronize, + reopened, + ] jobs: release: if: | (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase') && - contains(github.event.pull_request.labels.*.name, 'feature-branch') + ( + contains(github.event.pull_request.labels.*.name, 'feature-branch') || + contains(github.event.pull_request.labels.*.name, 'feature-branch-pro') || + contains(github.event.pull_request.labels.*.name, 'feature-branch-team') || + contains(github.event.pull_request.labels.*.name, 'feature-branch-business') || + contains(github.event.pull_request.labels.*.name, 'feature-branch-enterprise') + ) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + + - name: Set PAYLOAD_LICENSE_TYPE + id: set_license_type + run: | + if [[ "${{ contains(github.event.pull_request.labels.*.name, 'feature-branch') }}" == "true" ]]; then + echo "PAYLOAD_LICENSE_TYPE=free" >> $GITHUB_ENV + elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'feature-branch-pro') }}" == "true" ]]; then + echo "PAYLOAD_LICENSE_TYPE=pro" >> $GITHUB_ENV + elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'feature-branch-team') }}" == "true" ]]; then + echo "PAYLOAD_LICENSE_TYPE=team" >> $GITHUB_ENV + elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'feature-branch-business') }}" == "true" ]]; then + echo "PAYLOAD_LICENSE_TYPE=business" >> $GITHUB_ENV + elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'feature-branch-enterprise') }}" == "true" ]]; then + echo "PAYLOAD_LICENSE_TYPE=enterprise" >> $GITHUB_ENV + else + echo "PAYLOAD_LICENSE_TYPE=free" >> $GITHUB_ENV + fi + - uses: passeidireto/trigger-external-workflow-action@main env: PAYLOAD_BRANCH: ${{ github.head_ref }} PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }} - PAYLOAD_LICENSE_TYPE: "free" + PAYLOAD_LICENSE_TYPE: ${{ env.PAYLOAD_LICENSE_TYPE }} with: repository: budibase/budibase-deploys event: featurebranch-qa-deploy diff --git a/lerna.json b/lerna.json index 4d36affd04..13530e9aee 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "2.32.15", + "version": "2.32.17", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/backend-core/src/constants/misc.ts b/packages/backend-core/src/constants/misc.ts index aee099e10a..e2fd975e40 100644 --- a/packages/backend-core/src/constants/misc.ts +++ b/packages/backend-core/src/constants/misc.ts @@ -28,6 +28,7 @@ export enum Config { OIDC = "oidc", OIDC_LOGOS = "logos_oidc", SCIM = "scim", + AI = "AI", } export const MIN_VALID_DATE = new Date(-2147483647000) diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 2b37526dde..8ca20bf8e1 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -211,6 +211,17 @@ export class DatabaseImpl implements Database { }) } + async tryGet(id?: string): Promise { + try { + return await this.get(id) + } catch (err: any) { + if (err.statusCode === 404) { + return undefined + } + throw err + } + } + async getMultiple( ids: string[], opts?: { allowMissing?: boolean; excludeDocs?: boolean } diff --git a/packages/backend-core/src/db/instrumentation.ts b/packages/backend-core/src/db/instrumentation.ts index 7026224564..e08bfc0362 100644 --- a/packages/backend-core/src/db/instrumentation.ts +++ b/packages/backend-core/src/db/instrumentation.ts @@ -42,6 +42,13 @@ export class DDInstrumentedDatabase implements Database { }) } + tryGet(id?: string | undefined): Promise { + return tracer.trace("db.tryGet", span => { + span?.addTags({ db_name: this.name, doc_id: id }) + return this.db.tryGet(id) + }) + } + getMultiple( ids: string[], opts?: { allowMissing?: boolean | undefined } | undefined diff --git a/packages/backend-core/src/features/features.ts b/packages/backend-core/src/features/features.ts index efe6495cb5..20b207bb02 100644 --- a/packages/backend-core/src/features/features.ts +++ b/packages/backend-core/src/features/features.ts @@ -272,6 +272,7 @@ export const flags = new FlagSet({ SQS: Flag.boolean(env.isDev()), [FeatureFlag.AI_CUSTOM_CONFIGS]: Flag.boolean(env.isDev()), [FeatureFlag.ENRICHED_RELATIONSHIPS]: Flag.boolean(env.isDev()), + [FeatureFlag.TABLES_DEFAULT_ADMIN]: Flag.boolean(env.isDev()), }) type UnwrapPromise = T extends Promise ? U : T diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index 1fd89b3ff0..108bc0414c 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -457,7 +457,9 @@ export function getExternalRoleID(roleId: string, version?: string) { roleId.startsWith(DocumentType.ROLE) && (isBuiltin(roleId) || version === RoleIDVersion.NAME) ) { - return roleId.split(`${DocumentType.ROLE}${SEPARATOR}`)[1] + const parts = roleId.split(SEPARATOR) + parts.shift() + return parts.join(SEPARATOR) } return roleId } diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index f122ad1c41..95376945a0 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -273,6 +273,7 @@ class InternalBuilder { const col = parts.pop()! const schema = this.table.schema[col] let identifier = this.quotedIdentifier(field) + if ( schema.type === FieldType.STRING || schema.type === FieldType.LONGFORM || @@ -957,6 +958,13 @@ class InternalBuilder { return query } + isAggregateField(field: string): boolean { + const found = this.query.resource?.aggregations?.find( + aggregation => aggregation.name === field + ) + return !!found + } + addSorting(query: Knex.QueryBuilder): Knex.QueryBuilder { let { sort, resource } = this.query const primaryKey = this.table.primary @@ -979,13 +987,17 @@ class InternalBuilder { nulls = value.direction === SortOrder.ASCENDING ? "first" : "last" } - let composite = `${aliased}.${key}` - if (this.client === SqlClient.ORACLE) { - query = query.orderByRaw( - `${this.convertClobs(composite)} ${direction} nulls ${nulls}` - ) + if (this.isAggregateField(key)) { + query = query.orderBy(key, direction, nulls) } else { - query = query.orderBy(composite, direction, nulls) + let composite = `${aliased}.${key}` + if (this.client === SqlClient.ORACLE) { + query = query.orderByRaw( + `${this.convertClobs(composite)} ${direction} nulls ${nulls}` + ) + } else { + query = query.orderBy(composite, direction, nulls) + } } } } @@ -1276,7 +1288,8 @@ class InternalBuilder { schema.constraints?.presence === true || schema.type === FieldType.FORMULA || schema.type === FieldType.AUTO || - schema.type === FieldType.LINK + schema.type === FieldType.LINK || + schema.type === FieldType.AI ) { continue } diff --git a/packages/backend-core/src/sql/sqlTable.ts b/packages/backend-core/src/sql/sqlTable.ts index f5b02cc4e4..84f4e290aa 100644 --- a/packages/backend-core/src/sql/sqlTable.ts +++ b/packages/backend-core/src/sql/sqlTable.ts @@ -17,7 +17,7 @@ import SchemaBuilder = Knex.SchemaBuilder import CreateTableBuilder = Knex.CreateTableBuilder function isIgnoredType(type: FieldType) { - const ignored = [FieldType.LINK, FieldType.FORMULA] + const ignored = [FieldType.LINK, FieldType.FORMULA, FieldType.AI] return ignored.indexOf(type) !== -1 } @@ -144,6 +144,9 @@ function generateSchema( case FieldType.FORMULA: // This is allowed, but nothing to do on the external datasource break + case FieldType.AI: + // This is allowed, but nothing to do on the external datasource + break case FieldType.ATTACHMENTS: case FieldType.ATTACHMENT_SINGLE: case FieldType.SIGNATURE_SINGLE: diff --git a/packages/backend-core/tests/core/utilities/mocks/licenses.ts b/packages/backend-core/tests/core/utilities/mocks/licenses.ts index bc9a3b635c..5ba6fb36a1 100644 --- a/packages/backend-core/tests/core/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/core/utilities/mocks/licenses.ts @@ -102,6 +102,14 @@ export const useAppBuilders = () => { return useFeature(Feature.APP_BUILDERS) } +export const useBudibaseAI = () => { + return useFeature(Feature.BUDIBASE_AI) +} + +export const useAICustomConfigs = () => { + return useFeature(Feature.AI_CUSTOM_CONFIGS) +} + // QUOTAS export const setAutomationLogsQuota = (value: number) => { diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/ColumnsSettingContent.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/ColumnsSettingContent.svelte index a61fcf8346..9e9449976b 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/grid/ColumnsSettingContent.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/ColumnsSettingContent.svelte @@ -124,7 +124,7 @@ subtype: column.subtype, visible: column.visible, readonly: column.readonly, - constraints: column.constraints, // This is needed to properly display "users" column + icon: column.icon, }, } }) diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 19cc4ec1c0..143e292c55 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -26,6 +26,7 @@ import { createEventDispatcher, getContext, onMount } from "svelte" import { cloneDeep } from "lodash/fp" import { tables, datasources } from "stores/builder" + import { licensing } from "stores/portal" import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" import { FIELDS, @@ -35,6 +36,7 @@ } from "constants/backend" import { getAutoColumnInformation, buildAutoColumn } from "helpers/utils" import ConfirmDialog from "components/common/ConfirmDialog.svelte" + import AIFieldConfiguration from "components/common/AIFieldConfiguration.svelte" import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte" import { getBindings } from "components/backend/DataTable/formula" import JSONSchemaModal from "./JSONSchemaModal.svelte" @@ -50,18 +52,13 @@ import { isEnabled } from "helpers/featureFlags" import { getUserBindings } from "dataBinding" - const AUTO_TYPE = FieldType.AUTO - const FORMULA_TYPE = FieldType.FORMULA - const LINK_TYPE = FieldType.LINK - const STRING_TYPE = FieldType.STRING - const NUMBER_TYPE = FieldType.NUMBER - const JSON_TYPE = FieldType.JSON - const DATE_TYPE = FieldType.DATETIME + export let field const dispatch = createEventDispatcher() const { dispatch: gridDispatch, rows } = getContext("grid") - - export let field + const SafeID = `${makePropSafe("user")}.${makePropSafe("_id")}` + const SingleUserDefault = `{{ ${SafeID} }}` + const MultiUserDefault = `{{ js "${btoa(`return [$("${SafeID}")]`)}" }}` let mounted = false let originalName @@ -104,13 +101,15 @@ let optionsValid = true $: rowGoldenSample = RowUtils.generateGoldenSample($rows) + $: aiEnabled = + $licensing.customAIConfigsEnabled || $licensing.budibaseAIEnabled $: if (primaryDisplay) { editableColumn.constraints.presence = { allowEmpty: false } } $: { // this parses any changes the user has made when creating a new internal relationship // into what we expect the schema to look like - if (editableColumn.type === LINK_TYPE) { + if (editableColumn.type === FieldType.LINK) { relationshipTableIdPrimary = table._id if (relationshipPart1 === PrettyRelationshipDefinitions.ONE) { relationshipOpts2 = relationshipOpts2.filter( @@ -147,7 +146,7 @@ UNEDITABLE_USER_FIELDS.includes(editableColumn.name) $: invalid = !editableColumn?.name || - (editableColumn?.type === LINK_TYPE && !editableColumn?.tableId) || + (editableColumn?.type === FieldType.LINK && !editableColumn?.tableId) || Object.keys(errors).length !== 0 || !optionsValid $: errors = checkErrors(editableColumn) @@ -173,9 +172,9 @@ $: defaultValuesEnabled = isEnabled("DEFAULT_VALUES") $: canHaveDefault = !required && canHaveDefaultColumn(editableColumn.type) $: canBeRequired = - editableColumn?.type !== LINK_TYPE && + editableColumn?.type !== FieldType.LINK && !uneditable && - editableColumn?.type !== AUTO_TYPE && + editableColumn?.type !== FieldType.AUTO && !editableColumn.autocolumn $: hasDefault = editableColumn?.default != null && editableColumn?.default !== "" @@ -224,7 +223,7 @@ function makeFieldId(type, subtype, autocolumn) { // don't make field IDs for auto types - if (type === AUTO_TYPE || autocolumn) { + if (type === FieldType.AUTO || autocolumn) { return type.toUpperCase() } else if ( type === FieldType.BB_REFERENCE || @@ -249,7 +248,7 @@ // Here we are setting the relationship values based on the editableColumn // This part of the code is used when viewing an existing field hence the check // for the tableId - if (editableColumn.type === LINK_TYPE && editableColumn.tableId) { + if (editableColumn.type === FieldType.LINK && editableColumn.tableId) { relationshipTableIdPrimary = table._id relationshipTableIdSecondary = editableColumn.tableId if (editableColumn.relationshipType in relationshipMap) { @@ -290,14 +289,14 @@ delete saveColumn.fieldId - if (saveColumn.type === AUTO_TYPE) { + if (saveColumn.type === FieldType.AUTO) { saveColumn = buildAutoColumn( $tables.selected.name, saveColumn.name, saveColumn.subtype ) } - if (saveColumn.type !== LINK_TYPE) { + if (saveColumn.type !== FieldType.LINK) { delete saveColumn.fieldName } @@ -384,9 +383,9 @@ editableColumn.subtype = definition.subtype // Default relationships many to many - if (editableColumn.type === LINK_TYPE) { + if (editableColumn.type === FieldType.LINK) { editableColumn.relationshipType = RelationshipType.MANY_TO_MANY - } else if (editableColumn.type === FORMULA_TYPE) { + } else if (editableColumn.type === FieldType.FORMULA) { editableColumn.formulaType = "dynamic" } } @@ -452,6 +451,7 @@ FIELDS.BOOLEAN, FIELDS.DATETIME, FIELDS.LINK, + ...(aiEnabled ? [FIELDS.AI] : []), FIELDS.LONGFORM, FIELDS.USER, FIELDS.USERS, @@ -505,17 +505,23 @@ fieldToCheck.constraints = {} } // some string types may have been built by server, may not always have constraints - if (fieldToCheck.type === STRING_TYPE && !fieldToCheck.constraints.length) { + if ( + fieldToCheck.type === FieldType.STRING && + !fieldToCheck.constraints.length + ) { fieldToCheck.constraints.length = {} } // some number types made server-side will be missing constraints if ( - fieldToCheck.type === NUMBER_TYPE && + fieldToCheck.type === FieldType.NUMBER && !fieldToCheck.constraints.numericality ) { fieldToCheck.constraints.numericality = {} } - if (fieldToCheck.type === DATE_TYPE && !fieldToCheck.constraints.datetime) { + if ( + fieldToCheck.type === FieldType.DATETIME && + !fieldToCheck.constraints.datetime + ) { fieldToCheck.constraints.datetime = {} } } @@ -590,13 +596,13 @@ on:input={e => { if ( !uneditable && - !(linkEditDisabled && editableColumn.type === LINK_TYPE) + !(linkEditDisabled && editableColumn.type === FieldType.LINK) ) { editableColumn.name = e.target.value } }} disabled={uneditable || - (linkEditDisabled && editableColumn.type === LINK_TYPE)} + (linkEditDisabled && editableColumn.type === FieldType.LINK)} error={errors?.name} /> {/if} @@ -610,7 +616,7 @@ getOptionValue={field => field.fieldId} getOptionIcon={field => field.icon} isOptionEnabled={option => { - if (option.type === AUTO_TYPE) { + if (option.type === FieldType.AUTO) { return availableAutoColumnKeys?.length > 0 } return true @@ -653,7 +659,7 @@ bind:optionColors={editableColumn.optionColors} bind:valid={optionsValid} /> - {:else if editableColumn.type === DATE_TYPE && !editableColumn.autocolumn} + {:else if editableColumn.type === FieldType.DATETIME && !editableColumn.autocolumn}
@@ -740,7 +746,7 @@ {tableOptions} {errors} /> - {:else if editableColumn.type === FORMULA_TYPE} + {:else if editableColumn.type === FieldType.FORMULA} {#if !externalTable}
@@ -783,12 +789,19 @@ />
- {:else if editableColumn.type === JSON_TYPE} + {:else if editableColumn.type === FieldType.AI} + + {:else if editableColumn.type === FieldType.JSON} {/if} - {#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn} + {#if editableColumn.type === FieldType.AUTO || editableColumn.autocolumn} +{#if aiField.operation} + {#each Object.keys(OperationField) as key} + {#if OperationField[key] === OperationFieldTypes.BINDABLE_TEXT} + (aiField[key] = e.detail)} + value={aiField[key]} + {bindings} + allowJS + {context} + /> + {:else if OperationField[key] === OperationFieldTypes.MULTI_COLUMN} + + {:else if OperationField[key] === OperationFieldTypes.COLUMN} + + import { onMount } from "svelte" + import { clickOutside } from "@budibase/bbui" + import GridPopover from "../overlays/GridPopover.svelte" + + export let value + export let focused = false + export let api + + let textarea + let isOpen = false + let anchor + + $: { + if (!focused) { + isOpen = false + } + } + + const onKeyDown = () => { + return isOpen + } + + const open = async () => { + isOpen = true + } + + const close = () => { + textarea?.blur() + isOpen = false + } + + onMount(() => { + api = { + focus: () => open(), + blur: () => close(), + isActive: () => isOpen, + onKeyDown, + } + }) + + + + +
+
+ {value || ""} +
+
+ +{#if isOpen} + +