Merge branch 'develop' of github.com:Budibase/budibase into data-section-multidev
This commit is contained in:
commit
1e48020001
|
@ -55,7 +55,7 @@ http {
|
||||||
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
|
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
|
||||||
set $csp_object "object-src 'none'";
|
set $csp_object "object-src 'none'";
|
||||||
set $csp_base_uri "base-uri 'self'";
|
set $csp_base_uri "base-uri 'self'";
|
||||||
set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com";
|
set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com";
|
||||||
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
|
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
|
||||||
set $csp_frame "frame-src 'self' https:";
|
set $csp_frame "frame-src 'self' https:";
|
||||||
set $csp_img "img-src http: https: data: blob:";
|
set $csp_img "img-src http: https: data: blob:";
|
||||||
|
@ -82,6 +82,12 @@ http {
|
||||||
set $couchdb ${COUCHDB_UPSTREAM_URL};
|
set $couchdb ${COUCHDB_UPSTREAM_URL};
|
||||||
set $watchtower ${WATCHTOWER_UPSTREAM_URL};
|
set $watchtower ${WATCHTOWER_UPSTREAM_URL};
|
||||||
|
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
add_header 'Content-Type' 'application/json';
|
||||||
|
return 200 '{ "status": "OK" }';
|
||||||
|
}
|
||||||
|
|
||||||
location /app {
|
location /app {
|
||||||
proxy_pass $apps;
|
proxy_pass $apps;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.6.8-alpha.9",
|
"version": "2.6.16-alpha.0",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/backend-core",
|
"packages/backend-core",
|
||||||
|
|
|
@ -21,7 +21,7 @@ export enum ViewName {
|
||||||
AUTOMATION_LOGS = "automation_logs",
|
AUTOMATION_LOGS = "automation_logs",
|
||||||
ACCOUNT_BY_EMAIL = "account_by_email",
|
ACCOUNT_BY_EMAIL = "account_by_email",
|
||||||
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
|
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
|
||||||
USER_BY_GROUP = "by_group_user",
|
USER_BY_GROUP = "user_by_group",
|
||||||
APP_BACKUP_BY_TRIGGER = "by_trigger",
|
APP_BACKUP_BY_TRIGGER = "by_trigger",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,10 +69,10 @@ function findVersion() {
|
||||||
try {
|
try {
|
||||||
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
||||||
const content = readFileSync(packageJsonFile!, "utf-8")
|
const content = readFileSync(packageJsonFile!, "utf-8")
|
||||||
const version = JSON.parse(content).version
|
return JSON.parse(content).version
|
||||||
return version
|
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error("Cannot find a valid version in its package.json")
|
// throwing an error here is confusing/causes backend-core to be hard to import
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ const environment = {
|
||||||
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||||
REDIS_URL: process.env.REDIS_URL || "localhost:6379",
|
REDIS_URL: process.env.REDIS_URL || "localhost:6379",
|
||||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD || "budibase",
|
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||||
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
|
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
|
||||||
MOCK_REDIS: process.env.MOCK_REDIS,
|
MOCK_REDIS: process.env.MOCK_REDIS,
|
||||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import * as db from "../../db"
|
||||||
import { Header } from "../../constants"
|
import { Header } from "../../constants"
|
||||||
import { newid } from "../../utils"
|
import { newid } from "../../utils"
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
|
import { BBContext } from "@budibase/types"
|
||||||
|
|
||||||
describe("utils", () => {
|
describe("utils", () => {
|
||||||
const config = new DBTestConfiguration()
|
const config = new DBTestConfiguration()
|
||||||
|
@ -106,4 +107,85 @@ describe("utils", () => {
|
||||||
expect(actual).toBe(undefined)
|
expect(actual).toBe(undefined)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("isServingBuilder", () => {
|
||||||
|
let ctx: BBContext
|
||||||
|
|
||||||
|
const expectResult = (result: boolean) =>
|
||||||
|
expect(utils.isServingBuilder(ctx)).toBe(result)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ctx = structures.koa.newContext()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns true if current path is in builder", async () => {
|
||||||
|
ctx.path = "/builder/app/app_"
|
||||||
|
expectResult(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false if current path doesn't have '/' suffix", async () => {
|
||||||
|
ctx.path = "/builder/app"
|
||||||
|
expectResult(false)
|
||||||
|
|
||||||
|
ctx.path = "/xx"
|
||||||
|
expectResult(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("isServingBuilderPreview", () => {
|
||||||
|
let ctx: BBContext
|
||||||
|
|
||||||
|
const expectResult = (result: boolean) =>
|
||||||
|
expect(utils.isServingBuilderPreview(ctx)).toBe(result)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ctx = structures.koa.newContext()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns true if current path is in builder preview", async () => {
|
||||||
|
ctx.path = "/app/preview/xx"
|
||||||
|
expectResult(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false if current path is not in builder preview", async () => {
|
||||||
|
ctx.path = "/builder"
|
||||||
|
expectResult(false)
|
||||||
|
|
||||||
|
ctx.path = "/xx"
|
||||||
|
expectResult(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("isPublicAPIRequest", () => {
|
||||||
|
let ctx: BBContext
|
||||||
|
|
||||||
|
const expectResult = (result: boolean) =>
|
||||||
|
expect(utils.isPublicApiRequest(ctx)).toBe(result)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ctx = structures.koa.newContext()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns true if current path remains to public API", async () => {
|
||||||
|
ctx.path = "/api/public/v1/invoices"
|
||||||
|
expectResult(true)
|
||||||
|
|
||||||
|
ctx.path = "/api/public/v1"
|
||||||
|
expectResult(true)
|
||||||
|
|
||||||
|
ctx.path = "/api/public/v2"
|
||||||
|
expectResult(true)
|
||||||
|
|
||||||
|
ctx.path = "/api/public/v21"
|
||||||
|
expectResult(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false if current path doesn't remain to public API", async () => {
|
||||||
|
ctx.path = "/api/public"
|
||||||
|
expectResult(false)
|
||||||
|
|
||||||
|
ctx.path = "/xx"
|
||||||
|
expectResult(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
import { getAllApps, queryGlobalView } from "../db"
|
import { getAllApps } from "../db"
|
||||||
import {
|
import { Header, MAX_VALID_DATE, DocumentType, SEPARATOR } from "../constants"
|
||||||
Header,
|
|
||||||
MAX_VALID_DATE,
|
|
||||||
DocumentType,
|
|
||||||
SEPARATOR,
|
|
||||||
ViewName,
|
|
||||||
} from "../constants"
|
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import * as tenancy from "../tenancy"
|
import * as tenancy from "../tenancy"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
|
@ -23,7 +17,9 @@ const APP_PREFIX = DocumentType.APP + SEPARATOR
|
||||||
const PROD_APP_PREFIX = "/app/"
|
const PROD_APP_PREFIX = "/app/"
|
||||||
|
|
||||||
const BUILDER_PREVIEW_PATH = "/app/preview"
|
const BUILDER_PREVIEW_PATH = "/app/preview"
|
||||||
const BUILDER_REFERER_PREFIX = "/builder/app/"
|
const BUILDER_PREFIX = "/builder"
|
||||||
|
const BUILDER_APP_PREFIX = `${BUILDER_PREFIX}/app/`
|
||||||
|
const PUBLIC_API_PREFIX = "/api/public/v"
|
||||||
|
|
||||||
function confirmAppId(possibleAppId: string | undefined) {
|
function confirmAppId(possibleAppId: string | undefined) {
|
||||||
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
||||||
|
@ -69,6 +65,18 @@ export function isServingApp(ctx: Ctx) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isServingBuilder(ctx: Ctx): boolean {
|
||||||
|
return ctx.path.startsWith(BUILDER_APP_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isServingBuilderPreview(ctx: Ctx): boolean {
|
||||||
|
return ctx.path.startsWith(BUILDER_PREVIEW_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPublicApiRequest(ctx: Ctx): boolean {
|
||||||
|
return ctx.path.startsWith(PUBLIC_API_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a request tries to find the appId, which can be located in various places
|
* Given a request tries to find the appId, which can be located in various places
|
||||||
* @param {object} ctx The main request body to look through.
|
* @param {object} ctx The main request body to look through.
|
||||||
|
@ -110,7 +118,7 @@ export async function getAppIdFromCtx(ctx: Ctx) {
|
||||||
// make sure this is performed after prod app url resolution, in case the
|
// make sure this is performed after prod app url resolution, in case the
|
||||||
// referer header is present from a builder redirect
|
// referer header is present from a builder redirect
|
||||||
const referer = ctx.request.headers.referer
|
const referer = ctx.request.headers.referer
|
||||||
if (!appId && referer?.includes(BUILDER_REFERER_PREFIX)) {
|
if (!appId && referer?.includes(BUILDER_APP_PREFIX)) {
|
||||||
const refererId = parseAppIdFromUrl(ctx.request.headers.referer)
|
const refererId = parseAppIdFromUrl(ctx.request.headers.referer)
|
||||||
appId = confirmAppId(refererId)
|
appId = confirmAppId(refererId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
export let rowCount
|
export let rowCount
|
||||||
export let disableSorting = false
|
export let disableSorting = false
|
||||||
export let customPlaceholder = false
|
export let customPlaceholder = false
|
||||||
|
export let allowClickRows
|
||||||
|
export let allowEditing = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -109,7 +111,9 @@
|
||||||
{rowCount}
|
{rowCount}
|
||||||
{disableSorting}
|
{disableSorting}
|
||||||
{customPlaceholder}
|
{customPlaceholder}
|
||||||
|
allowEditRows={allowEditing}
|
||||||
showAutoColumns={!hideAutocolumns}
|
showAutoColumns={!hideAutocolumns}
|
||||||
|
{allowClickRows}
|
||||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||||
on:sort
|
on:sort
|
||||||
>
|
>
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
{loading}
|
{loading}
|
||||||
{type}
|
{type}
|
||||||
rowCount={10}
|
rowCount={10}
|
||||||
|
allowEditing={false}
|
||||||
bind:hideAutocolumns
|
bind:hideAutocolumns
|
||||||
>
|
>
|
||||||
<ViewFilterButton {view} />
|
<ViewFilterButton {view} />
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import { goto, isActive } from "@roxi/routify"
|
import { goto, isActive } from "@roxi/routify"
|
||||||
|
|
||||||
const alphabetical = (a, b) => a.name?.toLowerCase() > b.name?.toLowerCase()
|
const alphabetical = (a, b) =>
|
||||||
|
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||||
|
|
||||||
export let sourceId
|
export let sourceId
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
export let highlighted = false
|
export let highlighted = false
|
||||||
export let rightAlignIcon = false
|
export let rightAlignIcon = false
|
||||||
export let id
|
export let id
|
||||||
|
export let showTooltip = false
|
||||||
|
|
||||||
const scrollApi = getContext("scroll")
|
const scrollApi = getContext("scroll")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -84,7 +85,7 @@
|
||||||
<Icon color={iconColor} size="S" name={icon} />
|
<Icon color={iconColor} size="S" name={icon} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text">{text}</div>
|
<div class="text" title={showTooltip ? text : null}>{text}</div>
|
||||||
{#if withActions}
|
{#if withActions}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -28,13 +28,16 @@
|
||||||
let inviting = false
|
let inviting = false
|
||||||
let searchFocus = false
|
let searchFocus = false
|
||||||
|
|
||||||
|
// Initially filter entities without app access
|
||||||
|
// Show all when false
|
||||||
|
let filterByAppAccess = true
|
||||||
|
|
||||||
let appInvites = []
|
let appInvites = []
|
||||||
let filteredInvites = []
|
let filteredInvites = []
|
||||||
let filteredUsers = []
|
let filteredUsers = []
|
||||||
let filteredGroups = []
|
let filteredGroups = []
|
||||||
let selectedGroup
|
let selectedGroup
|
||||||
let userOnboardResponse = null
|
let userOnboardResponse = null
|
||||||
|
|
||||||
let userLimitReachedModal
|
let userLimitReachedModal
|
||||||
|
|
||||||
$: queryIsEmail = emailValidator(query) === true
|
$: queryIsEmail = emailValidator(query) === true
|
||||||
|
@ -52,15 +55,32 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterInvites = async query => {
|
const filterInvites = async query => {
|
||||||
appInvites = await getInvites()
|
if (!prodAppId) {
|
||||||
if (!query || query == "") {
|
|
||||||
filteredInvites = appInvites
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
filteredInvites = appInvites.filter(invite => invite.email.includes(query))
|
|
||||||
|
appInvites = await getInvites()
|
||||||
|
|
||||||
|
//On Focus behaviour
|
||||||
|
if (!filterByAppAccess && !query) {
|
||||||
|
filteredInvites =
|
||||||
|
appInvites.length > 100 ? appInvites.slice(0, 100) : [...appInvites]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredInvites = appInvites.filter(invite => {
|
||||||
|
const inviteInfo = invite.info?.apps
|
||||||
|
if (!query && inviteInfo && prodAppId) {
|
||||||
|
return Object.keys(inviteInfo).includes(prodAppId)
|
||||||
|
}
|
||||||
|
return invite.email.includes(query)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$: filterInvites(query)
|
$: filterByAppAccess, prodAppId, filterInvites(query)
|
||||||
|
$: if (searchFocus === true) {
|
||||||
|
filterByAppAccess = false
|
||||||
|
}
|
||||||
|
|
||||||
const usersFetch = fetchData({
|
const usersFetch = fetchData({
|
||||||
API,
|
API,
|
||||||
|
@ -79,9 +99,9 @@
|
||||||
}
|
}
|
||||||
await usersFetch.update({
|
await usersFetch.update({
|
||||||
query: {
|
query: {
|
||||||
appId: query ? null : prodAppId,
|
appId: query || !filterByAppAccess ? null : prodAppId,
|
||||||
email: query,
|
email: query,
|
||||||
paginated: query ? null : false,
|
paginated: query || !filterByAppAccess ? null : false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await usersFetch.refresh()
|
await usersFetch.refresh()
|
||||||
|
@ -107,7 +127,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const debouncedUpdateFetch = Utils.debounce(searchUsers, 250)
|
const debouncedUpdateFetch = Utils.debounce(searchUsers, 250)
|
||||||
$: debouncedUpdateFetch(query, $store.builderSidePanel, loaded)
|
$: debouncedUpdateFetch(
|
||||||
|
query,
|
||||||
|
$store.builderSidePanel,
|
||||||
|
loaded,
|
||||||
|
filterByAppAccess
|
||||||
|
)
|
||||||
|
|
||||||
const updateAppUser = async (user, role) => {
|
const updateAppUser = async (user, role) => {
|
||||||
if (!prodAppId) {
|
if (!prodAppId) {
|
||||||
|
@ -182,9 +207,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchGroups = (userGroups, query) => {
|
const searchGroups = (userGroups, query) => {
|
||||||
let filterGroups = query?.length
|
let filterGroups =
|
||||||
? userGroups
|
query?.length || !filterByAppAccess
|
||||||
: getAppGroups(userGroups, prodAppId)
|
? userGroups
|
||||||
|
: getAppGroups(userGroups, prodAppId)
|
||||||
return filterGroups
|
return filterGroups
|
||||||
.filter(group => {
|
.filter(group => {
|
||||||
if (!query?.length) {
|
if (!query?.length) {
|
||||||
|
@ -214,7 +240,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds the 'role' attribute and sets it to the current app.
|
// Adds the 'role' attribute and sets it to the current app.
|
||||||
$: enrichedGroups = getEnrichedGroups($groups)
|
$: enrichedGroups = getEnrichedGroups($groups, filterByAppAccess)
|
||||||
$: filteredGroups = searchGroups(enrichedGroups, query)
|
$: filteredGroups = searchGroups(enrichedGroups, query)
|
||||||
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
|
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
|
||||||
$: allUsers = [...filteredUsers, ...groupUsers]
|
$: allUsers = [...filteredUsers, ...groupUsers]
|
||||||
|
@ -226,7 +252,7 @@
|
||||||
specific roles for the app.
|
specific roles for the app.
|
||||||
*/
|
*/
|
||||||
const buildGroupUsers = (userGroups, filteredUsers) => {
|
const buildGroupUsers = (userGroups, filteredUsers) => {
|
||||||
if (query) {
|
if (query || !filterByAppAccess) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
// Must exclude users who have explicit privileges
|
// Must exclude users who have explicit privileges
|
||||||
|
@ -321,12 +347,12 @@
|
||||||
[prodAppId]: role,
|
[prodAppId]: role,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await filterInvites()
|
await filterInvites(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUninviteAppUser = async invite => {
|
const onUninviteAppUser = async invite => {
|
||||||
await uninviteAppUser(invite)
|
await uninviteAppUser(invite)
|
||||||
await filterInvites()
|
await filterInvites(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Purge only the app from the invite or recind the invite if only 1 app remains?
|
// Purge only the app from the invite or recind the invite if only 1 app remains?
|
||||||
|
@ -351,7 +377,6 @@
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
rendered = true
|
rendered = true
|
||||||
searchFocus = true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleKeyDown(evt) {
|
function handleKeyDown(evt) {
|
||||||
|
@ -417,7 +442,6 @@
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
disabled={inviting}
|
disabled={inviting}
|
||||||
value={query}
|
value={query}
|
||||||
autofocus
|
|
||||||
on:input={e => {
|
on:input={e => {
|
||||||
query = e.target.value.trim()
|
query = e.target.value.trim()
|
||||||
}}
|
}}
|
||||||
|
@ -428,16 +452,20 @@
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="search-input-icon"
|
class="search-input-icon"
|
||||||
class:searching={query}
|
class:searching={query || !filterByAppAccess}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
if (!filterByAppAccess) {
|
||||||
|
filterByAppAccess = true
|
||||||
|
}
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
query = null
|
query = null
|
||||||
userOnboardResponse = null
|
userOnboardResponse = null
|
||||||
|
filterByAppAccess = true
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name={query ? "Close" : "Search"} />
|
<Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -555,7 +583,7 @@
|
||||||
|
|
||||||
{#if filteredUsers?.length}
|
{#if filteredUsers?.length}
|
||||||
<div class="auth-entity-section">
|
<div class="auth-entity-section">
|
||||||
<div class="auth-entity-header ">
|
<div class="auth-entity-header">
|
||||||
<div class="auth-entity-title">Users</div>
|
<div class="auth-entity-title">Users</div>
|
||||||
<div class="auth-entity-access-title">Access</div>
|
<div class="auth-entity-access-title">Access</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -696,7 +724,7 @@
|
||||||
max-width: calc(100vw - 40px);
|
max-width: calc(100vw - 40px);
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
border-left: var(--border-light);
|
border-left: var(--border-light);
|
||||||
z-index: 3;
|
z-index: 999;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
|
@ -59,6 +59,7 @@
|
||||||
text={screen.routing.route}
|
text={screen.routing.route}
|
||||||
on:click={() => store.actions.screens.select(screen._id)}
|
on:click={() => store.actions.screens.select(screen._id)}
|
||||||
rightAlignIcon
|
rightAlignIcon
|
||||||
|
showTooltip
|
||||||
>
|
>
|
||||||
<ScreenDropdownMenu screenId={screen._id} />
|
<ScreenDropdownMenu screenId={screen._id} />
|
||||||
<RoleIndicator slot="right" roleId={screen.routing.roleId} />
|
<RoleIndicator slot="right" roleId={screen.routing.roleId} />
|
||||||
|
|
|
@ -107,8 +107,9 @@
|
||||||
useSampleData,
|
useSampleData,
|
||||||
isGoogle,
|
isGoogle,
|
||||||
}) => {
|
}) => {
|
||||||
|
let app
|
||||||
try {
|
try {
|
||||||
const app = await createApp(useSampleData)
|
app = await createApp(useSampleData)
|
||||||
|
|
||||||
let datasource
|
let datasource
|
||||||
if (datasourceConfig) {
|
if (datasourceConfig) {
|
||||||
|
@ -134,6 +135,17 @@
|
||||||
console.log(e)
|
console.log(e)
|
||||||
creationLoading = false
|
creationLoading = false
|
||||||
notifications.error("There was a problem creating your app")
|
notifications.error("There was a problem creating your app")
|
||||||
|
|
||||||
|
// Reset the store so that we don't send up stale headers
|
||||||
|
store.actions.reset()
|
||||||
|
|
||||||
|
// If we successfully created an app, delete it again so that we
|
||||||
|
// can try again once the error has been corrected.
|
||||||
|
// This also ensures onboarding can't be skipped by entering invalid
|
||||||
|
// data credentials.
|
||||||
|
if (app?.appId) {
|
||||||
|
await API.deleteApp(app.appId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -146,80 +158,87 @@
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<SplitPage>
|
<div class="full-width">
|
||||||
{#if stage === "name"}
|
<SplitPage>
|
||||||
<NamePanel bind:name bind:url onNext={() => (stage = "data")} />
|
{#if stage === "name"}
|
||||||
{:else if googleComplete}
|
<NamePanel bind:name bind:url onNext={() => (stage = "data")} />
|
||||||
<div class="centered">
|
{:else if googleComplete}
|
||||||
<Body
|
<div class="centered">
|
||||||
>Please login to your Google account in the new tab which as opened to
|
<Body
|
||||||
continue.</Body
|
>Please login to your Google account in the new tab which as opened to
|
||||||
>
|
continue.</Body
|
||||||
</div>
|
>
|
||||||
{:else if integrationsLoading || creationLoading}
|
|
||||||
<div class="centered">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
{:else if stage === "data"}
|
|
||||||
<DataPanel onBack={() => (stage = "name")}>
|
|
||||||
<div class="dataButton">
|
|
||||||
<FancyButton on:click={() => handleCreateApp({ useSampleData: true })}>
|
|
||||||
<div class="dataButtonContent">
|
|
||||||
<div class="dataButtonIcon">
|
|
||||||
<img
|
|
||||||
alt="Budibase Logo"
|
|
||||||
class="budibaseLogo"
|
|
||||||
src={"https://i.imgur.com/Xhdt1YP.png"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
Budibase Sample data
|
|
||||||
</div>
|
|
||||||
</FancyButton>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="dataButton">
|
{:else if integrationsLoading || creationLoading}
|
||||||
<FancyButton on:click={uploadModal.show}>
|
<div class="centered">
|
||||||
<div class="dataButtonContent">
|
<Spinner />
|
||||||
<div class="dataButtonIcon">
|
|
||||||
<FontAwesomeIcon name="fa-solid fa-file-arrow-up" />
|
|
||||||
</div>
|
|
||||||
Upload data (CSV or JSON)
|
|
||||||
</div>
|
|
||||||
</FancyButton>
|
|
||||||
</div>
|
</div>
|
||||||
{#each Object.entries(plusIntegrations) as [integrationType, schema]}
|
{:else if stage === "data"}
|
||||||
|
<DataPanel onBack={() => (stage = "name")}>
|
||||||
<div class="dataButton">
|
<div class="dataButton">
|
||||||
<FancyButton on:click={() => (stage = integrationType)}>
|
<FancyButton
|
||||||
|
on:click={() => handleCreateApp({ useSampleData: true })}
|
||||||
|
>
|
||||||
<div class="dataButtonContent">
|
<div class="dataButtonContent">
|
||||||
<div class="dataButtonIcon">
|
<div class="dataButtonIcon">
|
||||||
<IntegrationIcon {integrationType} {schema} />
|
<img
|
||||||
|
alt="Budibase Logo"
|
||||||
|
class="budibaseLogo"
|
||||||
|
src={"https://i.imgur.com/Xhdt1YP.png"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{schema.friendlyName}
|
Budibase Sample data
|
||||||
</div>
|
</div>
|
||||||
</FancyButton>
|
</FancyButton>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
<div class="dataButton">
|
||||||
</DataPanel>
|
<FancyButton on:click={uploadModal.show}>
|
||||||
{:else if stage in plusIntegrations}
|
<div class="dataButtonContent">
|
||||||
<DatasourceConfigPanel
|
<div class="dataButtonIcon">
|
||||||
title={plusIntegrations[stage].friendlyName}
|
<FontAwesomeIcon name="fa-solid fa-file-arrow-up" />
|
||||||
fields={plusIntegrations[stage].datasource}
|
</div>
|
||||||
type={stage}
|
Upload data (CSV or JSON)
|
||||||
onBack={() => (stage = "data")}
|
</div>
|
||||||
onNext={data => {
|
</FancyButton>
|
||||||
const isGoogle = data.isGoogle
|
</div>
|
||||||
delete data.isGoogle
|
{#each Object.entries(plusIntegrations) as [integrationType, schema]}
|
||||||
return handleCreateApp({ datasourceConfig: data, isGoogle })
|
<div class="dataButton">
|
||||||
}}
|
<FancyButton on:click={() => (stage = integrationType)}>
|
||||||
/>
|
<div class="dataButtonContent">
|
||||||
{:else}
|
<div class="dataButtonIcon">
|
||||||
<p>There was an problem. Please refresh the page and try again.</p>
|
<IntegrationIcon {integrationType} {schema} />
|
||||||
{/if}
|
</div>
|
||||||
<div slot="right">
|
{schema.friendlyName}
|
||||||
<ExampleApp {name} showData={stage !== "name"} />
|
</div>
|
||||||
</div>
|
</FancyButton>
|
||||||
</SplitPage>
|
</div>
|
||||||
|
{/each}
|
||||||
|
</DataPanel>
|
||||||
|
{:else if stage in plusIntegrations}
|
||||||
|
<DatasourceConfigPanel
|
||||||
|
title={plusIntegrations[stage].friendlyName}
|
||||||
|
fields={plusIntegrations[stage].datasource}
|
||||||
|
type={stage}
|
||||||
|
onBack={() => (stage = "data")}
|
||||||
|
onNext={data => {
|
||||||
|
const isGoogle = data.isGoogle
|
||||||
|
delete data.isGoogle
|
||||||
|
return handleCreateApp({ datasourceConfig: data, isGoogle })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<p>There was an problem. Please refresh the page and try again.</p>
|
||||||
|
{/if}
|
||||||
|
<div slot="right">
|
||||||
|
<ExampleApp {name} showData={stage !== "name"} />
|
||||||
|
</div>
|
||||||
|
</SplitPage>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.centered {
|
.centered {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import EditUserPicker from "./EditUserPicker.svelte"
|
import EditUserPicker from "./EditUserPicker.svelte"
|
||||||
|
|
||||||
import { Heading, Pagination, Table } from "@budibase/bbui"
|
import { Heading, Pagination, Table, Search } from "@budibase/bbui"
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
import { fetchData } from "@budibase/frontend-core"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -12,7 +12,9 @@
|
||||||
|
|
||||||
export let groupId
|
export let groupId
|
||||||
|
|
||||||
const fetchGroupUsers = fetchData({
|
let emailSearch
|
||||||
|
let fetchGroupUsers
|
||||||
|
$: fetchGroupUsers = fetchData({
|
||||||
API,
|
API,
|
||||||
datasource: {
|
datasource: {
|
||||||
type: "groupUser",
|
type: "groupUser",
|
||||||
|
@ -20,6 +22,7 @@
|
||||||
options: {
|
options: {
|
||||||
query: {
|
query: {
|
||||||
groupId,
|
groupId,
|
||||||
|
emailSearch,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -59,24 +62,31 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<Heading size="S">Users</Heading>
|
|
||||||
{#if !scimEnabled}
|
{#if !scimEnabled}
|
||||||
<EditUserPicker {groupId} onUsersUpdated={fetchGroupUsers.getInitialData} />
|
<EditUserPicker {groupId} onUsersUpdated={fetchGroupUsers.getInitialData} />
|
||||||
{:else}
|
{:else}
|
||||||
<ScimBanner />
|
<ScimBanner />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="controls-right">
|
||||||
|
<Search bind:value={emailSearch} placeholder="Search email" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Table
|
<Table
|
||||||
schema={userSchema}
|
schema={userSchema}
|
||||||
data={$fetchGroupUsers?.rows}
|
data={$fetchGroupUsers?.rows}
|
||||||
|
loading={$fetchGroupUsers.loading}
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
customPlaceholder
|
customPlaceholder
|
||||||
customRenderers={customUserTableRenderers}
|
customRenderers={customUserTableRenderers}
|
||||||
on:click={e => $goto(`../users/${e.detail._id}`)}
|
on:click={e => $goto(`../users/${e.detail._id}`)}
|
||||||
>
|
>
|
||||||
<div class="placeholder" slot="placeholder">
|
<div class="placeholder" slot="placeholder">
|
||||||
<Heading size="S">This user group doesn't have any users</Heading>
|
<Heading size="S"
|
||||||
|
>{emailSearch
|
||||||
|
? `No users found matching the email "${emailSearch}"`
|
||||||
|
: "This user group doesn't have any users"}</Heading
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
@ -98,7 +108,7 @@
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
@ -109,4 +119,15 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.controls-right :global(.spectrum-Search) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -88,6 +88,16 @@
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPendingSchema = tblSchema => {
|
||||||
|
if (!tblSchema) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
let pendingSchema = JSON.parse(JSON.stringify(tblSchema))
|
||||||
|
pendingSchema.email.displayName = "Pending Invites"
|
||||||
|
return pendingSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
$: pendingSchema = getPendingSchema(schema)
|
||||||
$: userData = []
|
$: userData = []
|
||||||
$: inviteUsersResponse = { successful: [], unsuccessful: [] }
|
$: inviteUsersResponse = { successful: [], unsuccessful: [] }
|
||||||
$: {
|
$: {
|
||||||
|
@ -110,6 +120,24 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
let invitesLoaded = false
|
||||||
|
let pendingInvites = []
|
||||||
|
let parsedInvites = []
|
||||||
|
|
||||||
|
const invitesToSchema = invites => {
|
||||||
|
return invites.map(invite => {
|
||||||
|
const { admin, builder, userGroups, apps } = invite.info
|
||||||
|
|
||||||
|
return {
|
||||||
|
email: invite.email,
|
||||||
|
builder,
|
||||||
|
admin,
|
||||||
|
userGroups: userGroups,
|
||||||
|
apps: apps ? [...new Set(Object.keys(apps))] : undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
$: parsedInvites = invitesToSchema(pendingInvites)
|
||||||
|
|
||||||
const updateFetch = email => {
|
const updateFetch = email => {
|
||||||
fetch.update({
|
fetch.update({
|
||||||
|
@ -144,6 +172,7 @@
|
||||||
}))
|
}))
|
||||||
try {
|
try {
|
||||||
inviteUsersResponse = await users.invite(payload)
|
inviteUsersResponse = await users.invite(payload)
|
||||||
|
pendingInvites = await users.getInvites()
|
||||||
inviteConfirmationModal.show()
|
inviteConfirmationModal.show()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error inviting user")
|
notifications.error("Error inviting user")
|
||||||
|
@ -232,6 +261,9 @@
|
||||||
try {
|
try {
|
||||||
await groups.actions.init()
|
await groups.actions.init()
|
||||||
groupsLoaded = true
|
groupsLoaded = true
|
||||||
|
|
||||||
|
pendingInvites = await users.getInvites()
|
||||||
|
invitesLoaded = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error fetching user group data")
|
notifications.error("Error fetching user group data")
|
||||||
}
|
}
|
||||||
|
@ -324,6 +356,15 @@
|
||||||
goToNextPage={fetch.nextPage}
|
goToNextPage={fetch.nextPage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Table
|
||||||
|
schema={pendingSchema}
|
||||||
|
data={parsedInvites}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
{customRenderers}
|
||||||
|
loading={!invitesLoaded}
|
||||||
|
allowClickRows={false}
|
||||||
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Modal bind:this={createUserModal}>
|
<Modal bind:this={createUserModal}>
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"budi": "dist/index.js"
|
"budi": "dist/src/index.js"
|
||||||
},
|
},
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
|
|
|
@ -2,17 +2,17 @@
|
||||||
process.env.DISABLE_PINO_LOGGER = "1"
|
process.env.DISABLE_PINO_LOGGER = "1"
|
||||||
import "./prebuilds"
|
import "./prebuilds"
|
||||||
import "./environment"
|
import "./environment"
|
||||||
import { env } from "@budibase/backend-core"
|
|
||||||
import { getCommands } from "./options"
|
import { getCommands } from "./options"
|
||||||
import { Command } from "commander"
|
import { Command } from "commander"
|
||||||
import { getHelpDescription } from "./utils"
|
import { getHelpDescription } from "./utils"
|
||||||
|
import { version } from "../package.json"
|
||||||
|
|
||||||
// add hosting config
|
// add hosting config
|
||||||
async function init() {
|
async function init() {
|
||||||
const program = new Command()
|
const program = new Command()
|
||||||
.addHelpCommand("help", getHelpDescription("Help with Budibase commands."))
|
.addHelpCommand("help", getHelpDescription("Help with Budibase commands."))
|
||||||
.helpOption(false)
|
.helpOption(false)
|
||||||
.version(env.VERSION)
|
.version(version)
|
||||||
// add commands
|
// add commands
|
||||||
for (let command of getCommands()) {
|
for (let command of getCommands()) {
|
||||||
command.configure(program)
|
command.configure(program)
|
||||||
|
|
|
@ -13,7 +13,7 @@ if (!process.argv[0].includes("node")) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForBinaries() {
|
function checkForBinaries() {
|
||||||
const readDir = join(__filename, "..", "..", PREBUILDS, ARCH)
|
const readDir = join(__filename, "..", "..", "..", PREBUILDS, ARCH)
|
||||||
if (fs.existsSync(PREBUILD_DIR) || !fs.existsSync(readDir)) {
|
if (fs.existsSync(PREBUILD_DIR) || !fs.existsSync(readDir)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"resolveJsonModule": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@budibase/types": ["../types/src"],
|
"@budibase/types": ["../types/src"],
|
||||||
"@budibase/backend-core": ["../backend-core/src"],
|
"@budibase/backend-core": ["../backend-core/src"],
|
||||||
|
@ -16,6 +17,6 @@
|
||||||
"swc": true
|
"swc": true
|
||||||
},
|
},
|
||||||
"references": [{ "path": "../types" }, { "path": "../backend-core" }],
|
"references": [{ "path": "../types" }, { "path": "../backend-core" }],
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*", "package.json"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,10 +55,13 @@ export const buildGroupsEndpoints = API => {
|
||||||
/**
|
/**
|
||||||
* Gets a group users by the group id
|
* Gets a group users by the group id
|
||||||
*/
|
*/
|
||||||
getGroupUsers: async ({ id, bookmark }) => {
|
getGroupUsers: async ({ id, bookmark, emailSearch }) => {
|
||||||
let url = `/api/global/groups/${id}/users?`
|
let url = `/api/global/groups/${id}/users?`
|
||||||
if (bookmark) {
|
if (bookmark) {
|
||||||
url += `bookmark=${bookmark}`
|
url += `bookmark=${bookmark}&`
|
||||||
|
}
|
||||||
|
if (emailSearch) {
|
||||||
|
url += `emailSearch=${emailSearch}&`
|
||||||
}
|
}
|
||||||
|
|
||||||
return await API.get({
|
return await API.get({
|
||||||
|
|
|
@ -31,6 +31,7 @@ export default class GroupUserFetch extends DataFetch {
|
||||||
try {
|
try {
|
||||||
const res = await this.API.getGroupUsers({
|
const res = await this.API.getGroupUsers({
|
||||||
id: query.groupId,
|
id: query.groupId,
|
||||||
|
emailSearch: query.emailSearch,
|
||||||
bookmark: cursor,
|
bookmark: cursor,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit c57a98d246a50a43905d8572a88c901ec598390c
|
Subproject commit 64a2025727c25d5813832c92eb360de3947b7aa6
|
|
@ -118,8 +118,11 @@ export async function patch(ctx: UserCtx) {
|
||||||
combinedRow[key] = inputs[key]
|
combinedRow[key] = inputs[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// need to copy the table so it can be differenced on way out
|
||||||
|
const tableClone = cloneDeep(dbTable)
|
||||||
|
|
||||||
// this returns the table and row incase they have been updated
|
// this returns the table and row incase they have been updated
|
||||||
let { table, row } = inputProcessing(ctx.user, dbTable, combinedRow)
|
let { table, row } = inputProcessing(ctx.user, tableClone, combinedRow)
|
||||||
const validateResult = await utils.validate({
|
const validateResult = await utils.validate({
|
||||||
row,
|
row,
|
||||||
table,
|
table,
|
||||||
|
@ -163,7 +166,12 @@ export async function save(ctx: UserCtx) {
|
||||||
|
|
||||||
// this returns the table and row incase they have been updated
|
// this returns the table and row incase they have been updated
|
||||||
const dbTable = await db.get(inputs.tableId)
|
const dbTable = await db.get(inputs.tableId)
|
||||||
let { table, row } = inputProcessing(ctx.user, dbTable, inputs)
|
|
||||||
|
// need to copy the table so it can be differenced on way out
|
||||||
|
const tableClone = cloneDeep(dbTable)
|
||||||
|
|
||||||
|
let { table, row } = inputProcessing(ctx.user, tableClone, inputs)
|
||||||
|
|
||||||
const validateResult = await utils.validate({
|
const validateResult = await utils.validate({
|
||||||
row,
|
row,
|
||||||
table,
|
table,
|
||||||
|
|
|
@ -97,6 +97,7 @@ export async function bulkImport(ctx: UserCtx) {
|
||||||
// right now we don't trigger anything for bulk import because it
|
// right now we don't trigger anything for bulk import because it
|
||||||
// can only be done in the builder, but in the future we may need to
|
// can only be done in the builder, but in the future we may need to
|
||||||
// think about events for bulk items
|
// think about events for bulk items
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = { message: `Bulk rows created.` }
|
ctx.body = { message: `Bulk rows created.` }
|
||||||
}
|
}
|
||||||
|
|
|
@ -184,8 +184,13 @@ export async function destroy(ctx: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bulkImport(ctx: any) {
|
export async function bulkImport(ctx: any) {
|
||||||
|
const db = context.getAppDB()
|
||||||
const table = await sdk.tables.getTable(ctx.params.tableId)
|
const table = await sdk.tables.getTable(ctx.params.tableId)
|
||||||
const { rows } = ctx.request.body
|
const { rows } = ctx.request.body
|
||||||
await handleDataImport(ctx.user, table, rows)
|
await handleDataImport(ctx.user, table, rows)
|
||||||
|
|
||||||
|
// Ensure auto id and other table updates are persisted
|
||||||
|
await db.put(table)
|
||||||
|
|
||||||
return table
|
return table
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,17 +129,17 @@ export function importToRows(
|
||||||
// the real schema of the table passed in, not the clone used for
|
// the real schema of the table passed in, not the clone used for
|
||||||
// incrementing auto IDs
|
// incrementing auto IDs
|
||||||
for (const [fieldName, schema] of Object.entries(originalTable.schema)) {
|
for (const [fieldName, schema] of Object.entries(originalTable.schema)) {
|
||||||
|
const rowVal = Array.isArray(row[fieldName])
|
||||||
|
? row[fieldName]
|
||||||
|
: [row[fieldName]]
|
||||||
if (
|
if (
|
||||||
(schema.type === FieldTypes.OPTIONS ||
|
(schema.type === FieldTypes.OPTIONS ||
|
||||||
schema.type === FieldTypes.ARRAY) &&
|
schema.type === FieldTypes.ARRAY) &&
|
||||||
row[fieldName] &&
|
row[fieldName]
|
||||||
(!schema.constraints!.inclusion ||
|
|
||||||
schema.constraints!.inclusion.indexOf(row[fieldName]) === -1)
|
|
||||||
) {
|
) {
|
||||||
schema.constraints!.inclusion = [
|
let merged = [...schema.constraints!.inclusion!, ...rowVal]
|
||||||
...schema.constraints!.inclusion!,
|
let superSet = new Set(merged)
|
||||||
row[fieldName],
|
schema.constraints!.inclusion = Array.from(superSet)
|
||||||
]
|
|
||||||
schema.constraints!.inclusion.sort()
|
schema.constraints!.inclusion.sort()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,13 +42,17 @@ if (!env.isTest()) {
|
||||||
host: REDIS_OPTS.host,
|
host: REDIS_OPTS.host,
|
||||||
port: REDIS_OPTS.port,
|
port: REDIS_OPTS.port,
|
||||||
},
|
},
|
||||||
password:
|
}
|
||||||
REDIS_OPTS.opts.password || REDIS_OPTS.opts.redisOptions.password,
|
|
||||||
|
if (REDIS_OPTS.opts?.password || REDIS_OPTS.opts.redisOptions?.password) {
|
||||||
|
// @ts-ignore
|
||||||
|
options.password =
|
||||||
|
REDIS_OPTS.opts.password || REDIS_OPTS.opts.redisOptions.password
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!env.REDIS_CLUSTERED) {
|
if (!env.REDIS_CLUSTERED) {
|
||||||
// Can't set direct redis db in clustered env
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
// Can't set direct redis db in clustered env
|
||||||
options.database = 1
|
options.database = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,18 +73,97 @@ describe("run misc tests", () => {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
e: {
|
||||||
|
name: "Auto ID",
|
||||||
|
type: "number",
|
||||||
|
subtype: "autoID",
|
||||||
|
icon: "ri-magic-line",
|
||||||
|
autocolumn: true,
|
||||||
|
constraints: {
|
||||||
|
type: "number",
|
||||||
|
presence: false,
|
||||||
|
numericality: {
|
||||||
|
greaterThanOrEqualTo: "",
|
||||||
|
lessThanOrEqualTo: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
f: {
|
||||||
|
type: "array",
|
||||||
|
constraints: {
|
||||||
|
type: "array",
|
||||||
|
presence: {
|
||||||
|
"allowEmpty": true
|
||||||
|
},
|
||||||
|
inclusion: [
|
||||||
|
"One",
|
||||||
|
"Two",
|
||||||
|
"Three",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
name: "Sample Tags",
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
g: {
|
||||||
|
type: "options",
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
presence: false,
|
||||||
|
inclusion: [
|
||||||
|
"Alpha",
|
||||||
|
"Beta",
|
||||||
|
"Gamma"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
name: "Sample Opts"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Shift specific row tests to the row spec
|
||||||
await tableUtils.handleDataImport(
|
await tableUtils.handleDataImport(
|
||||||
{ userId: "test" },
|
{ userId: "test" },
|
||||||
table,
|
table,
|
||||||
[{ a: '1', b: '2', c: '3', d: '4'}]
|
[
|
||||||
|
{ a: '1', b: '2', c: '3', d: '4', f: "['One']", g: "Alpha" },
|
||||||
|
{ a: '5', b: '6', c: '7', d: '8', f: "[]", g: undefined},
|
||||||
|
{ a: '9', b: '10', c: '11', d: '12', f: "['Two','Four']", g: ""},
|
||||||
|
{ a: '13', b: '14', c: '15', d: '16', g: "Omega"}
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 4 rows imported, the auto ID starts at 1
|
||||||
|
// We expect the handleDataImport function to update the lastID
|
||||||
|
expect(table.schema.e.lastID).toEqual(4);
|
||||||
|
|
||||||
|
// Array/Multi - should have added a new value to the inclusion.
|
||||||
|
expect(table.schema.f.constraints.inclusion).toEqual(['Four','One','Three','Two']);
|
||||||
|
|
||||||
|
// Options - should have a new value in the inclusion
|
||||||
|
expect(table.schema.g.constraints.inclusion).toEqual(['Alpha','Beta','Gamma','Omega']);
|
||||||
|
|
||||||
const rows = await config.getRows()
|
const rows = await config.getRows()
|
||||||
expect(rows[0].a).toEqual("1")
|
expect(rows.length).toEqual(4);
|
||||||
expect(rows[0].b).toEqual("2")
|
|
||||||
expect(rows[0].c).toEqual("3")
|
const rowOne = rows.find(row => row.e === 1)
|
||||||
|
expect(rowOne.a).toEqual("1")
|
||||||
|
expect(rowOne.f).toEqual(['One'])
|
||||||
|
expect(rowOne.g).toEqual('Alpha')
|
||||||
|
|
||||||
|
const rowTwo = rows.find(row => row.e === 2)
|
||||||
|
expect(rowTwo.a).toEqual("5")
|
||||||
|
expect(rowTwo.f).toEqual([])
|
||||||
|
expect(rowTwo.g).toEqual(undefined)
|
||||||
|
|
||||||
|
const rowThree = rows.find(row => row.e === 3)
|
||||||
|
expect(rowThree.a).toEqual("9")
|
||||||
|
expect(rowThree.f).toEqual(['Two','Four'])
|
||||||
|
expect(rowThree.g).toEqual(null)
|
||||||
|
|
||||||
|
const rowFour = rows.find(row => row.e === 4)
|
||||||
|
expect(rowFour.a).toEqual("13")
|
||||||
|
expect(rowFour.f).toEqual(undefined)
|
||||||
|
expect(rowFour.g).toEqual('Omega')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -34,9 +34,9 @@ describe("/rows", () => {
|
||||||
row = basicRow(table._id)
|
row = basicRow(table._id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadRow = async (id, status = 200) =>
|
const loadRow = async (id, tbl_Id, status = 200) =>
|
||||||
await request
|
await request
|
||||||
.get(`/api/${table._id}/rows/${id}`)
|
.get(`/api/${tbl_Id}/rows/${id}`)
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(status)
|
.expect(status)
|
||||||
|
@ -79,6 +79,60 @@ describe("/rows", () => {
|
||||||
await assertQueryUsage(queryUsage + 1)
|
await assertQueryUsage(queryUsage + 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("Increment row autoId per create row request", async () => {
|
||||||
|
const rowUsage = await getRowUsage()
|
||||||
|
const queryUsage = await getQueryUsage()
|
||||||
|
|
||||||
|
const newTable = await config.createTable({
|
||||||
|
name: "TestTableAuto",
|
||||||
|
type: "table",
|
||||||
|
key: "name",
|
||||||
|
schema: {
|
||||||
|
...table.schema,
|
||||||
|
"Row ID": {
|
||||||
|
name: "Row ID",
|
||||||
|
type: "number",
|
||||||
|
subtype: "autoID",
|
||||||
|
icon: "ri-magic-line",
|
||||||
|
autocolumn: true,
|
||||||
|
constraints: {
|
||||||
|
type: "number",
|
||||||
|
presence: false,
|
||||||
|
numericality: {
|
||||||
|
greaterThanOrEqualTo: "",
|
||||||
|
lessThanOrEqualTo: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ids = [1,2,3]
|
||||||
|
|
||||||
|
// Performing several create row requests should increment the autoID fields accordingly
|
||||||
|
const createRow = async (id) => {
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/${newTable._id}/rows`)
|
||||||
|
.send({
|
||||||
|
name: "row_" + id
|
||||||
|
})
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(res.res.statusMessage).toEqual(`${newTable.name} saved successfully`)
|
||||||
|
expect(res.body.name).toEqual("row_" + id)
|
||||||
|
expect(res.body._rev).toBeDefined()
|
||||||
|
expect(res.body["Row ID"]).toEqual(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i=0; i<ids.length; i++ ){
|
||||||
|
await createRow(ids[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertRowUsage(rowUsage + ids.length)
|
||||||
|
await assertQueryUsage(queryUsage + ids.length)
|
||||||
|
})
|
||||||
|
|
||||||
it("updates a row successfully", async () => {
|
it("updates a row successfully", async () => {
|
||||||
const existing = await config.createRow()
|
const existing = await config.createRow()
|
||||||
const rowUsage = await getRowUsage()
|
const rowUsage = await getRowUsage()
|
||||||
|
@ -182,8 +236,32 @@ describe("/rows", () => {
|
||||||
type: "string",
|
type: "string",
|
||||||
presence: false,
|
presence: false,
|
||||||
datetime: { earliest: "", latest: "" },
|
datetime: { earliest: "", latest: "" },
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
const arrayField = {
|
||||||
|
type: "array",
|
||||||
|
constraints: {
|
||||||
|
type: "array",
|
||||||
|
presence: false,
|
||||||
|
inclusion: [
|
||||||
|
"One",
|
||||||
|
"Two",
|
||||||
|
"Three",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
name: "Sample Tags",
|
||||||
|
sortable: false
|
||||||
|
}
|
||||||
|
const optsField = {
|
||||||
|
fieldName: "Sample Opts",
|
||||||
|
name: "Sample Opts",
|
||||||
|
type: "options",
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
presence: false,
|
||||||
|
inclusion: [ "Alpha", "Beta", "Gamma" ]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
table = await config.createTable({
|
table = await config.createTable({
|
||||||
name: "TestTable2",
|
name: "TestTable2",
|
||||||
|
@ -212,7 +290,15 @@ describe("/rows", () => {
|
||||||
attachmentNull: attachment,
|
attachmentNull: attachment,
|
||||||
attachmentUndefined: attachment,
|
attachmentUndefined: attachment,
|
||||||
attachmentEmpty: attachment,
|
attachmentEmpty: attachment,
|
||||||
attachmentEmptyArrayStr: attachment
|
attachmentEmptyArrayStr: attachment,
|
||||||
|
arrayFieldEmptyArrayStr: arrayField,
|
||||||
|
arrayFieldArrayStrKnown: arrayField,
|
||||||
|
arrayFieldNull: arrayField,
|
||||||
|
arrayFieldUndefined: arrayField,
|
||||||
|
optsFieldEmptyStr: optsField,
|
||||||
|
optsFieldUndefined: optsField,
|
||||||
|
optsFieldNull: optsField,
|
||||||
|
optsFieldStrKnown: optsField
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -241,11 +327,20 @@ describe("/rows", () => {
|
||||||
attachmentUndefined: undefined,
|
attachmentUndefined: undefined,
|
||||||
attachmentEmpty: "",
|
attachmentEmpty: "",
|
||||||
attachmentEmptyArrayStr: "[]",
|
attachmentEmptyArrayStr: "[]",
|
||||||
|
arrayFieldEmptyArrayStr: "[]",
|
||||||
|
arrayFieldUndefined: undefined,
|
||||||
|
arrayFieldNull: null,
|
||||||
|
arrayFieldArrayStrKnown: "['One']",
|
||||||
|
optsFieldEmptyStr: "",
|
||||||
|
optsFieldUndefined: undefined,
|
||||||
|
optsFieldNull: null,
|
||||||
|
optsFieldStrKnown: 'Alpha'
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = (await config.createRow(row))._id
|
const createdRow = await config.createRow(row);
|
||||||
|
const id = createdRow._id
|
||||||
|
|
||||||
const saved = (await loadRow(id)).body
|
const saved = (await loadRow(id, table._id)).body
|
||||||
|
|
||||||
expect(saved.stringUndefined).toBe(undefined)
|
expect(saved.stringUndefined).toBe(undefined)
|
||||||
expect(saved.stringNull).toBe("")
|
expect(saved.stringNull).toBe("")
|
||||||
|
@ -270,7 +365,15 @@ describe("/rows", () => {
|
||||||
expect(saved.attachmentNull).toEqual([])
|
expect(saved.attachmentNull).toEqual([])
|
||||||
expect(saved.attachmentUndefined).toBe(undefined)
|
expect(saved.attachmentUndefined).toBe(undefined)
|
||||||
expect(saved.attachmentEmpty).toEqual([])
|
expect(saved.attachmentEmpty).toEqual([])
|
||||||
expect(saved.attachmentEmptyArrayStr).toEqual([])
|
expect(saved.attachmentEmptyArrayStr).toEqual([])
|
||||||
|
expect(saved.arrayFieldEmptyArrayStr).toEqual([])
|
||||||
|
expect(saved.arrayFieldNull).toEqual([])
|
||||||
|
expect(saved.arrayFieldUndefined).toEqual(undefined)
|
||||||
|
expect(saved.optsFieldEmptyStr).toEqual(null)
|
||||||
|
expect(saved.optsFieldUndefined).toEqual(undefined)
|
||||||
|
expect(saved.optsFieldNull).toEqual(null)
|
||||||
|
expect(saved.arrayFieldArrayStrKnown).toEqual(['One'])
|
||||||
|
expect(saved.optsFieldStrKnown).toEqual('Alpha')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -299,7 +402,7 @@ describe("/rows", () => {
|
||||||
expect(res.body.name).toEqual("Updated Name")
|
expect(res.body.name).toEqual("Updated Name")
|
||||||
expect(res.body.description).toEqual(existing.description)
|
expect(res.body.description).toEqual(existing.description)
|
||||||
|
|
||||||
const savedRow = await loadRow(res.body._id)
|
const savedRow = await loadRow(res.body._id, table._id)
|
||||||
|
|
||||||
expect(savedRow.body.description).toEqual(existing.description)
|
expect(savedRow.body.description).toEqual(existing.description)
|
||||||
expect(savedRow.body.name).toEqual("Updated Name")
|
expect(savedRow.body.name).toEqual("Updated Name")
|
||||||
|
@ -401,7 +504,7 @@ describe("/rows", () => {
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.length).toEqual(2)
|
expect(res.body.length).toEqual(2)
|
||||||
await loadRow(row1._id, 404)
|
await loadRow(row1._id, table._id, 404)
|
||||||
await assertRowUsage(rowUsage - 2)
|
await assertRowUsage(rowUsage - 2)
|
||||||
await assertQueryUsage(queryUsage + 1)
|
await assertQueryUsage(queryUsage + 1)
|
||||||
})
|
})
|
||||||
|
|
|
@ -167,7 +167,10 @@ describe("/tables", () => {
|
||||||
|
|
||||||
expect(events.table.created).not.toHaveBeenCalled()
|
expect(events.table.created).not.toHaveBeenCalled()
|
||||||
expect(events.rows.imported).toBeCalledTimes(1)
|
expect(events.rows.imported).toBeCalledTimes(1)
|
||||||
expect(events.rows.imported).toBeCalledWith(table, 1)
|
expect(events.rows.imported).toBeCalledWith(expect.objectContaining({
|
||||||
|
name: "TestTable",
|
||||||
|
_id: table._id
|
||||||
|
}), 1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -137,8 +137,7 @@ export function inputProcessing(
|
||||||
opts?: AutoColumnProcessingOpts
|
opts?: AutoColumnProcessingOpts
|
||||||
) {
|
) {
|
||||||
let clonedRow = cloneDeep(row)
|
let clonedRow = cloneDeep(row)
|
||||||
// need to copy the table so it can be differenced on way out
|
|
||||||
const copiedTable = cloneDeep(table)
|
|
||||||
const dontCleanseKeys = ["type", "_id", "_rev", "tableId"]
|
const dontCleanseKeys = ["type", "_id", "_rev", "tableId"]
|
||||||
for (let [key, value] of Object.entries(clonedRow)) {
|
for (let [key, value] of Object.entries(clonedRow)) {
|
||||||
const field = table.schema[key]
|
const field = table.schema[key]
|
||||||
|
@ -175,7 +174,7 @@ export function inputProcessing(
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle auto columns - this returns an object like {table, row}
|
// handle auto columns - this returns an object like {table, row}
|
||||||
return processAutoColumn(user, copiedTable, clonedRow, opts)
|
return processAutoColumn(user, table, clonedRow, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,6 +2,22 @@
|
||||||
import { FieldTypes } from "../../constants"
|
import { FieldTypes } from "../../constants"
|
||||||
import { logging } from "@budibase/backend-core"
|
import { logging } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
const parseArrayString = value => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
if (value === "") {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let result
|
||||||
|
try {
|
||||||
|
result = JSON.parse(value.replace(/'/g, '"'))
|
||||||
|
return result
|
||||||
|
} catch (e) {
|
||||||
|
logging.logAlert("Could not parse row value", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A map of how we convert various properties in rows to each other based on the row type.
|
* A map of how we convert various properties in rows to each other based on the row type.
|
||||||
*/
|
*/
|
||||||
|
@ -26,9 +42,9 @@ export const TYPE_TRANSFORM_MAP: any = {
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
},
|
},
|
||||||
[FieldTypes.ARRAY]: {
|
[FieldTypes.ARRAY]: {
|
||||||
"": [],
|
|
||||||
[null]: [],
|
[null]: [],
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
|
parse: parseArrayString,
|
||||||
},
|
},
|
||||||
[FieldTypes.STRING]: {
|
[FieldTypes.STRING]: {
|
||||||
"": "",
|
"": "",
|
||||||
|
@ -70,21 +86,7 @@ export const TYPE_TRANSFORM_MAP: any = {
|
||||||
[FieldTypes.ATTACHMENT]: {
|
[FieldTypes.ATTACHMENT]: {
|
||||||
[null]: [],
|
[null]: [],
|
||||||
[undefined]: undefined,
|
[undefined]: undefined,
|
||||||
parse: attachments => {
|
parse: parseArrayString,
|
||||||
if (typeof attachments === "string") {
|
|
||||||
if (attachments === "") {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
let result
|
|
||||||
try {
|
|
||||||
result = JSON.parse(attachments)
|
|
||||||
} catch (e) {
|
|
||||||
logging.logAlert("Could not parse attachments", e)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return attachments
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
[FieldTypes.BOOLEAN]: {
|
[FieldTypes.BOOLEAN]: {
|
||||||
"": null,
|
"": null,
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { events } from "@budibase/backend-core"
|
import { events } from "@budibase/backend-core"
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
import { structures, TestConfiguration, mocks } from "../../../../tests"
|
import { structures, TestConfiguration, mocks } from "../../../../tests"
|
||||||
|
import { UserGroup } from "@budibase/types"
|
||||||
|
|
||||||
|
mocks.licenses.useGroups()
|
||||||
|
|
||||||
describe("/api/global/groups", () => {
|
describe("/api/global/groups", () => {
|
||||||
const config = new TestConfiguration()
|
const config = new TestConfiguration()
|
||||||
|
@ -113,4 +117,118 @@ describe("/api/global/groups", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("find users", () => {
|
||||||
|
describe("without users", () => {
|
||||||
|
let group: UserGroup
|
||||||
|
beforeAll(async () => {
|
||||||
|
group = structures.groups.UserGroup()
|
||||||
|
await config.api.groups.saveGroup(group)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return empty", async () => {
|
||||||
|
const result = await config.api.groups.searchUsers(group._id!)
|
||||||
|
expect(result.body).toEqual({
|
||||||
|
users: [],
|
||||||
|
bookmark: undefined,
|
||||||
|
hasNextPage: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("existing users", () => {
|
||||||
|
let groupId: string
|
||||||
|
let users: { _id: string; email: string }[] = []
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
groupId = (
|
||||||
|
await config.api.groups.saveGroup(structures.groups.UserGroup())
|
||||||
|
).body._id
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Array.from({ length: 30 }).map(async (_, i) => {
|
||||||
|
const email = `user${i}@${generator.domain()}`
|
||||||
|
const user = await config.api.users.saveUser({
|
||||||
|
...structures.users.user(),
|
||||||
|
email,
|
||||||
|
})
|
||||||
|
users.push({ _id: user.body._id, email })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
users = users.sort((a, b) => a._id.localeCompare(b._id))
|
||||||
|
await config.api.groups.updateGroupUsers(groupId, {
|
||||||
|
add: users.map(u => u._id),
|
||||||
|
remove: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("pagination", () => {
|
||||||
|
it("should return first page", async () => {
|
||||||
|
const result = await config.api.groups.searchUsers(groupId)
|
||||||
|
expect(result.body).toEqual({
|
||||||
|
users: users.slice(0, 10),
|
||||||
|
bookmark: users[10]._id,
|
||||||
|
hasNextPage: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("given a bookmark, should return skip items", async () => {
|
||||||
|
const result = await config.api.groups.searchUsers(groupId, {
|
||||||
|
bookmark: users[7]._id,
|
||||||
|
})
|
||||||
|
expect(result.body).toEqual({
|
||||||
|
users: users.slice(7, 17),
|
||||||
|
bookmark: users[17]._id,
|
||||||
|
hasNextPage: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("bookmarking the last page, should return last page info", async () => {
|
||||||
|
const result = await config.api.groups.searchUsers(groupId, {
|
||||||
|
bookmark: users[20]._id,
|
||||||
|
})
|
||||||
|
expect(result.body).toEqual({
|
||||||
|
users: users.slice(20),
|
||||||
|
bookmark: undefined,
|
||||||
|
hasNextPage: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("search by email", () => {
|
||||||
|
it('should be able to search "starting" by email', async () => {
|
||||||
|
const result = await config.api.groups.searchUsers(groupId, {
|
||||||
|
emailSearch: `user1`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const matchedUsers = users
|
||||||
|
.filter(u => u.email.startsWith("user1"))
|
||||||
|
.sort((a, b) => a.email.localeCompare(b.email))
|
||||||
|
|
||||||
|
expect(result.body).toEqual({
|
||||||
|
users: matchedUsers.slice(0, 10),
|
||||||
|
bookmark: matchedUsers[10].email,
|
||||||
|
hasNextPage: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to bookmark when searching by email", async () => {
|
||||||
|
const matchedUsers = users
|
||||||
|
.filter(u => u.email.startsWith("user1"))
|
||||||
|
.sort((a, b) => a.email.localeCompare(b.email))
|
||||||
|
|
||||||
|
const result = await config.api.groups.searchUsers(groupId, {
|
||||||
|
emailSearch: `user1`,
|
||||||
|
bookmark: matchedUsers[4].email,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.body).toEqual({
|
||||||
|
users: matchedUsers.slice(4),
|
||||||
|
bookmark: undefined,
|
||||||
|
hasNextPage: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,4 +23,34 @@ export class GroupsAPI extends TestAPI {
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchUsers = (
|
||||||
|
id: string,
|
||||||
|
params?: { bookmark?: string; emailSearch?: string }
|
||||||
|
) => {
|
||||||
|
let url = `/api/global/groups/${id}/users?`
|
||||||
|
if (params?.bookmark) {
|
||||||
|
url += `bookmark=${params.bookmark}&`
|
||||||
|
}
|
||||||
|
if (params?.emailSearch) {
|
||||||
|
url += `emailSearch=${params.emailSearch}&`
|
||||||
|
}
|
||||||
|
return this.request
|
||||||
|
.get(url)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGroupUsers = (
|
||||||
|
id: string,
|
||||||
|
body: { add: string[]; remove: string[] }
|
||||||
|
) => {
|
||||||
|
return this.request
|
||||||
|
.post(`/api/global/groups/${id}/users`)
|
||||||
|
.send(body)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,12 @@ export default class LicenseAPI {
|
||||||
internal: true,
|
internal: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not update license for accountId=${accountId}: ${response.status}`
|
||||||
|
)
|
||||||
|
}
|
||||||
return [response, json]
|
return [response, json]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,6 @@ describe("Internal API - App Specific Roles & Permissions", () => {
|
||||||
await config.beforeAll()
|
await config.beforeAll()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await config.afterAll()
|
|
||||||
})
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await config.afterAll()
|
await config.afterAll()
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue