Frontend component of updating the API and accounting for default value not being in the paginated results of the relationship picker.
This commit is contained in:
parent
6bbce23910
commit
16d551542e
|
@ -14,13 +14,14 @@ import {
|
||||||
} from "../db"
|
} from "../db"
|
||||||
import {
|
import {
|
||||||
BulkDocsResponse,
|
BulkDocsResponse,
|
||||||
|
ContextUser,
|
||||||
|
SearchQuery,
|
||||||
|
SearchQueryOperators,
|
||||||
SearchUsersRequest,
|
SearchUsersRequest,
|
||||||
User,
|
User,
|
||||||
ContextUser,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { getGlobalDB } from "../context"
|
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
import { user as userCache } from "../cache"
|
import { getGlobalDB } from "../context"
|
||||||
|
|
||||||
type GetOpts = { cleanup?: boolean }
|
type GetOpts = { cleanup?: boolean }
|
||||||
|
|
||||||
|
@ -39,6 +40,27 @@ function removeUserPassword(users: User | User[]) {
|
||||||
return users
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isSupportedUserSearch = (query: SearchQuery) => {
|
||||||
|
const allowed = [
|
||||||
|
{ op: SearchQueryOperators.STRING, key: "email" },
|
||||||
|
{ op: SearchQueryOperators.EQUAL, key: "_id" },
|
||||||
|
]
|
||||||
|
for (let [key, operation] of Object.entries(query)) {
|
||||||
|
if (typeof operation !== "object") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const fields = Object.keys(operation || {})
|
||||||
|
const allowedOperation = allowed.find(
|
||||||
|
allow =>
|
||||||
|
allow.op === key && fields.length === 1 && fields[0] === allow.key
|
||||||
|
)
|
||||||
|
if (!allowedOperation && fields.length > 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
export const bulkGetGlobalUsersById = async (
|
export const bulkGetGlobalUsersById = async (
|
||||||
userIds: string[],
|
userIds: string[],
|
||||||
opts?: GetOpts
|
opts?: GetOpts
|
||||||
|
@ -211,8 +233,8 @@ export const searchGlobalUsersByEmail = async (
|
||||||
|
|
||||||
const PAGE_LIMIT = 8
|
const PAGE_LIMIT = 8
|
||||||
export const paginatedUsers = async ({
|
export const paginatedUsers = async ({
|
||||||
page,
|
bookmark,
|
||||||
email,
|
query,
|
||||||
appId,
|
appId,
|
||||||
}: SearchUsersRequest = {}) => {
|
}: SearchUsersRequest = {}) => {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
|
@ -222,18 +244,20 @@ export const paginatedUsers = async ({
|
||||||
limit: PAGE_LIMIT + 1,
|
limit: PAGE_LIMIT + 1,
|
||||||
}
|
}
|
||||||
// add a startkey if the page was specified (anchor)
|
// add a startkey if the page was specified (anchor)
|
||||||
if (page) {
|
if (bookmark) {
|
||||||
opts.startkey = page
|
opts.startkey = bookmark
|
||||||
}
|
}
|
||||||
// property specifies what to use for the page/anchor
|
// property specifies what to use for the page/anchor
|
||||||
let userList: User[],
|
let userList: User[],
|
||||||
property = "_id",
|
property = "_id",
|
||||||
getKey
|
getKey
|
||||||
if (appId) {
|
if (query?.equal?._id) {
|
||||||
|
userList = [await getById(query.equal._id)]
|
||||||
|
} else if (appId) {
|
||||||
userList = await searchGlobalUsersByApp(appId, opts)
|
userList = await searchGlobalUsersByApp(appId, opts)
|
||||||
getKey = (doc: any) => getGlobalUserByAppPage(appId, doc)
|
getKey = (doc: any) => getGlobalUserByAppPage(appId, doc)
|
||||||
} else if (email) {
|
} else if (query?.string?.email) {
|
||||||
userList = await searchGlobalUsersByEmail(email, opts)
|
userList = await searchGlobalUsersByEmail(query?.string?.email, opts)
|
||||||
property = "email"
|
property = "email"
|
||||||
} else {
|
} else {
|
||||||
// no search, query allDocs
|
// no search, query allDocs
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
import { FieldTypes } from "../../../constants"
|
import { FieldTypes } from "../../../constants"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
const { API } = getContext("sdk")
|
const { API } = getContext("sdk")
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
let tableDefinition
|
let tableDefinition
|
||||||
let searchTerm
|
let searchTerm
|
||||||
let open
|
let open
|
||||||
|
let hasFetchedDefault, fetchedDefault
|
||||||
|
|
||||||
$: type =
|
$: type =
|
||||||
datasourceType === "table" ? FieldTypes.LINK : FieldTypes.BB_REFERENCE
|
datasourceType === "table" ? FieldTypes.LINK : FieldTypes.BB_REFERENCE
|
||||||
|
@ -75,8 +77,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: enrichedOptions = enrichOptions(optionsObj, $fetch.rows)
|
$: enrichedOptions = enrichOptions(optionsObj, $fetch.rows, fetchedDefault)
|
||||||
const enrichOptions = (optionsObj, fetchResults) => {
|
const enrichOptions = (optionsObj, fetchResults, fetchedDefault) => {
|
||||||
const result = (fetchResults || [])?.reduce((accumulator, row) => {
|
const result = (fetchResults || [])?.reduce((accumulator, row) => {
|
||||||
if (!accumulator[row._id]) {
|
if (!accumulator[row._id]) {
|
||||||
accumulator[row._id] = row
|
accumulator[row._id] = row
|
||||||
|
@ -84,7 +86,11 @@
|
||||||
return accumulator
|
return accumulator
|
||||||
}, optionsObj)
|
}, optionsObj)
|
||||||
|
|
||||||
return Object.values(result)
|
const final = Object.values(result)
|
||||||
|
if (fetchedDefault && !final.find(row => row._id === fetchedDefault._id)) {
|
||||||
|
final.push(fetchedDefault)
|
||||||
|
}
|
||||||
|
return final
|
||||||
}
|
}
|
||||||
$: {
|
$: {
|
||||||
// We don't want to reorder while the dropdown is open, to avoid UX jumps
|
// We don't want to reorder while the dropdown is open, to avoid UX jumps
|
||||||
|
@ -105,16 +111,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: fetchRows(searchTerm, primaryDisplay)
|
$: fetchRows(searchTerm, primaryDisplay, hasFetchedDefault)
|
||||||
|
|
||||||
const fetchRows = (searchTerm, primaryDisplay) => {
|
const fetchRows = async (searchTerm, primaryDisplay, gotDefault) => {
|
||||||
const allRowsFetched =
|
const allRowsFetched =
|
||||||
$fetch.loaded &&
|
$fetch.loaded &&
|
||||||
!Object.keys($fetch.query?.string || {}).length &&
|
!Object.keys($fetch.query?.string || {}).length &&
|
||||||
!$fetch.hasNextPage
|
!$fetch.hasNextPage
|
||||||
// Don't request until we have the primary display
|
const shouldFetch = !defaultValue ? !allRowsFetched : gotDefault
|
||||||
if (!allRowsFetched && primaryDisplay) {
|
// Don't request until we have the primary display or default value has been fetched
|
||||||
fetch.update({
|
if (shouldFetch && primaryDisplay) {
|
||||||
|
await fetch.update({
|
||||||
query: { string: { [primaryDisplay]: searchTerm } },
|
query: { string: { [primaryDisplay]: searchTerm } },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -171,6 +178,20 @@
|
||||||
fetch.nextPage()
|
fetch.nextPage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
// the pagination might not include the default row
|
||||||
|
if (defaultValue) {
|
||||||
|
await fetch.update({
|
||||||
|
query: { equal: { _id: defaultValue }}
|
||||||
|
})
|
||||||
|
const fetched = $fetch.rows?.[0]
|
||||||
|
if (fetched) {
|
||||||
|
fetchedDefault = { ...fetched }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasFetchedDefault = true
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
|
|
|
@ -33,7 +33,6 @@ export const buildUserEndpoints = API => ({
|
||||||
if (limit) {
|
if (limit) {
|
||||||
opts.limit = limit
|
opts.limit = limit
|
||||||
}
|
}
|
||||||
console.log(opts)
|
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: `/api/global/users/search`,
|
url: `/api/global/users/search`,
|
||||||
body: opts,
|
body: opts,
|
||||||
|
|
|
@ -32,9 +32,9 @@ export default class UserFetch extends DataFetch {
|
||||||
const { cursor, query } = get(this.store)
|
const { cursor, query } = get(this.store)
|
||||||
let finalQuery
|
let finalQuery
|
||||||
// convert old format to new one - we now allow use of the lucene format
|
// convert old format to new one - we now allow use of the lucene format
|
||||||
const { appId, paginated, ...rest} = query
|
const { appId, paginated, ...rest } = query
|
||||||
if (!LuceneUtils.isLuceneFilter(query) && rest.email) {
|
if (!LuceneUtils.hasFilters(query) && rest.email) {
|
||||||
finalQuery = { string: { email: rest.email }}
|
finalQuery = { string: { email: rest.email } }
|
||||||
} else {
|
} else {
|
||||||
finalQuery = rest
|
finalQuery = rest
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,8 @@ import {
|
||||||
QuotaUsageType,
|
QuotaUsageType,
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
Row,
|
Row,
|
||||||
SaveTableRequest, SearchQueryOperators,
|
SaveTableRequest,
|
||||||
|
SearchQueryOperators,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
SortType,
|
SortType,
|
||||||
StaticQuotaName,
|
StaticQuotaName,
|
||||||
|
@ -1141,7 +1142,9 @@ describe.each([
|
||||||
)
|
)
|
||||||
|
|
||||||
const createViewResponse = await config.createView({
|
const createViewResponse = await config.createView({
|
||||||
query: [{ operator: SearchQueryOperators.EQUAL, field: "age", value: 40 }],
|
query: [
|
||||||
|
{ operator: SearchQueryOperators.EQUAL, field: "age", value: 40 },
|
||||||
|
],
|
||||||
schema: viewSchema,
|
schema: viewSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ import {
|
||||||
UpdateViewRequest,
|
UpdateViewRequest,
|
||||||
ViewV2,
|
ViewV2,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {generator} from "@budibase/backend-core/tests"
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
import {generateDatasourceID} from "../../../db/utils"
|
import { generateDatasourceID } from "../../../db/utils"
|
||||||
|
|
||||||
function priceTable(): Table {
|
function priceTable(): Table {
|
||||||
return {
|
return {
|
||||||
|
@ -90,7 +90,13 @@ describe.each([
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
primaryDisplay: generator.word(),
|
primaryDisplay: generator.word(),
|
||||||
query: [{ operator: SearchQueryOperators.EQUAL, field: "field", value: "value" }],
|
query: [
|
||||||
|
{
|
||||||
|
operator: SearchQueryOperators.EQUAL,
|
||||||
|
field: "field",
|
||||||
|
value: "value",
|
||||||
|
},
|
||||||
|
],
|
||||||
sort: {
|
sort: {
|
||||||
field: "fieldToSort",
|
field: "fieldToSort",
|
||||||
order: SortOrder.DESCENDING,
|
order: SortOrder.DESCENDING,
|
||||||
|
@ -185,7 +191,13 @@ describe.each([
|
||||||
const tableId = table._id!
|
const tableId = table._id!
|
||||||
await config.api.viewV2.update({
|
await config.api.viewV2.update({
|
||||||
...view,
|
...view,
|
||||||
query: [{ operator: SearchQueryOperators.EQUAL, field: "newField", value: "thatValue" }],
|
query: [
|
||||||
|
{
|
||||||
|
operator: SearchQueryOperators.EQUAL,
|
||||||
|
field: "newField",
|
||||||
|
value: "thatValue",
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect((await config.api.table.get(tableId)).views).toEqual({
|
expect((await config.api.table.get(tableId)).views).toEqual({
|
||||||
|
@ -280,7 +292,13 @@ describe.each([
|
||||||
{
|
{
|
||||||
...view,
|
...view,
|
||||||
tableId: generator.guid(),
|
tableId: generator.guid(),
|
||||||
query: [{ operator: SearchQueryOperators.EQUAL, field: "newField", value: "thatValue" }],
|
query: [
|
||||||
|
{
|
||||||
|
operator: SearchQueryOperators.EQUAL,
|
||||||
|
field: "newField",
|
||||||
|
value: "thatValue",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ expectStatus: 404 }
|
{ expectStatus: 404 }
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,8 +9,8 @@ import {
|
||||||
SortDirection,
|
SortDirection,
|
||||||
SortType,
|
SortType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {OperatorOptions, SqlNumberTypeRangeMap} from "./constants"
|
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
|
||||||
import {deepGet} from "./helpers"
|
import { deepGet } from "./helpers"
|
||||||
|
|
||||||
const HBS_REGEX = /{{([^{].*?)}}/g
|
const HBS_REGEX = /{{([^{].*?)}}/g
|
||||||
|
|
||||||
|
@ -239,18 +239,6 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
// type unknown
|
|
||||||
export const isLuceneFilter = (search: any) => {
|
|
||||||
if (typeof search !== "object") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const operators = Object.values(SearchQueryOperators) as string[]
|
|
||||||
const anySearchKey = Object.keys(search).find(key => {
|
|
||||||
return operators.includes(key) && typeof search[key] === "object"
|
|
||||||
})
|
|
||||||
return !!anySearchKey
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a client-side lucene search on an array of data
|
* Performs a client-side lucene search on an array of data
|
||||||
* @param docs the data
|
* @param docs the data
|
||||||
|
@ -286,18 +274,26 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process a string match (fails if the value does not start with the string)
|
// Process a string match (fails if the value does not start with the string)
|
||||||
const stringMatch = match(SearchQueryOperators.STRING, (docValue: string, testValue: string) => {
|
const stringMatch = match(
|
||||||
|
SearchQueryOperators.STRING,
|
||||||
|
(docValue: string, testValue: string) => {
|
||||||
return (
|
return (
|
||||||
!docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
|
!docValue ||
|
||||||
|
!docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
// Process a fuzzy match (treat the same as starts with when running locally)
|
// Process a fuzzy match (treat the same as starts with when running locally)
|
||||||
const fuzzyMatch = match(SearchQueryOperators.FUZZY, (docValue: string, testValue: string) => {
|
const fuzzyMatch = match(
|
||||||
|
SearchQueryOperators.FUZZY,
|
||||||
|
(docValue: string, testValue: string) => {
|
||||||
return (
|
return (
|
||||||
!docValue || !docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
|
!docValue ||
|
||||||
|
!docValue?.toLowerCase().startsWith(testValue?.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
// Process a range match
|
// Process a range match
|
||||||
const rangeMatch = match(
|
const rangeMatch = match(
|
||||||
|
@ -332,17 +328,25 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Process an empty match (fails if the value is not empty)
|
// Process an empty match (fails if the value is not empty)
|
||||||
const emptyMatch = match(SearchQueryOperators.EMPTY, (docValue: string | null) => {
|
const emptyMatch = match(
|
||||||
|
SearchQueryOperators.EMPTY,
|
||||||
|
(docValue: string | null) => {
|
||||||
return docValue != null && docValue !== ""
|
return docValue != null && docValue !== ""
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Process a not-empty match (fails is the value is empty)
|
// Process a not-empty match (fails is the value is empty)
|
||||||
const notEmptyMatch = match(SearchQueryOperators.NOT_EMPTY, (docValue: string | null) => {
|
const notEmptyMatch = match(
|
||||||
|
SearchQueryOperators.NOT_EMPTY,
|
||||||
|
(docValue: string | null) => {
|
||||||
return docValue == null || docValue === ""
|
return docValue == null || docValue === ""
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Process an includes match (fails if the value is not included)
|
// Process an includes match (fails if the value is not included)
|
||||||
const oneOf = match(SearchQueryOperators.ONE_OF, (docValue: any, testValue: any) => {
|
const oneOf = match(
|
||||||
|
SearchQueryOperators.ONE_OF,
|
||||||
|
(docValue: any, testValue: any) => {
|
||||||
if (typeof testValue === "string") {
|
if (typeof testValue === "string") {
|
||||||
testValue = testValue.split(",")
|
testValue = testValue.split(",")
|
||||||
if (typeof docValue === "number") {
|
if (typeof docValue === "number") {
|
||||||
|
@ -350,11 +354,15 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return !testValue?.includes(docValue)
|
return !testValue?.includes(docValue)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const containsAny = match(SearchQueryOperators.CONTAINS_ANY, (docValue: any, testValue: any) => {
|
const containsAny = match(
|
||||||
|
SearchQueryOperators.CONTAINS_ANY,
|
||||||
|
(docValue: any, testValue: any) => {
|
||||||
return !docValue?.includes(...testValue)
|
return !docValue?.includes(...testValue)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const contains = match(
|
const contains = match(
|
||||||
SearchQueryOperators.CONTAINS,
|
SearchQueryOperators.CONTAINS,
|
||||||
|
@ -446,7 +454,7 @@ export const hasFilters = (query?: SearchQuery) => {
|
||||||
if (skipped.includes(key) || typeof value !== "object") {
|
if (skipped.includes(key) || typeof value !== "object") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (Object.keys(value).length !== 0) {
|
if (Object.keys(value || {}).length !== 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { User } from "../../documents"
|
import { User } from "../../documents"
|
||||||
|
import { SearchQuery } from "./searchFilter"
|
||||||
|
|
||||||
export interface SaveUserResponse {
|
export interface SaveUserResponse {
|
||||||
_id: string
|
_id: string
|
||||||
|
@ -51,10 +52,10 @@ export interface InviteUsersResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchUsersRequest {
|
export interface SearchUsersRequest {
|
||||||
page?: string
|
bookmark?: string
|
||||||
email?: string
|
query?: SearchQuery
|
||||||
appId?: string
|
appId?: string
|
||||||
paginated?: boolean
|
paginate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateAdminUserRequest {
|
export interface CreateAdminUserRequest {
|
||||||
|
|
|
@ -197,7 +197,12 @@ export const getAppUsers = async (ctx: Ctx<SearchUsersRequest>) => {
|
||||||
export const search = async (ctx: Ctx<SearchUsersRequest>) => {
|
export const search = async (ctx: Ctx<SearchUsersRequest>) => {
|
||||||
const body = ctx.request.body
|
const body = ctx.request.body
|
||||||
|
|
||||||
if (body.paginated === false) {
|
// TODO: for now only one supported search key, string.email
|
||||||
|
if (body?.query && !userSdk.core.isSupportedUserSearch(body.query)) {
|
||||||
|
ctx.throw(501, "Can only search by string.email or equal._id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.paginate === false) {
|
||||||
await getAppUsers(ctx)
|
await getAppUsers(ctx)
|
||||||
} else {
|
} else {
|
||||||
const paginated = await userSdk.core.paginatedUsers(body)
|
const paginated = await userSdk.core.paginatedUsers(body)
|
||||||
|
|
Loading…
Reference in New Issue