Merge pull request #13543 from Budibase/budi-8123/single-user-column-type
Single user column type
This commit is contained in:
commit
849cd9599f
|
@ -53,7 +53,8 @@
|
|||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"local-rules/no-budibase-imports": "error"
|
||||
"no-redeclare": "off",
|
||||
"@typescript-eslint/no-redeclare": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -69,7 +69,7 @@ async function populateUsersFromDB(
|
|||
export async function getUser(
|
||||
userId: string,
|
||||
tenantId?: string,
|
||||
populateUser?: any
|
||||
populateUser?: (userId: string, tenantId: string) => Promise<User>
|
||||
) {
|
||||
if (!populateUser) {
|
||||
populateUser = populateFromDB
|
||||
|
@ -83,7 +83,7 @@ export async function getUser(
|
|||
}
|
||||
const client = await redis.getUserClient()
|
||||
// try cache
|
||||
let user = await client.get(userId)
|
||||
let user: User = await client.get(userId)
|
||||
if (!user) {
|
||||
user = await populateUser(userId, tenantId)
|
||||
await client.store(userId, user, EXPIRY_SECONDS)
|
||||
|
|
|
@ -13,7 +13,7 @@ import { getGlobalDB, doInTenant } from "../context"
|
|||
import { decrypt } from "../security/encryption"
|
||||
import * as identity from "../context/identity"
|
||||
import env from "../environment"
|
||||
import { Ctx, EndpointMatcher, SessionCookie } from "@budibase/types"
|
||||
import { Ctx, EndpointMatcher, SessionCookie, User } from "@budibase/types"
|
||||
import { InvalidAPIKeyError, ErrorCode } from "../errors"
|
||||
import tracer from "dd-trace"
|
||||
|
||||
|
@ -41,7 +41,10 @@ function finalise(ctx: any, opts: FinaliseOpts = {}) {
|
|||
ctx.version = opts.version
|
||||
}
|
||||
|
||||
async function checkApiKey(apiKey: string, populateUser?: Function) {
|
||||
async function checkApiKey(
|
||||
apiKey: string,
|
||||
populateUser?: (userId: string, tenantId: string) => Promise<User>
|
||||
) {
|
||||
// check both the primary and the fallback internal api keys
|
||||
// this allows for rotation
|
||||
if (isValidInternalAPIKey(apiKey)) {
|
||||
|
@ -128,6 +131,7 @@ export default function (
|
|||
} else {
|
||||
user = await getUser(userId, session.tenantId)
|
||||
}
|
||||
// @ts-ignore
|
||||
user.csrfToken = session.csrfToken
|
||||
|
||||
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
||||
|
@ -167,19 +171,25 @@ export default function (
|
|||
authenticated = false
|
||||
}
|
||||
|
||||
if (user) {
|
||||
const isUser = (
|
||||
user: any
|
||||
): user is User & { budibaseAccess?: string } => {
|
||||
return user && user.email
|
||||
}
|
||||
|
||||
if (isUser(user)) {
|
||||
tracer.setUser({
|
||||
id: user?._id,
|
||||
tenantId: user?.tenantId,
|
||||
budibaseAccess: user?.budibaseAccess,
|
||||
status: user?.status,
|
||||
id: user._id!,
|
||||
tenantId: user.tenantId,
|
||||
budibaseAccess: user.budibaseAccess,
|
||||
status: user.status,
|
||||
})
|
||||
}
|
||||
|
||||
// isAuthenticated is a function, so use a variable to be able to check authed state
|
||||
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
|
||||
|
||||
if (user && user.email) {
|
||||
if (isUser(user)) {
|
||||
return identity.doInUserContext(user, ctx, next)
|
||||
} else {
|
||||
return next()
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||
import { onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { FIELDS } from "constants/backend"
|
||||
|
||||
export let block
|
||||
export let testData
|
||||
|
@ -228,6 +229,10 @@
|
|||
categoryName,
|
||||
bindingName
|
||||
) => {
|
||||
const field = Object.values(FIELDS).find(
|
||||
field => field.type === value.type && field.subtype === value.subtype
|
||||
)
|
||||
|
||||
return {
|
||||
readableBinding: bindingName
|
||||
? `${bindingName}.${name}`
|
||||
|
@ -238,7 +243,7 @@
|
|||
icon,
|
||||
category: categoryName,
|
||||
display: {
|
||||
type: value.type,
|
||||
type: field?.name || value.type,
|
||||
name,
|
||||
rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
|
||||
},
|
||||
|
@ -282,6 +287,7 @@
|
|||
for (const key in table?.schema) {
|
||||
schema[key] = {
|
||||
type: table.schema[key].type,
|
||||
subtype: table.schema[key].subtype,
|
||||
}
|
||||
}
|
||||
// remove the original binding
|
||||
|
|
|
@ -55,7 +55,7 @@ export function getBindings({
|
|||
)
|
||||
}
|
||||
const field = Object.values(FIELDS).find(
|
||||
field => field.type === schema.type
|
||||
field => field.type === schema.type && field.subtype === schema.subtype
|
||||
)
|
||||
|
||||
const label = path == null ? column : `${path}.0.${column}`
|
||||
|
|
|
@ -29,11 +29,7 @@
|
|||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||
import { getBindings } from "components/backend/DataTable/formula"
|
||||
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
||||
import {
|
||||
FieldType,
|
||||
BBReferenceFieldSubType,
|
||||
SourceName,
|
||||
} from "@budibase/types"
|
||||
import { FieldType, SourceName } from "@budibase/types"
|
||||
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||
import { RowUtils } from "@budibase/frontend-core"
|
||||
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
||||
|
@ -67,7 +63,6 @@
|
|||
let savingColumn
|
||||
let deleteColName
|
||||
let jsonSchemaModal
|
||||
let allowedTypes = []
|
||||
let editableColumn = {
|
||||
type: FIELDS.STRING.type,
|
||||
constraints: FIELDS.STRING.constraints,
|
||||
|
@ -175,6 +170,11 @@
|
|||
SWITCHABLE_TYPES[field.type] &&
|
||||
!editableColumn?.autocolumn)
|
||||
|
||||
$: allowedTypes = getAllowedTypes(datasource).map(t => ({
|
||||
fieldId: makeFieldId(t.type, t.subtype),
|
||||
...t,
|
||||
}))
|
||||
|
||||
const fieldDefinitions = Object.values(FIELDS).reduce(
|
||||
// Storing the fields by complex field id
|
||||
(acc, field) => ({
|
||||
|
@ -188,7 +188,10 @@
|
|||
// don't make field IDs for auto types
|
||||
if (type === AUTO_TYPE || autocolumn) {
|
||||
return type.toUpperCase()
|
||||
} else if (type === FieldType.BB_REFERENCE) {
|
||||
} else if (
|
||||
type === FieldType.BB_REFERENCE ||
|
||||
type === FieldType.BB_REFERENCE_SINGLE
|
||||
) {
|
||||
return `${type}${subtype || ""}`.toUpperCase()
|
||||
} else {
|
||||
return type.toUpperCase()
|
||||
|
@ -226,11 +229,6 @@
|
|||
editableColumn.subtype,
|
||||
editableColumn.autocolumn
|
||||
)
|
||||
|
||||
allowedTypes = getAllowedTypes().map(t => ({
|
||||
fieldId: makeFieldId(t.type, t.subtype),
|
||||
...t,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,13 +262,6 @@
|
|||
if (saveColumn.type !== LINK_TYPE) {
|
||||
delete saveColumn.fieldName
|
||||
}
|
||||
if (isUsersColumn(saveColumn)) {
|
||||
if (saveColumn.subtype === BBReferenceFieldSubType.USER) {
|
||||
saveColumn.relationshipType = RelationshipType.ONE_TO_MANY
|
||||
} else if (saveColumn.subtype === BBReferenceFieldSubType.USERS) {
|
||||
saveColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await tables.saveField({
|
||||
|
@ -363,7 +354,7 @@
|
|||
deleteColName = ""
|
||||
}
|
||||
|
||||
function getAllowedTypes() {
|
||||
function getAllowedTypes(datasource) {
|
||||
if (originalName) {
|
||||
const possibleTypes = SWITCHABLE_TYPES[field.type] || [
|
||||
editableColumn.type,
|
||||
|
@ -373,10 +364,6 @@
|
|||
.map(([_, fieldDefinition]) => fieldDefinition)
|
||||
}
|
||||
|
||||
const isUsers =
|
||||
editableColumn.type === FieldType.BB_REFERENCE &&
|
||||
editableColumn.subtype === BBReferenceFieldSubType.USERS
|
||||
|
||||
if (!externalTable) {
|
||||
return [
|
||||
FIELDS.STRING,
|
||||
|
@ -393,7 +380,8 @@
|
|||
FIELDS.LINK,
|
||||
FIELDS.FORMULA,
|
||||
FIELDS.JSON,
|
||||
isUsers ? FIELDS.USERS : FIELDS.USER,
|
||||
FIELDS.USER,
|
||||
FIELDS.USERS,
|
||||
FIELDS.AUTO,
|
||||
]
|
||||
} else {
|
||||
|
@ -407,8 +395,12 @@
|
|||
FIELDS.BOOLEAN,
|
||||
FIELDS.FORMULA,
|
||||
FIELDS.BIGINT,
|
||||
isUsers ? FIELDS.USERS : FIELDS.USER,
|
||||
FIELDS.USER,
|
||||
]
|
||||
|
||||
if (datasource && datasource.source !== SourceName.GOOGLE_SHEETS) {
|
||||
fields.push(FIELDS.USERS)
|
||||
}
|
||||
// no-sql or a spreadsheet
|
||||
if (!externalTable || table.sql) {
|
||||
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY]
|
||||
|
@ -482,15 +474,6 @@
|
|||
return newError
|
||||
}
|
||||
|
||||
function isUsersColumn(column) {
|
||||
return (
|
||||
column.type === FieldType.BB_REFERENCE &&
|
||||
[BBReferenceFieldSubType.USER, BBReferenceFieldSubType.USERS].includes(
|
||||
column.subtype
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mounted = true
|
||||
})
|
||||
|
@ -689,22 +672,6 @@
|
|||
<Button primary text on:click={openJsonSchemaEditor}
|
||||
>Open schema editor</Button
|
||||
>
|
||||
{:else if isUsersColumn(editableColumn) && datasource?.source !== SourceName.GOOGLE_SHEETS}
|
||||
<Toggle
|
||||
value={editableColumn.subtype === BBReferenceFieldSubType.USERS}
|
||||
on:change={e =>
|
||||
handleTypeChange(
|
||||
makeFieldId(
|
||||
FieldType.BB_REFERENCE,
|
||||
e.detail
|
||||
? BBReferenceFieldSubType.USERS
|
||||
: BBReferenceFieldSubType.USER
|
||||
)
|
||||
)}
|
||||
disabled={!isCreating}
|
||||
thin
|
||||
text="Allow multiple users"
|
||||
/>
|
||||
{/if}
|
||||
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
|
||||
<Select
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
onMount(() => subscribe("edit-column", editColumn))
|
||||
</script>
|
||||
|
||||
<CreateEditColumn
|
||||
field={editableColumn}
|
||||
on:updatecolumns={rows.actions.refreshData}
|
||||
/>
|
||||
{#if editableColumn}
|
||||
<CreateEditColumn
|
||||
field={editableColumn}
|
||||
on:updatecolumns={rows.actions.refreshData}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -66,6 +66,10 @@
|
|||
label: "Users",
|
||||
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USERS}`,
|
||||
},
|
||||
{
|
||||
label: "User",
|
||||
value: `${FieldType.BB_REFERENCE_SINGLE}${BBReferenceFieldSubType.USER}`,
|
||||
},
|
||||
]
|
||||
|
||||
$: {
|
||||
|
|
|
@ -47,4 +47,5 @@ export const FieldTypeToComponentMap = {
|
|||
[FieldType.JSON]: "jsonfield",
|
||||
[FieldType.BARCODEQR]: "codescanner",
|
||||
[FieldType.BB_REFERENCE]: "bbreferencefield",
|
||||
[FieldType.BB_REFERENCE_SINGLE]: "bbreferencesinglefield",
|
||||
}
|
||||
|
|
|
@ -158,16 +158,27 @@ export const FIELDS = {
|
|||
},
|
||||
},
|
||||
USER: {
|
||||
name: "User",
|
||||
type: FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
icon: TypeIconMap[FieldType.BB_REFERENCE_SINGLE][
|
||||
BBReferenceFieldSubType.USER
|
||||
],
|
||||
},
|
||||
// Used for display of editing existing columns
|
||||
DEPRECATED_USER: {
|
||||
name: "User",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
icon: TypeIconMap[FieldType.USER],
|
||||
icon: TypeIconMap[FieldType.BB_REFERENCE_SINGLE][
|
||||
BBReferenceFieldSubType.USER
|
||||
],
|
||||
},
|
||||
USERS: {
|
||||
name: "Users",
|
||||
name: "User List",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
icon: TypeIconMap[FieldType.USERS],
|
||||
icon: TypeIconMap[FieldType.BB_REFERENCE][BBReferenceFieldSubType.USERS],
|
||||
constraints: {
|
||||
type: "array",
|
||||
},
|
||||
|
|
|
@ -29,6 +29,7 @@ import { JSONUtils, Constants } from "@budibase/frontend-core"
|
|||
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
|
||||
import { environment, licensing } from "stores/portal"
|
||||
import { convertOldFieldFormat } from "components/design/settings/controls/FieldConfiguration/utils"
|
||||
import { FIELDS } from "constants/backend"
|
||||
|
||||
const { ContextScopes } = Constants
|
||||
|
||||
|
@ -1019,6 +1020,12 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
|||
// are objects
|
||||
let fixedSchema = {}
|
||||
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
|
||||
const field = Object.values(FIELDS).find(
|
||||
field =>
|
||||
field.type === fieldSchema.type &&
|
||||
field.subtype === fieldSchema.subtype
|
||||
)
|
||||
|
||||
if (typeof fieldSchema === "string") {
|
||||
fixedSchema[fieldName] = {
|
||||
type: fieldSchema,
|
||||
|
@ -1027,6 +1034,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
|||
} else {
|
||||
fixedSchema[fieldName] = {
|
||||
...fieldSchema,
|
||||
type: field?.name || fieldSchema.name,
|
||||
name: fieldName,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
"multifieldselect",
|
||||
"s3upload",
|
||||
"codescanner",
|
||||
"bbreferencesinglefield",
|
||||
"bbreferencefield"
|
||||
]
|
||||
},
|
||||
|
|
|
@ -4220,7 +4220,7 @@
|
|||
]
|
||||
},
|
||||
"attachmentfield": {
|
||||
"name": "Attachment list",
|
||||
"name": "Attachment List",
|
||||
"icon": "Attach",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
|
@ -7025,8 +7025,8 @@
|
|||
},
|
||||
"bbreferencefield": {
|
||||
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
|
||||
"name": "User Field",
|
||||
"icon": "User",
|
||||
"name": "User List Field",
|
||||
"icon": "UserGroup",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
|
@ -7130,5 +7130,113 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"bbreferencesinglefield": {
|
||||
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
|
||||
"name": "User Field",
|
||||
"icon": "User",
|
||||
"styles": ["size"],
|
||||
"requiredAncestors": ["form"],
|
||||
"editable": true,
|
||||
"size": {
|
||||
"width": 400,
|
||||
"height": 50
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/bb_reference_single",
|
||||
"label": "Field",
|
||||
"key": "field",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Label",
|
||||
"key": "label"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Placeholder",
|
||||
"key": "placeholder"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Default value",
|
||||
"key": "defaultValue"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Help text",
|
||||
"key": "helpText"
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"label": "On change",
|
||||
"key": "onChange",
|
||||
"context": [
|
||||
{
|
||||
"label": "Field Value",
|
||||
"key": "value"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "validation/link",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Search",
|
||||
"key": "autocomplete",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Read only",
|
||||
"key": "readonly",
|
||||
"defaultValue": false,
|
||||
"dependsOn": {
|
||||
"setting": "disabled",
|
||||
"value": true,
|
||||
"invert": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Layout",
|
||||
"key": "span",
|
||||
"defaultValue": 6,
|
||||
"hidden": true,
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "1 column",
|
||||
"value": 6,
|
||||
"barIcon": "Stop",
|
||||
"barTitle": "1 column"
|
||||
},
|
||||
{
|
||||
"label": "2 columns",
|
||||
"value": 3,
|
||||
"barIcon": "ColumnTwoA",
|
||||
"barTitle": "2 columns"
|
||||
},
|
||||
{
|
||||
"label": "3 columns",
|
||||
"value": 2,
|
||||
"barIcon": "ViewColumn",
|
||||
"barTitle": "3 columns"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
[FieldType.JSON]: "jsonfield",
|
||||
[FieldType.BARCODEQR]: "codescanner",
|
||||
[FieldType.BB_REFERENCE]: "bbreferencefield",
|
||||
[FieldType.BB_REFERENCE_SINGLE]: "bbreferencesinglefield",
|
||||
}
|
||||
|
||||
const getFieldSchema = field => {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<script>
|
||||
import RelationshipField from "./RelationshipField.svelte"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { FieldType } from "@budibase/types"
|
||||
import RelationshipField from "./RelationshipField.svelte"
|
||||
|
||||
export let defaultValue
|
||||
export let type = FieldType.BB_REFERENCE
|
||||
|
||||
function updateUserIDs(value) {
|
||||
if (Array.isArray(value)) {
|
||||
|
@ -22,6 +24,7 @@
|
|||
|
||||
<RelationshipField
|
||||
{...$$props}
|
||||
{type}
|
||||
datasourceType={"user"}
|
||||
primaryDisplay={"email"}
|
||||
defaultValue={updateReferences(defaultValue)}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<script>
|
||||
import { FieldType } from "@budibase/types"
|
||||
import BBReferenceField from "./BBReferenceField.svelte"
|
||||
</script>
|
||||
|
||||
<BBReferenceField {...$$restProps} type={FieldType.BB_REFERENCE_SINGLE} />
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
||||
import { FieldType } from "@budibase/types"
|
||||
import { fetchData, Utils } from "@budibase/frontend-core"
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { getContext } from "svelte"
|
||||
import Field from "./Field.svelte"
|
||||
import { FieldTypes } from "../../../constants"
|
||||
|
||||
const { API } = getContext("sdk")
|
||||
|
||||
|
@ -21,6 +21,7 @@
|
|||
export let primaryDisplay
|
||||
export let span
|
||||
export let helpText = null
|
||||
export let type = FieldType.LINK
|
||||
|
||||
let fieldState
|
||||
let fieldApi
|
||||
|
@ -28,12 +29,10 @@
|
|||
let tableDefinition
|
||||
let searchTerm
|
||||
let open
|
||||
let initialValue
|
||||
|
||||
$: type =
|
||||
datasourceType === "table" ? FieldTypes.LINK : FieldTypes.BB_REFERENCE
|
||||
|
||||
$: multiselect = fieldSchema?.relationshipType !== "one-to-many"
|
||||
$: multiselect =
|
||||
[FieldType.LINK, FieldType.BB_REFERENCE].includes(type) &&
|
||||
fieldSchema?.relationshipType !== "one-to-many"
|
||||
$: linkedTableId = fieldSchema?.tableId
|
||||
$: fetch = fetchData({
|
||||
API,
|
||||
|
@ -52,18 +51,19 @@
|
|||
? flatten(fieldState?.value) ?? []
|
||||
: flatten(fieldState?.value)?.[0]
|
||||
$: component = multiselect ? CoreMultiselect : CoreSelect
|
||||
$: expandedDefaultValue = expand(defaultValue)
|
||||
$: primaryDisplay = primaryDisplay || tableDefinition?.primaryDisplay
|
||||
|
||||
let optionsObj = {}
|
||||
let initialValuesProcessed
|
||||
let optionsObj
|
||||
|
||||
$: {
|
||||
if (!initialValuesProcessed && primaryDisplay) {
|
||||
if (primaryDisplay && fieldState && !optionsObj) {
|
||||
// Persist the initial values as options, allowing them to be present in the dropdown,
|
||||
// even if they are not in the inital fetch results
|
||||
initialValuesProcessed = true
|
||||
optionsObj = (fieldState?.value || []).reduce((accumulator, value) => {
|
||||
let valueAsSafeArray = fieldState.value || []
|
||||
if (!Array.isArray(fieldState.value)) {
|
||||
valueAsSafeArray = [fieldState.value]
|
||||
}
|
||||
optionsObj = valueAsSafeArray.reduce((accumulator, value) => {
|
||||
// fieldState has to be an array of strings to be valid for an update
|
||||
// therefore we cannot guarantee value will be an object
|
||||
// https://linear.app/budibase/issue/BUDI-7577/refactor-the-relationshipfield-component-to-have-better-support-for
|
||||
|
@ -75,7 +75,7 @@
|
|||
[primaryDisplay]: value.primaryDisplay,
|
||||
}
|
||||
return accumulator
|
||||
}, optionsObj)
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,7 +86,7 @@
|
|||
accumulator[row._id] = row
|
||||
}
|
||||
return accumulator
|
||||
}, optionsObj)
|
||||
}, optionsObj || {})
|
||||
|
||||
return Object.values(result)
|
||||
}
|
||||
|
@ -110,17 +110,10 @@
|
|||
}
|
||||
|
||||
$: forceFetchRows(filter)
|
||||
$: debouncedFetchRows(
|
||||
searchTerm,
|
||||
primaryDisplay,
|
||||
initialValue || defaultValue
|
||||
)
|
||||
$: debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||
|
||||
const forceFetchRows = async () => {
|
||||
// if the filter has changed, then we need to reset the options, clear the selection, and re-fetch
|
||||
optionsObj = {}
|
||||
fieldApi?.setValue([])
|
||||
selectedValue = []
|
||||
debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||
}
|
||||
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
||||
|
@ -136,7 +129,7 @@
|
|||
if (defaultVal && !Array.isArray(defaultVal)) {
|
||||
defaultVal = defaultVal.split(",")
|
||||
}
|
||||
if (defaultVal && defaultVal.some(val => !optionsObj[val])) {
|
||||
if (defaultVal && optionsObj && defaultVal.some(val => !optionsObj[val])) {
|
||||
await fetch.update({
|
||||
query: { oneOf: { _id: defaultVal } },
|
||||
})
|
||||
|
@ -162,16 +155,13 @@
|
|||
if (!values) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!Array.isArray(values)) {
|
||||
values = [values]
|
||||
}
|
||||
values = values.map(value =>
|
||||
typeof value === "object" ? value._id : value
|
||||
)
|
||||
// Make sure field state is valid
|
||||
if (values?.length > 0) {
|
||||
fieldApi.setValue(values)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
|
@ -179,25 +169,20 @@
|
|||
return row?.[primaryDisplay] || "-"
|
||||
}
|
||||
|
||||
const singleHandler = e => {
|
||||
handleChange(e.detail == null ? [] : [e.detail])
|
||||
}
|
||||
|
||||
const multiHandler = e => {
|
||||
handleChange(e.detail)
|
||||
}
|
||||
|
||||
const expand = values => {
|
||||
if (!values) {
|
||||
return []
|
||||
const handleChange = e => {
|
||||
let value = e.detail
|
||||
if (!multiselect) {
|
||||
value = value == null ? [] : [value]
|
||||
}
|
||||
|
||||
if (
|
||||
type === FieldType.BB_REFERENCE_SINGLE &&
|
||||
value &&
|
||||
Array.isArray(value)
|
||||
) {
|
||||
value = value[0] || null
|
||||
}
|
||||
if (Array.isArray(values)) {
|
||||
return values
|
||||
}
|
||||
return values.split(",").map(value => value.trim())
|
||||
}
|
||||
|
||||
const handleChange = value => {
|
||||
const changed = fieldApi.setValue(value)
|
||||
if (onChange && changed) {
|
||||
onChange({
|
||||
|
@ -211,16 +196,6 @@
|
|||
fetch.nextPage()
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// if the form is in 'Update' mode, then we need to fetch the matching row so that the value is correctly set
|
||||
if (fieldState?.value) {
|
||||
initialValue =
|
||||
fieldSchema?.relationshipType !== "one-to-many"
|
||||
? flatten(fieldState?.value) ?? []
|
||||
: flatten(fieldState?.value)?.[0]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Field
|
||||
|
@ -229,7 +204,7 @@
|
|||
{disabled}
|
||||
{readonly}
|
||||
{validation}
|
||||
defaultValue={expandedDefaultValue}
|
||||
{defaultValue}
|
||||
{type}
|
||||
{span}
|
||||
{helpText}
|
||||
|
@ -243,7 +218,7 @@
|
|||
options={enrichedOptions}
|
||||
{autocomplete}
|
||||
value={selectedValue}
|
||||
on:change={multiselect ? multiHandler : singleHandler}
|
||||
on:change={handleChange}
|
||||
on:loadMore={loadMore}
|
||||
id={fieldState.fieldId}
|
||||
disabled={fieldState.disabled}
|
||||
|
|
|
@ -17,3 +17,4 @@ export { default as jsonfield } from "./JSONField.svelte"
|
|||
export { default as s3upload } from "./S3Upload.svelte"
|
||||
export { default as codescanner } from "./CodeScannerField.svelte"
|
||||
export { default as bbreferencefield } from "./BBReferenceField.svelte"
|
||||
export { default as bbreferencesinglefield } from "./BBReferenceSingleField.svelte"
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { BBReferenceFieldSubType, RelationshipType } from "@budibase/types"
|
||||
|
||||
export let api
|
||||
export let hideCounter = false
|
||||
|
||||
const { API } = getContext("grid")
|
||||
const { subtype } = $$props.schema
|
||||
|
@ -48,4 +49,5 @@
|
|||
{schema}
|
||||
{searchFunction}
|
||||
primaryDisplay={"email"}
|
||||
{hideCounter}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
import BbReferenceCell from "./BBReferenceCell.svelte"
|
||||
|
||||
export let value
|
||||
export let onChange
|
||||
export let api
|
||||
|
||||
$: arrayValue = (!Array.isArray(value) && value ? [value] : value) || []
|
||||
|
||||
$: onValueChange = value => {
|
||||
value = value[0] || null
|
||||
onChange(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<BbReferenceCell
|
||||
bind:api
|
||||
{...$$restProps}
|
||||
value={arrayValue}
|
||||
onChange={onValueChange}
|
||||
hideCounter={true}
|
||||
/>
|
|
@ -17,6 +17,7 @@
|
|||
export let contentLines = 1
|
||||
export let searchFunction = API.searchTable
|
||||
export let primaryDisplay
|
||||
export let hideCounter = false
|
||||
|
||||
const color = getColor(0)
|
||||
|
||||
|
@ -263,7 +264,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if value?.length}
|
||||
{#if !hideCounter && value?.length}
|
||||
<div class="count">
|
||||
{value?.length || 0}
|
||||
</div>
|
||||
|
|
|
@ -13,6 +13,7 @@ import JSONCell from "../cells/JSONCell.svelte"
|
|||
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
||||
import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
|
||||
import BBReferenceCell from "../cells/BBReferenceCell.svelte"
|
||||
import BBReferenceSingleCell from "../cells/BBReferenceSingleCell.svelte"
|
||||
|
||||
const TypeComponentMap = {
|
||||
[FieldType.STRING]: TextCell,
|
||||
|
@ -29,6 +30,7 @@ const TypeComponentMap = {
|
|||
[FieldType.FORMULA]: FormulaCell,
|
||||
[FieldType.JSON]: JSONCell,
|
||||
[FieldType.BB_REFERENCE]: BBReferenceCell,
|
||||
[FieldType.BB_REFERENCE_SINGLE]: BBReferenceSingleCell,
|
||||
}
|
||||
export const getCellRenderer = column => {
|
||||
return TypeComponentMap[column?.schema?.type] || TextCell
|
||||
|
|
|
@ -131,10 +131,11 @@ export const TypeIconMap = {
|
|||
[FieldType.JSON]: "Brackets",
|
||||
[FieldType.BIGINT]: "TagBold",
|
||||
[FieldType.AUTO]: "MagicWand",
|
||||
[FieldType.USER]: "User",
|
||||
[FieldType.USERS]: "UserGroup",
|
||||
[FieldType.BB_REFERENCE]: {
|
||||
[BBReferenceFieldSubType.USER]: "User",
|
||||
[BBReferenceFieldSubType.USERS]: "UserGroup",
|
||||
},
|
||||
[FieldType.BB_REFERENCE_SINGLE]: {
|
||||
[BBReferenceFieldSubType.USER]: "User",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
SourceName,
|
||||
Table,
|
||||
TableSchema,
|
||||
SupportedSqlTypes,
|
||||
} from "@budibase/types"
|
||||
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
|
||||
import { tableForDatasource } from "../../../tests/utilities/structures"
|
||||
|
@ -261,20 +262,6 @@ describe("/datasources", () => {
|
|||
})
|
||||
)
|
||||
|
||||
type SupportedSqlTypes =
|
||||
| FieldType.STRING
|
||||
| FieldType.BARCODEQR
|
||||
| FieldType.LONGFORM
|
||||
| FieldType.OPTIONS
|
||||
| FieldType.DATETIME
|
||||
| FieldType.NUMBER
|
||||
| FieldType.BOOLEAN
|
||||
| FieldType.FORMULA
|
||||
| FieldType.BIGINT
|
||||
| FieldType.BB_REFERENCE
|
||||
| FieldType.LINK
|
||||
| FieldType.ARRAY
|
||||
|
||||
const fullSchema: {
|
||||
[type in SupportedSqlTypes]: FieldSchema & { type: type }
|
||||
} = {
|
||||
|
@ -337,7 +324,12 @@ describe("/datasources", () => {
|
|||
[FieldType.BB_REFERENCE]: {
|
||||
name: "bb_reference",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
[FieldType.BB_REFERENCE_SINGLE]: {
|
||||
name: "bb_reference_single",
|
||||
type: FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -54,11 +54,13 @@ function generateSchema(
|
|||
) {
|
||||
continue
|
||||
}
|
||||
switch (column.type) {
|
||||
const columnType = column.type
|
||||
switch (columnType) {
|
||||
case FieldType.STRING:
|
||||
case FieldType.OPTIONS:
|
||||
case FieldType.LONGFORM:
|
||||
case FieldType.BARCODEQR:
|
||||
case FieldType.BB_REFERENCE_SINGLE:
|
||||
schema.text(key)
|
||||
break
|
||||
case FieldType.BB_REFERENCE: {
|
||||
|
@ -127,6 +129,18 @@ function generateSchema(
|
|||
.references(`${tableName}.${relatedPrimary}`)
|
||||
}
|
||||
break
|
||||
case FieldType.FORMULA:
|
||||
// This is allowed, but nothing to do on the external datasource
|
||||
break
|
||||
case FieldType.ATTACHMENTS:
|
||||
case FieldType.ATTACHMENT_SINGLE:
|
||||
case FieldType.AUTO:
|
||||
case FieldType.JSON:
|
||||
case FieldType.INTERNAL:
|
||||
throw `${column.type} is not a valid SQL type`
|
||||
|
||||
default:
|
||||
utils.unreachable(columnType)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,17 +52,30 @@ interface AuthTokenResponse {
|
|||
access_token: string
|
||||
}
|
||||
|
||||
const ALLOWED_TYPES = [
|
||||
FieldType.STRING,
|
||||
FieldType.FORMULA,
|
||||
FieldType.NUMBER,
|
||||
FieldType.LONGFORM,
|
||||
FieldType.DATETIME,
|
||||
FieldType.OPTIONS,
|
||||
FieldType.BOOLEAN,
|
||||
FieldType.BARCODEQR,
|
||||
FieldType.BB_REFERENCE,
|
||||
]
|
||||
const isTypeAllowed: Record<FieldType, boolean> = {
|
||||
[FieldType.STRING]: true,
|
||||
[FieldType.FORMULA]: true,
|
||||
[FieldType.NUMBER]: true,
|
||||
[FieldType.LONGFORM]: true,
|
||||
[FieldType.DATETIME]: true,
|
||||
[FieldType.OPTIONS]: true,
|
||||
[FieldType.BOOLEAN]: true,
|
||||
[FieldType.BARCODEQR]: true,
|
||||
[FieldType.BB_REFERENCE]: true,
|
||||
[FieldType.BB_REFERENCE_SINGLE]: true,
|
||||
[FieldType.ARRAY]: false,
|
||||
[FieldType.ATTACHMENTS]: false,
|
||||
[FieldType.ATTACHMENT_SINGLE]: false,
|
||||
[FieldType.LINK]: false,
|
||||
[FieldType.AUTO]: false,
|
||||
[FieldType.JSON]: false,
|
||||
[FieldType.INTERNAL]: false,
|
||||
[FieldType.BIGINT]: false,
|
||||
}
|
||||
|
||||
const ALLOWED_TYPES = Object.entries(isTypeAllowed)
|
||||
.filter(([_, allowed]) => allowed)
|
||||
.map(([type]) => type as FieldType)
|
||||
|
||||
const SCHEMA: Integration = {
|
||||
plus: true,
|
||||
|
|
|
@ -378,6 +378,7 @@ function copyExistingPropsOver(
|
|||
case FieldType.ATTACHMENT_SINGLE:
|
||||
case FieldType.JSON:
|
||||
case FieldType.BB_REFERENCE:
|
||||
case FieldType.BB_REFERENCE_SINGLE:
|
||||
shouldKeepSchema = keepIfType(FieldType.JSON, FieldType.STRING)
|
||||
break
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ const tableWithUserCol: Table = {
|
|||
schema: {
|
||||
user: {
|
||||
name: "user",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
type: FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
},
|
||||
|
@ -35,7 +35,7 @@ const tableWithUsersCol: Table = {
|
|||
user: {
|
||||
name: "user",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USERS,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -86,6 +86,18 @@ export function searchInputMapping(table: Table, options: RowSearchParams) {
|
|||
}
|
||||
for (let [key, column] of Object.entries(table.schema)) {
|
||||
switch (column.type) {
|
||||
case FieldType.BB_REFERENCE_SINGLE: {
|
||||
const subtype = column.subtype
|
||||
switch (subtype) {
|
||||
case BBReferenceFieldSubType.USER:
|
||||
userColumnMapping(key, options)
|
||||
break
|
||||
|
||||
default:
|
||||
utils.unreachable(subtype)
|
||||
}
|
||||
break
|
||||
}
|
||||
case FieldType.BB_REFERENCE: {
|
||||
const subtype = column.subtype
|
||||
switch (subtype) {
|
||||
|
|
|
@ -45,6 +45,7 @@ const FieldTypeMap: Record<FieldType, SQLiteType> = {
|
|||
[FieldType.BIGINT]: SQLiteType.TEXT,
|
||||
// TODO: consider the difference between multi-user and single user types (subtyping)
|
||||
[FieldType.BB_REFERENCE]: SQLiteType.TEXT,
|
||||
[FieldType.BB_REFERENCE_SINGLE]: SQLiteType.TEXT,
|
||||
}
|
||||
|
||||
function buildRelationshipDefinitions(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { cache, db as dbCore } from "@budibase/backend-core"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import {
|
||||
FieldType,
|
||||
BBReferenceFieldSubType,
|
||||
DocumentType,
|
||||
SEPARATOR,
|
||||
|
@ -9,92 +10,184 @@ import { InvalidBBRefError } from "./errors"
|
|||
|
||||
const ROW_PREFIX = DocumentType.ROW + SEPARATOR
|
||||
|
||||
export function processInputBBReferences(
|
||||
value: string | { _id: string },
|
||||
type: FieldType.BB_REFERENCE_SINGLE
|
||||
): Promise<string | null>
|
||||
export function processInputBBReferences(
|
||||
value: string | string[] | { _id: string } | { _id: string }[],
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType
|
||||
): Promise<string | null>
|
||||
|
||||
export async function processInputBBReferences(
|
||||
value: string | string[] | { _id: string } | { _id: string }[],
|
||||
subtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
||||
type: FieldType.BB_REFERENCE | FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype?: BBReferenceFieldSubType
|
||||
): Promise<string | string[] | null> {
|
||||
let referenceIds: string[] = []
|
||||
switch (type) {
|
||||
case FieldType.BB_REFERENCE: {
|
||||
let referenceIds: string[] = []
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
referenceIds.push(
|
||||
...value.map(idOrDoc =>
|
||||
typeof idOrDoc === "string" ? idOrDoc : idOrDoc._id
|
||||
)
|
||||
)
|
||||
} else if (typeof value !== "string") {
|
||||
referenceIds.push(value._id)
|
||||
} else {
|
||||
referenceIds.push(
|
||||
...value
|
||||
.split(",")
|
||||
.filter(x => x)
|
||||
.map((id: string) => id.trim())
|
||||
)
|
||||
}
|
||||
|
||||
// make sure all reference IDs are correct global user IDs
|
||||
// they may be user metadata references (start with row prefix)
|
||||
// and these need to be converted to global IDs
|
||||
referenceIds = referenceIds.map(id => {
|
||||
if (id?.startsWith(ROW_PREFIX)) {
|
||||
return dbCore.getGlobalIDFromUserMetadataID(id)
|
||||
} else {
|
||||
return id
|
||||
}
|
||||
})
|
||||
|
||||
switch (subtype) {
|
||||
case BBReferenceFieldSubType.USER:
|
||||
case BBReferenceFieldSubType.USERS: {
|
||||
const { notFoundIds } = await cache.user.getUsers(referenceIds)
|
||||
|
||||
if (notFoundIds?.length) {
|
||||
throw new InvalidBBRefError(
|
||||
notFoundIds[0],
|
||||
BBReferenceFieldSubType.USER
|
||||
if (Array.isArray(value)) {
|
||||
referenceIds.push(
|
||||
...value.map(idOrDoc =>
|
||||
typeof idOrDoc === "string" ? idOrDoc : idOrDoc._id
|
||||
)
|
||||
)
|
||||
} else if (typeof value !== "string") {
|
||||
referenceIds.push(value._id)
|
||||
} else {
|
||||
referenceIds.push(
|
||||
...value
|
||||
.split(",")
|
||||
.filter(x => x)
|
||||
.map((id: string) => id.trim())
|
||||
)
|
||||
}
|
||||
|
||||
if (subtype === BBReferenceFieldSubType.USERS) {
|
||||
return referenceIds
|
||||
// make sure all reference IDs are correct global user IDs
|
||||
// they may be user metadata references (start with row prefix)
|
||||
// and these need to be converted to global IDs
|
||||
referenceIds = referenceIds.map(id => {
|
||||
if (id?.startsWith(ROW_PREFIX)) {
|
||||
return dbCore.getGlobalIDFromUserMetadataID(id)
|
||||
} else {
|
||||
return id
|
||||
}
|
||||
})
|
||||
|
||||
switch (subtype) {
|
||||
case undefined:
|
||||
throw "Subtype must be defined"
|
||||
case BBReferenceFieldSubType.USER:
|
||||
case BBReferenceFieldSubType.USERS: {
|
||||
const { notFoundIds } = await cache.user.getUsers(referenceIds)
|
||||
|
||||
if (notFoundIds?.length) {
|
||||
throw new InvalidBBRefError(
|
||||
notFoundIds[0],
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
}
|
||||
|
||||
if (!referenceIds?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (subtype === BBReferenceFieldSubType.USERS) {
|
||||
return referenceIds
|
||||
}
|
||||
|
||||
return referenceIds.join(",")
|
||||
}
|
||||
default:
|
||||
throw utils.unreachable(subtype)
|
||||
}
|
||||
}
|
||||
case FieldType.BB_REFERENCE_SINGLE: {
|
||||
if (value && Array.isArray(value)) {
|
||||
throw "BB_REFERENCE_SINGLE cannot be an array"
|
||||
}
|
||||
|
||||
return referenceIds.join(",") || null
|
||||
const id = typeof value === "string" ? value : value._id
|
||||
|
||||
const user = await cache.user.getUser(id)
|
||||
|
||||
if (!user) {
|
||||
throw new InvalidBBRefError(id, BBReferenceFieldSubType.USER)
|
||||
}
|
||||
|
||||
return user._id!
|
||||
}
|
||||
|
||||
default:
|
||||
throw utils.unreachable(subtype)
|
||||
throw utils.unreachable(type)
|
||||
}
|
||||
}
|
||||
|
||||
interface UserReferenceInfo {
|
||||
_id: string
|
||||
primaryDisplay: string
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
}
|
||||
|
||||
export function processOutputBBReferences(
|
||||
value: string,
|
||||
type: FieldType.BB_REFERENCE_SINGLE
|
||||
): Promise<UserReferenceInfo | undefined>
|
||||
export function processOutputBBReferences(
|
||||
value: string,
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType
|
||||
): Promise<UserReferenceInfo[] | undefined>
|
||||
|
||||
export async function processOutputBBReferences(
|
||||
value: string | string[],
|
||||
subtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
||||
type: FieldType.BB_REFERENCE | FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype?: BBReferenceFieldSubType
|
||||
) {
|
||||
if (value === null || value === undefined) {
|
||||
// Already processed or nothing to process
|
||||
return value || undefined
|
||||
}
|
||||
|
||||
const ids =
|
||||
typeof value === "string" ? value.split(",").filter(id => !!id) : value
|
||||
switch (type) {
|
||||
case FieldType.BB_REFERENCE: {
|
||||
const ids =
|
||||
typeof value === "string" ? value.split(",").filter(id => !!id) : value
|
||||
|
||||
switch (subtype) {
|
||||
case BBReferenceFieldSubType.USER:
|
||||
case BBReferenceFieldSubType.USERS: {
|
||||
const { users } = await cache.user.getUsers(ids)
|
||||
if (!users.length) {
|
||||
switch (subtype) {
|
||||
case undefined:
|
||||
throw "Subtype must be defined"
|
||||
case BBReferenceFieldSubType.USER:
|
||||
case BBReferenceFieldSubType.USERS: {
|
||||
const { users } = await cache.user.getUsers(ids)
|
||||
if (!users.length) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return users.map(u => ({
|
||||
_id: u._id,
|
||||
primaryDisplay: u.email,
|
||||
email: u.email,
|
||||
firstName: u.firstName,
|
||||
lastName: u.lastName,
|
||||
}))
|
||||
}
|
||||
default:
|
||||
throw utils.unreachable(subtype)
|
||||
}
|
||||
}
|
||||
case FieldType.BB_REFERENCE_SINGLE: {
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return users.map(u => ({
|
||||
_id: u._id,
|
||||
primaryDisplay: u.email,
|
||||
email: u.email,
|
||||
firstName: u.firstName,
|
||||
lastName: u.lastName,
|
||||
}))
|
||||
let user
|
||||
try {
|
||||
user = await cache.user.getUser(value as string)
|
||||
} catch (err: any) {
|
||||
if (err.code !== 404) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
if (!user) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
_id: user._id,
|
||||
primaryDisplay: user.email,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw utils.unreachable(subtype)
|
||||
throw utils.unreachable(type)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -160,10 +160,14 @@ export async function inputProcessing(
|
|||
if (attachment?.url) {
|
||||
delete clonedRow[key].url
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.BB_REFERENCE && value) {
|
||||
clonedRow[key] = await processInputBBReferences(value, field.subtype)
|
||||
} else if (field.type === FieldType.BB_REFERENCE && value) {
|
||||
clonedRow[key] = await processInputBBReferences(
|
||||
value,
|
||||
field.type,
|
||||
field.subtype
|
||||
)
|
||||
} else if (field.type === FieldType.BB_REFERENCE_SINGLE && value) {
|
||||
clonedRow[key] = await processInputBBReferences(value, field.type)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,9 +253,20 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
for (let row of enriched) {
|
||||
row[property] = await processOutputBBReferences(
|
||||
row[property],
|
||||
column.type,
|
||||
column.subtype
|
||||
)
|
||||
}
|
||||
} else if (
|
||||
!opts.skipBBReferences &&
|
||||
column.type == FieldType.BB_REFERENCE_SINGLE
|
||||
) {
|
||||
for (let row of enriched) {
|
||||
row[property] = await processOutputBBReferences(
|
||||
row[property],
|
||||
column.type
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import _ from "lodash"
|
||||
import * as backendCore from "@budibase/backend-core"
|
||||
import { BBReferenceFieldSubType, User } from "@budibase/types"
|
||||
import { BBReferenceFieldSubType, FieldType, User } from "@budibase/types"
|
||||
import {
|
||||
processInputBBReferences,
|
||||
processOutputBBReferences,
|
||||
|
@ -63,7 +63,11 @@ describe("bbReferenceProcessor", () => {
|
|||
const userId = user!._id!
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences(userId, BBReferenceFieldSubType.USER)
|
||||
processInputBBReferences(
|
||||
userId,
|
||||
FieldType.BB_REFERENCE,
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
|
||||
expect(result).toEqual(userId)
|
||||
|
@ -76,7 +80,11 @@ describe("bbReferenceProcessor", () => {
|
|||
|
||||
await expect(
|
||||
config.doInTenant(() =>
|
||||
processInputBBReferences(userId, BBReferenceFieldSubType.USER)
|
||||
processInputBBReferences(
|
||||
userId,
|
||||
FieldType.BB_REFERENCE,
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
).rejects.toThrow(
|
||||
new InvalidBBRefError(userId, BBReferenceFieldSubType.USER)
|
||||
|
@ -90,7 +98,11 @@ describe("bbReferenceProcessor", () => {
|
|||
|
||||
const userIdCsv = userIds.join(" , ")
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences(userIdCsv, BBReferenceFieldSubType.USER)
|
||||
processInputBBReferences(
|
||||
userIdCsv,
|
||||
FieldType.BB_REFERENCE,
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
|
||||
expect(result).toEqual(userIds.join(","))
|
||||
|
@ -110,7 +122,11 @@ describe("bbReferenceProcessor", () => {
|
|||
|
||||
await expect(
|
||||
config.doInTenant(() =>
|
||||
processInputBBReferences(userIdCsv, BBReferenceFieldSubType.USER)
|
||||
processInputBBReferences(
|
||||
userIdCsv,
|
||||
FieldType.BB_REFERENCE,
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
).rejects.toThrow(
|
||||
new InvalidBBRefError(wrongId, BBReferenceFieldSubType.USER)
|
||||
|
@ -123,6 +139,7 @@ describe("bbReferenceProcessor", () => {
|
|||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences(
|
||||
{ _id: userId },
|
||||
FieldType.BB_REFERENCE,
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
|
@ -136,7 +153,11 @@ describe("bbReferenceProcessor", () => {
|
|||
const userIds = _.sampleSize(users, 3).map(x => x._id!)
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences(userIds, BBReferenceFieldSubType.USER)
|
||||
processInputBBReferences(
|
||||
userIds,
|
||||
FieldType.BB_REFERENCE,
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
|
||||
expect(result).toEqual(userIds.join(","))
|
||||
|
@ -146,7 +167,11 @@ describe("bbReferenceProcessor", () => {
|
|||
|
||||
it("empty strings will return null", async () => {
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences("", BBReferenceFieldSubType.USER)
|
||||
processInputBBReferences(
|
||||
"",
|
||||
FieldType.BB_REFERENCE,
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
|
||||
expect(result).toEqual(null)
|
||||
|
@ -154,7 +179,11 @@ describe("bbReferenceProcessor", () => {
|
|||
|
||||
it("empty arrays will return null", async () => {
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences([], BBReferenceFieldSubType.USER)
|
||||
processInputBBReferences(
|
||||
[],
|
||||
FieldType.BB_REFERENCE,
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
|
||||
expect(result).toEqual(null)
|
||||
|
@ -164,7 +193,11 @@ describe("bbReferenceProcessor", () => {
|
|||
const userId = _.sample(users)!._id!
|
||||
const userMetadataId = backendCore.db.generateUserMetadataID(userId)
|
||||
const result = await config.doInTenant(() =>
|
||||
processInputBBReferences(userMetadataId, BBReferenceFieldSubType.USER)
|
||||
processInputBBReferences(
|
||||
userMetadataId,
|
||||
FieldType.BB_REFERENCE,
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
expect(result).toBe(userId)
|
||||
})
|
||||
|
@ -178,7 +211,11 @@ describe("bbReferenceProcessor", () => {
|
|||
const userId = user._id!
|
||||
|
||||
const result = await config.doInTenant(() =>
|
||||
processOutputBBReferences(userId, BBReferenceFieldSubType.USER)
|
||||
processOutputBBReferences(
|
||||
userId,
|
||||
FieldType.BB_REFERENCE,
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
|
||||
expect(result).toEqual([
|
||||
|
@ -202,6 +239,7 @@ describe("bbReferenceProcessor", () => {
|
|||
const result = await config.doInTenant(() =>
|
||||
processOutputBBReferences(
|
||||
[userId1, userId2].join(","),
|
||||
FieldType.BB_REFERENCE,
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
)
|
||||
|
|
|
@ -67,6 +67,7 @@ describe("rowProcessor - inputProcessing", () => {
|
|||
)
|
||||
expect(bbReferenceProcessor.processInputBBReferences).toHaveBeenCalledWith(
|
||||
"123",
|
||||
"bb_reference",
|
||||
"user"
|
||||
)
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
).toHaveBeenCalledTimes(1)
|
||||
expect(bbReferenceProcessor.processOutputBBReferences).toHaveBeenCalledWith(
|
||||
"123",
|
||||
FieldType.BB_REFERENCE,
|
||||
BBReferenceFieldSubType.USER
|
||||
)
|
||||
})
|
||||
|
|
|
@ -54,6 +54,7 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
|
|||
type: columnType,
|
||||
subtype: columnSubtype,
|
||||
autocolumn: isAutoColumn,
|
||||
constraints,
|
||||
} = schema[columnName] || {}
|
||||
|
||||
// If the column had an invalid value we don't want to override it
|
||||
|
@ -61,6 +62,12 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
|
|||
return
|
||||
}
|
||||
|
||||
const isRequired =
|
||||
!!constraints &&
|
||||
((typeof constraints.presence !== "boolean" &&
|
||||
!constraints.presence?.allowEmpty) ||
|
||||
constraints.presence === true)
|
||||
|
||||
// If the columnType is not a string, then it's not present in the schema, and should be added to the invalid columns array
|
||||
if (typeof columnType !== "string") {
|
||||
results.invalidColumns.push(columnName)
|
||||
|
@ -92,8 +99,9 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
|
|||
) {
|
||||
results.schemaValidation[columnName] = false
|
||||
} else if (
|
||||
columnType === FieldType.BB_REFERENCE &&
|
||||
!isValidBBReference(columnData, columnSubtype)
|
||||
(columnType === FieldType.BB_REFERENCE ||
|
||||
columnType === FieldType.BB_REFERENCE_SINGLE) &&
|
||||
!isValidBBReference(columnData, columnType, columnSubtype, isRequired)
|
||||
) {
|
||||
results.schemaValidation[columnName] = false
|
||||
} else {
|
||||
|
@ -147,6 +155,10 @@ export function parse(rows: Rows, schema: TableSchema): Rows {
|
|||
utils.unreachable(columnSubtype)
|
||||
}
|
||||
}
|
||||
} else if (columnType === FieldType.BB_REFERENCE_SINGLE) {
|
||||
const parsedValue =
|
||||
columnData && parseCsvExport<{ _id: string }>(columnData)
|
||||
parsedRow[columnName] = parsedValue?._id
|
||||
} else if (
|
||||
(columnType === FieldType.ATTACHMENTS ||
|
||||
columnType === FieldType.ATTACHMENT_SINGLE) &&
|
||||
|
@ -163,24 +175,32 @@ export function parse(rows: Rows, schema: TableSchema): Rows {
|
|||
}
|
||||
|
||||
function isValidBBReference(
|
||||
columnData: any,
|
||||
columnSubtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
||||
data: any,
|
||||
type: FieldType.BB_REFERENCE | FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype: BBReferenceFieldSubType,
|
||||
isRequired: boolean
|
||||
): boolean {
|
||||
switch (columnSubtype) {
|
||||
if (typeof data !== "string") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (type === FieldType.BB_REFERENCE_SINGLE) {
|
||||
if (!data) {
|
||||
return !isRequired
|
||||
}
|
||||
const user = parseCsvExport<{ _id: string }>(data)
|
||||
return db.isGlobalUserID(user._id)
|
||||
}
|
||||
|
||||
switch (subtype) {
|
||||
case BBReferenceFieldSubType.USER:
|
||||
case BBReferenceFieldSubType.USERS: {
|
||||
if (typeof columnData !== "string") {
|
||||
return false
|
||||
}
|
||||
const userArray = parseCsvExport<{ _id: string }[]>(columnData)
|
||||
const userArray = parseCsvExport<{ _id: string }[]>(data)
|
||||
if (!Array.isArray(userArray)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
columnSubtype === BBReferenceFieldSubType.USER &&
|
||||
userArray.length > 1
|
||||
) {
|
||||
if (subtype === BBReferenceFieldSubType.USER && userArray.length > 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -190,6 +210,6 @@ function isValidBBReference(
|
|||
return !constainsWrongId
|
||||
}
|
||||
default:
|
||||
throw utils.unreachable(columnSubtype)
|
||||
throw utils.unreachable(subtype)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ export const getValidOperatorsForType = (
|
|||
subtype?: BBReferenceFieldSubType
|
||||
formulaType?: FormulaType
|
||||
},
|
||||
field: string,
|
||||
datasource: Datasource & { tableId: any }
|
||||
field?: string,
|
||||
datasource?: Datasource & { tableId: any }
|
||||
) => {
|
||||
const Op = OperatorOptions
|
||||
const stringOps = [
|
||||
|
@ -69,7 +69,8 @@ export const getValidOperatorsForType = (
|
|||
} else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) {
|
||||
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
||||
} else if (
|
||||
type === FieldType.BB_REFERENCE &&
|
||||
(type === FieldType.BB_REFERENCE_SINGLE ||
|
||||
type === FieldType.BB_REFERENCE) &&
|
||||
subtype == BBReferenceFieldSubType.USER
|
||||
) {
|
||||
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
|
||||
|
|
|
@ -18,6 +18,7 @@ const allowDisplayColumnByType: Record<FieldType, boolean> = {
|
|||
[FieldType.LINK]: false,
|
||||
[FieldType.JSON]: false,
|
||||
[FieldType.BB_REFERENCE]: false,
|
||||
[FieldType.BB_REFERENCE_SINGLE]: false,
|
||||
}
|
||||
|
||||
const allowSortColumnByType: Record<FieldType, boolean> = {
|
||||
|
@ -39,6 +40,7 @@ const allowSortColumnByType: Record<FieldType, boolean> = {
|
|||
[FieldType.ARRAY]: false,
|
||||
[FieldType.LINK]: false,
|
||||
[FieldType.BB_REFERENCE]: false,
|
||||
[FieldType.BB_REFERENCE_SINGLE]: false,
|
||||
}
|
||||
|
||||
export function canBeDisplayColumn(type: FieldType): boolean {
|
||||
|
|
|
@ -100,13 +100,17 @@ export enum FieldType {
|
|||
*/
|
||||
BIGINT = "bigint",
|
||||
/**
|
||||
* a JSON type, called User within Budibase. This type is used to represent a link to an internal Budibase
|
||||
* a JSON type, called Users within Budibase. It will hold an array of strings. This type is used to represent a link to an internal Budibase
|
||||
* resource, like a user or group, today only users are supported. This type will be represented as an
|
||||
* array of internal resource IDs (e.g. user IDs) within the row - this ID list will be enriched with
|
||||
* the full resources when rows are returned from the API. The full resources can be input to the API, or
|
||||
* an array of resource IDs, the API will squash these down and validate them before saving the row.
|
||||
*/
|
||||
BB_REFERENCE = "bb_reference",
|
||||
/**
|
||||
* a string type, called User within Budibase. Same logic as `bb_reference`, storing a single id as string instead of an array
|
||||
*/
|
||||
BB_REFERENCE_SINGLE = "bb_reference_single",
|
||||
}
|
||||
|
||||
export interface RowAttachment {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { FieldType } from "../row"
|
||||
|
||||
export enum RelationshipType {
|
||||
ONE_TO_MANY = "one-to-many",
|
||||
MANY_TO_ONE = "many-to-one",
|
||||
|
@ -27,5 +29,21 @@ export enum FormulaType {
|
|||
|
||||
export enum BBReferenceFieldSubType {
|
||||
USER = "user",
|
||||
/** @deprecated this should not be used anymore, left here in order to support the existing usages */
|
||||
USERS = "users",
|
||||
}
|
||||
|
||||
export type SupportedSqlTypes =
|
||||
| FieldType.STRING
|
||||
| FieldType.BARCODEQR
|
||||
| FieldType.LONGFORM
|
||||
| FieldType.OPTIONS
|
||||
| FieldType.DATETIME
|
||||
| FieldType.NUMBER
|
||||
| FieldType.BOOLEAN
|
||||
| FieldType.FORMULA
|
||||
| FieldType.BIGINT
|
||||
| FieldType.BB_REFERENCE
|
||||
| FieldType.BB_REFERENCE_SINGLE
|
||||
| FieldType.LINK
|
||||
| FieldType.ARRAY
|
||||
|
|
|
@ -110,9 +110,14 @@ export interface FormulaFieldMetadata extends BaseFieldSchema {
|
|||
export interface BBReferenceFieldMetadata
|
||||
extends Omit<BaseFieldSchema, "subtype"> {
|
||||
type: FieldType.BB_REFERENCE
|
||||
subtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
||||
subtype: BBReferenceFieldSubType
|
||||
relationshipType?: RelationshipType
|
||||
}
|
||||
export interface BBReferenceSingleFieldMetadata
|
||||
extends Omit<BaseFieldSchema, "subtype"> {
|
||||
type: FieldType.BB_REFERENCE_SINGLE
|
||||
subtype: Exclude<BBReferenceFieldSubType, BBReferenceFieldSubType.USERS>
|
||||
}
|
||||
|
||||
export interface AttachmentFieldMetadata extends BaseFieldSchema {
|
||||
type: FieldType.ATTACHMENTS
|
||||
|
@ -164,6 +169,7 @@ interface OtherFieldMetadata extends BaseFieldSchema {
|
|||
| FieldType.NUMBER
|
||||
| FieldType.LONGFORM
|
||||
| FieldType.BB_REFERENCE
|
||||
| FieldType.BB_REFERENCE_SINGLE
|
||||
| FieldType.ATTACHMENTS
|
||||
>
|
||||
}
|
||||
|
@ -179,6 +185,7 @@ export type FieldSchema =
|
|||
| BBReferenceFieldMetadata
|
||||
| JsonFieldMetadata
|
||||
| AttachmentFieldMetadata
|
||||
| BBReferenceSingleFieldMetadata
|
||||
|
||||
export interface TableSchema {
|
||||
[key: string]: FieldSchema
|
||||
|
|
Loading…
Reference in New Issue