1
0
Fork 0
mirror of synced 2024-08-09 15:17:57 +12:00

Merge pull request #11988 from Budibase/budi-7589/user-column-multi-user-filtering-support

Multi user column
This commit is contained in:
Adria Navarro 2023-10-09 11:52:22 +02:00 committed by GitHub
commit 6705b67b67
12 changed files with 167 additions and 78 deletions

View file

@ -33,7 +33,7 @@
import { getBindings } from "components/backend/DataTable/formula"
import JSONSchemaModal from "./JSONSchemaModal.svelte"
import { ValidColumnNameRegex } from "@budibase/shared-core"
import { FieldType } from "@budibase/types"
import { FieldType, FieldSubtype, SourceName } from "@budibase/types"
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
const AUTO_TYPE = "auto"
@ -43,7 +43,6 @@
const NUMBER_TYPE = FIELDS.NUMBER.type
const JSON_TYPE = FIELDS.JSON.type
const DATE_TYPE = FIELDS.DATETIME.type
const USER_REFRENCE_TYPE = FIELDS.BB_REFERENCE_USER.compositeType
const dispatch = createEventDispatcher()
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
@ -52,7 +51,19 @@
export let field
let mounted = false
let fieldDefinitions = cloneDeep(FIELDS)
const fieldDefinitions = Object.values(FIELDS).reduce(
// Storing the fields by complex field id
(acc, field) => ({
...acc,
[makeFieldId(field.type, field.subtype)]: field,
}),
{}
)
function makeFieldId(type, subtype) {
return `${type}${subtype || ""}`.toUpperCase()
}
let originalName
let linkEditDisabled
let primaryDisplay
@ -72,8 +83,8 @@
let jsonSchemaModal
let allowedTypes = []
let editableColumn = {
type: fieldDefinitions.STRING.type,
constraints: fieldDefinitions.STRING.constraints,
type: FIELDS.STRING.type,
constraints: FIELDS.STRING.constraints,
// Initial value for column name in other table for linked records
fieldName: $tables.selected.name,
}
@ -139,9 +150,6 @@
$tables.selected.primaryDisplay == null ||
$tables.selected.primaryDisplay === editableColumn.name
if (editableColumn.type === FieldType.BB_REFERENCE) {
editableColumn.type = `${editableColumn.type}_${editableColumn.subtype}`
}
// 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
@ -172,7 +180,17 @@
}
}
allowedTypes = getAllowedTypes()
if (!savingColumn) {
editableColumn.fieldId = makeFieldId(
editableColumn.type,
editableColumn.subtype
)
allowedTypes = getAllowedTypes().map(t => ({
fieldId: makeFieldId(t.type, t.subtype),
...t,
}))
}
}
$: initialiseField(field, savingColumn)
@ -249,13 +267,7 @@
let saveColumn = cloneDeep(editableColumn)
// Handle types on composite types
const definition = fieldDefinitions[saveColumn.type.toUpperCase()]
if (definition && saveColumn.type === definition.compositeType) {
saveColumn.type = definition.type
saveColumn.subtype = definition.subtype
delete saveColumn.compositeType
}
delete saveColumn.fieldId
if (saveColumn.type === AUTO_TYPE) {
saveColumn = buildAutoColumn(
@ -320,27 +332,33 @@
}
}
function handleTypeChange(event) {
function onHandleTypeChange(event) {
handleTypeChange(event.detail)
}
function handleTypeChange(type) {
// remove any extra fields that may not be related to this type
delete editableColumn.autocolumn
delete editableColumn.subtype
delete editableColumn.tableId
delete editableColumn.relationshipType
delete editableColumn.formulaType
delete editableColumn.constraints
// Add in defaults and initial definition
const definition = fieldDefinitions[event.detail?.toUpperCase()]
const definition = fieldDefinitions[type?.toUpperCase()]
if (definition?.constraints) {
editableColumn.constraints = definition.constraints
}
editableColumn.type = definition.type
editableColumn.subtype = definition.subtype
// Default relationships many to many
if (editableColumn.type === LINK_TYPE) {
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
} else if (editableColumn.type === FORMULA_TYPE) {
editableColumn.formulaType = "dynamic"
} else if (editableColumn.type === USER_REFRENCE_TYPE) {
editableColumn.relationshipType = RelationshipType.ONE_TO_MANY
}
}
@ -381,9 +399,26 @@
return ALLOWABLE_NUMBER_OPTIONS
}
const isUsers =
editableColumn.type === FieldType.BB_REFERENCE &&
editableColumn.subtype === FieldSubtype.USERS
if (!external) {
return [
...Object.values(fieldDefinitions),
FIELDS.STRING,
FIELDS.BARCODEQR,
FIELDS.LONGFORM,
FIELDS.OPTIONS,
FIELDS.ARRAY,
FIELDS.NUMBER,
FIELDS.BIGINT,
FIELDS.BOOLEAN,
FIELDS.DATETIME,
FIELDS.ATTACHMENT,
FIELDS.LINK,
FIELDS.FORMULA,
FIELDS.JSON,
isUsers ? FIELDS.USERS : FIELDS.USER,
{ name: "Auto Column", type: AUTO_TYPE },
]
} else {
@ -397,7 +432,7 @@
FIELDS.BOOLEAN,
FIELDS.FORMULA,
FIELDS.BIGINT,
FIELDS.BB_REFERENCE_USER,
isUsers ? FIELDS.USERS : FIELDS.USER,
]
// no-sql or a spreadsheet
if (!external || table.sql) {
@ -472,6 +507,13 @@
return newError
}
function isUsersColumn(column) {
return (
column.type === FieldType.BB_REFERENCE &&
[FieldSubtype.USER, FieldSubtype.USERS].includes(column.subtype)
)
}
onMount(() => {
mounted = true
})
@ -489,11 +531,11 @@
{/if}
<Select
disabled={!typeEnabled}
bind:value={editableColumn.type}
on:change={handleTypeChange}
bind:value={editableColumn.fieldId}
on:change={onHandleTypeChange}
options={allowedTypes}
getOptionLabel={field => field.name}
getOptionValue={field => field.compositeType || field.type}
getOptionValue={field => field.fieldId}
getOptionIcon={field => field.icon}
isOptionEnabled={option => {
if (option.type == AUTO_TYPE) {
@ -555,7 +597,7 @@
<DatePicker bind:value={editableColumn.constraints.datetime.latest} />
</div>
</div>
{#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER" && !editableColumn.dateOnly}
{#if datasource?.source !== SourceName.ORACLE && datasource?.source !== SourceName.SQL_SERVER && !editableColumn.dateOnly}
<div>
<div class="row">
<Label>Time zones</Label>
@ -659,18 +701,20 @@
<Button primary text on:click={openJsonSchemaEditor}
>Open schema editor</Button
>
{:else if editableColumn.type === USER_REFRENCE_TYPE}
<!-- Disabled temporally -->
<!-- <Toggle
value={editableColumn.relationshipType === RelationshipType.MANY_TO_MANY}
{:else if isUsersColumn(editableColumn) && datasource?.source !== SourceName.GOOGLE_SHEETS}
<Toggle
value={editableColumn.subtype === FieldSubtype.USERS}
on:change={e =>
(editableColumn.relationshipType = e.detail
? RelationshipType.MANY_TO_MANY
: RelationshipType.ONE_TO_MANY)}
handleTypeChange(
makeFieldId(
FieldType.BB_REFERENCE,
e.detail ? FieldSubtype.USERS : FieldSubtype.USER
)
)}
disabled={!isCreating}
thin
text="Allow multiple users"
/> -->
/>
{/if}
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
<Select

View file

@ -20,7 +20,6 @@
import { FieldType } from "@budibase/types"
import { createEventDispatcher, onMount } from "svelte"
import FilterUsers from "./FilterUsers.svelte"
import { RelationshipType } from "constants/backend"
export let schemaFields
export let filters = []
@ -126,6 +125,7 @@
// Update type based on field
const fieldSchema = enrichedSchemaFields.find(x => x.name === filter.field)
filter.type = fieldSchema?.type
filter.subtype = fieldSchema?.subtype
// Update external type based on field
filter.externalType = getSchema(filter)?.externalType
@ -196,7 +196,7 @@
}
return LuceneUtils.getValidOperatorsForType(
filter.type,
{ type: filter.type, subtype: filter.subtype },
filter.field,
datasource
)
@ -301,9 +301,10 @@
{:else if filter.type === FieldType.BB_REFERENCE}
<FilterUsers
bind:value={filter.value}
multiselect={getSchema(filter).relationshipType ===
RelationshipType.MANY_TO_MANY ||
filter.operator === OperatorOptions.In.value}
multiselect={[
OperatorOptions.In.value,
OperatorOptions.ContainsAny.value,
].includes(filter.operator)}
disabled={filter.noValue}
/>
{:else}

View file

@ -1,7 +1,9 @@
import { FieldType, FieldSubtype } from "@budibase/types"
export const FIELDS = {
STRING: {
name: "Text",
type: "string",
type: FieldType.STRING,
icon: "Text",
constraints: {
type: "string",
@ -11,7 +13,7 @@ export const FIELDS = {
},
BARCODEQR: {
name: "Barcode/QR",
type: "barcodeqr",
type: FieldType.BARCODEQR,
icon: "Camera",
constraints: {
type: "string",
@ -21,7 +23,7 @@ export const FIELDS = {
},
LONGFORM: {
name: "Long Form Text",
type: "longform",
type: FieldType.LONGFORM,
icon: "TextAlignLeft",
constraints: {
type: "string",
@ -31,7 +33,7 @@ export const FIELDS = {
},
OPTIONS: {
name: "Options",
type: "options",
type: FieldType.OPTIONS,
icon: "Dropdown",
constraints: {
type: "string",
@ -41,7 +43,7 @@ export const FIELDS = {
},
ARRAY: {
name: "Multi-select",
type: "array",
type: FieldType.ARRAY,
icon: "Duplicate",
constraints: {
type: "array",
@ -51,7 +53,7 @@ export const FIELDS = {
},
NUMBER: {
name: "Number",
type: "number",
type: FieldType.NUMBER,
icon: "123",
constraints: {
type: "number",
@ -61,12 +63,12 @@ export const FIELDS = {
},
BIGINT: {
name: "BigInt",
type: "bigint",
type: FieldType.BIGINT,
icon: "TagBold",
},
BOOLEAN: {
name: "Boolean",
type: "boolean",
type: FieldType.BOOLEAN,
icon: "Boolean",
constraints: {
type: "boolean",
@ -75,7 +77,7 @@ export const FIELDS = {
},
DATETIME: {
name: "Date/Time",
type: "datetime",
type: FieldType.DATETIME,
icon: "Calendar",
constraints: {
type: "string",
@ -89,7 +91,7 @@ export const FIELDS = {
},
ATTACHMENT: {
name: "Attachment",
type: "attachment",
type: FieldType.ATTACHMENT,
icon: "Folder",
constraints: {
type: "array",
@ -98,7 +100,7 @@ export const FIELDS = {
},
LINK: {
name: "Relationship",
type: "link",
type: FieldType.LINK,
icon: "Link",
constraints: {
type: "array",
@ -107,26 +109,34 @@ export const FIELDS = {
},
FORMULA: {
name: "Formula",
type: "formula",
type: FieldType.FORMULA,
icon: "Calculator",
constraints: {},
},
JSON: {
name: "JSON",
type: "json",
type: FieldType.JSON,
icon: "Brackets",
constraints: {
type: "object",
presence: false,
},
},
BB_REFERENCE_USER: {
USER: {
name: "User",
type: "bb_reference",
subtype: "user",
compositeType: "bb_reference_user", // Used for working with the subtype on CreateEditColumn as is it was a primary type
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USER,
icon: "User",
},
USERS: {
name: "Users",
type: FieldType.BB_REFERENCE,
subtype: FieldSubtype.USERS,
icon: "User",
constraints: {
type: "array",
},
},
}
export const AUTO_COLUMN_SUB_TYPES = {

View file

@ -118,7 +118,7 @@
}
const getOperatorOptions = condition => {
return LuceneUtils.getValidOperatorsForType(condition.valueType)
return LuceneUtils.getValidOperatorsForType({ type: condition.valueType })
}
const onOperatorChange = (condition, newOperator) => {
@ -137,9 +137,9 @@
condition.referenceValue = null
// Ensure a valid operator is set
const validOperators = LuceneUtils.getValidOperatorsForType(newType).map(
x => x.value
)
const validOperators = LuceneUtils.getValidOperatorsForType({
type: newType,
}).map(x => x.value)
if (!validOperators.includes(condition.operator)) {
condition.operator =
validOperators[0] ?? Constants.OperatorOptions.Equals.value

View file

@ -63,7 +63,7 @@
// Ensure a valid operator is set
const validOperators = LuceneUtils.getValidOperatorsForType(
expression.type,
{ type: expression.type },
expression.field,
datasource
).map(x => x.value)
@ -125,7 +125,7 @@
<Select
disabled={!filter.field}
options={LuceneUtils.getValidOperatorsForType(
filter.type,
{ type: filter.type, subtype: filter.subtype },
filter.field,
datasource
)}

View file

@ -1,7 +1,7 @@
<script>
import { getContext } from "svelte"
import RelationshipCell from "./RelationshipCell.svelte"
import { FieldSubtype } from "@budibase/types"
import { FieldSubtype, RelationshipType } from "@budibase/types"
export let api
@ -12,10 +12,14 @@
...$$props.schema,
// This is not really used, just adding some content to be able to render the relationship cell
tableId: "external",
relationshipType:
subtype === FieldSubtype.USER
? RelationshipType.ONE_TO_MANY
: RelationshipType.MANY_TO_MANY,
}
async function searchFunction(searchParams) {
if (subtype !== FieldSubtype.USER) {
if (subtype !== FieldSubtype.USER && subtype !== FieldSubtype.USERS) {
throw `Search for '${subtype}' not implemented`
}

View file

@ -21,6 +21,7 @@ const TypeIconMap = {
bigint: "TagBold",
bb_reference: {
user: "User",
users: "UserGroup",
},
}

View file

@ -1566,15 +1566,13 @@ describe.each([
() => ({
user: {
name: "user",
relationshipType: RelationshipType.ONE_TO_MANY,
type: FieldType.BB_REFERENCE,
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
},
users: {
name: "users",
type: FieldType.BB_REFERENCE,
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
relationshipType: RelationshipType.MANY_TO_MANY,
subtype: FieldTypeSubtypes.BB_REFERENCE.USERS,
},
}),
() => config.createUser(),

View file

@ -1,9 +1,16 @@
import { Knex, knex } from "knex"
import { Operation, QueryJson, RenameColumn, Table } from "@budibase/types"
import {
FieldSubtype,
Operation,
QueryJson,
RenameColumn,
Table,
} from "@budibase/types"
import { breakExternalTableId } from "../utils"
import SchemaBuilder = Knex.SchemaBuilder
import CreateTableBuilder = Knex.CreateTableBuilder
import { FieldTypes, RelationshipType } from "../../constants"
import { utils } from "@budibase/shared-core"
function generateSchema(
schema: CreateTableBuilder,
@ -41,9 +48,21 @@ function generateSchema(
case FieldTypes.OPTIONS:
case FieldTypes.LONGFORM:
case FieldTypes.BARCODEQR:
case FieldTypes.BB_REFERENCE:
schema.text(key)
break
case FieldTypes.BB_REFERENCE:
const subtype = column.subtype as FieldSubtype
switch (subtype) {
case FieldSubtype.USER:
schema.text(key)
break
case FieldSubtype.USERS:
schema.json(key)
break
default:
throw utils.unreachable(subtype)
}
break
case FieldTypes.NUMBER:
// if meta is specified then this is a junction table entry
if (column.meta && column.meta.toKey && column.meta.toTable) {

View file

@ -8,7 +8,7 @@ const ROW_PREFIX = DocumentType.ROW + SEPARATOR
export async function processInputBBReferences(
value: string | string[] | { _id: string } | { _id: string }[],
subtype: FieldSubtype
): Promise<string | null> {
): Promise<string | string[] | null> {
let referenceIds: string[] = []
if (Array.isArray(value)) {
@ -41,33 +41,39 @@ export async function processInputBBReferences(
switch (subtype) {
case FieldSubtype.USER:
case FieldSubtype.USERS:
const { notFoundIds } = await cache.user.getUsers(referenceIds)
if (notFoundIds?.length) {
throw new InvalidBBRefError(notFoundIds[0], FieldSubtype.USER)
}
break
if (subtype === FieldSubtype.USERS) {
return referenceIds
}
return referenceIds.join(",") || null
default:
throw utils.unreachable(subtype)
}
return referenceIds.join(",") || null
}
export async function processOutputBBReferences(
value: string,
value: string | string[],
subtype: FieldSubtype
) {
if (typeof value !== "string") {
if (value === null || value === undefined) {
// Already processed or nothing to process
return value || undefined
}
const ids = value.split(",").filter(id => !!id)
const ids =
typeof value === "string" ? value.split(",").filter(id => !!id) : value
switch (subtype) {
case FieldSubtype.USER:
case FieldSubtype.USERS:
const { users } = await cache.user.getUsers(ids)
if (!users.length) {
return undefined

View file

@ -6,6 +6,7 @@ import {
SearchFilter,
SearchQuery,
SearchQueryFields,
FieldSubtype,
} from "@budibase/types"
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
import { deepGet } from "./helpers"
@ -16,7 +17,7 @@ const HBS_REGEX = /{{([^{].*?)}}/g
* Returns the valid operator options for a certain data type
*/
export const getValidOperatorsForType = (
type: FieldType,
fieldType: { type: FieldType; subtype?: FieldSubtype },
field: string,
datasource: Datasource & { tableId: any } // TODO: is this table id ever populated?
) => {
@ -43,6 +44,7 @@ export const getValidOperatorsForType = (
value: string
label: string
}[] = []
const { type, subtype } = fieldType
if (type === FieldType.STRING) {
ops = stringOps
} else if (type === FieldType.NUMBER || type === FieldType.BIGINT) {
@ -59,8 +61,10 @@ export const getValidOperatorsForType = (
ops = numOps
} else if (type === FieldType.FORMULA) {
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
} else if (type === FieldType.BB_REFERENCE) {
} else if (type === FieldType.BB_REFERENCE && subtype == FieldSubtype.USER) {
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
} else if (type === FieldType.BB_REFERENCE && subtype == FieldSubtype.USERS) {
ops = [Op.Contains, Op.NotContains, Op.ContainsAny, Op.Empty, Op.NotEmpty]
}
// Only allow equal/not equal for _id in SQL tables

View file

@ -37,10 +37,12 @@ export interface Row extends Document {
export enum FieldSubtype {
USER = "user",
USERS = "users",
}
export const FieldTypeSubtypes = {
BB_REFERENCE: {
USER: FieldSubtype.USER,
USERS: FieldSubtype.USERS,
},
}