1
0
Fork 0
mirror of synced 2024-07-08 15:56:23 +12:00

Merge branch 'master' into chore/update-gh-actions

This commit is contained in:
Adria Navarro 2024-01-25 15:20:22 +01:00
commit 5f9bdd5e2b
27 changed files with 216 additions and 63 deletions

View file

@ -1,5 +1,5 @@
{
"version": "2.15.5",
"version": "2.15.6",
"npmClient": "yarn",
"packages": [
"packages/*",

View file

@ -2,6 +2,7 @@ export * as configs from "./configs"
export * as events from "./events"
export * as migrations from "./migrations"
export * as users from "./users"
export * as userUtils from "./users/utils"
export * as roles from "./security/roles"
export * as permissions from "./security/permissions"
export * as accounts from "./accounts"

View file

@ -251,7 +251,8 @@ export class UserDB {
}
const change = dbUser ? 0 : 1 // no change if there is existing user
const creatorsChange = isCreator(dbUser) !== isCreator(user) ? 1 : 0
const creatorsChange =
(await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0
return UserDB.quotas.addUsers(change, creatorsChange, async () => {
await validateUniqueUser(email, tenantId)
@ -335,7 +336,7 @@ export class UserDB {
}
newUser.userGroups = groups || []
newUsers.push(newUser)
if (isCreator(newUser)) {
if (await isCreator(newUser)) {
newCreators.push(newUser)
}
}
@ -432,12 +433,16 @@ export class UserDB {
_deleted: true,
}))
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
const creatorsToDelete = usersToDelete.filter(isCreator)
const creatorsEval = await Promise.all(usersToDelete.map(isCreator))
const creatorsToDeleteCount = creatorsEval.filter(
creator => !!creator
).length
for (let user of usersToDelete) {
await bulkDeleteProcessing(user)
}
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDelete.length)
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount)
// Build Response
// index users by id
@ -486,7 +491,7 @@ export class UserDB {
await db.remove(userId, dbUser._rev)
const creatorsToDelete = isCreator(dbUser) ? 1 : 0
const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0
await UserDB.quotas.removeUsers(1, creatorsToDelete)
await eventHelpers.handleDeleteEvents(dbUser)
await cache.user.invalidateUser(userId)

View file

@ -0,0 +1,67 @@
import { User, UserGroup } from "@budibase/types"
import { generator, structures } from "../../../tests"
import { DBTestConfiguration } from "../../../tests/extra"
import { getGlobalDB } from "../../context"
import { isCreator } from "../utils"
const config = new DBTestConfiguration()
describe("Users", () => {
it("User is a creator if it is configured as a global builder", async () => {
const user: User = structures.users.user({ builder: { global: true } })
expect(await isCreator(user)).toBe(true)
})
it("User is a creator if it is configured as a global admin", async () => {
const user: User = structures.users.user({ admin: { global: true } })
expect(await isCreator(user)).toBe(true)
})
it("User is a creator if it is configured with creator permission", async () => {
const user: User = structures.users.user({ builder: { creator: true } })
expect(await isCreator(user)).toBe(true)
})
it("User is a creator if it is a builder in some application", async () => {
const user: User = structures.users.user({ builder: { apps: ["app1"] } })
expect(await isCreator(user)).toBe(true)
})
it("User is a creator if it has CREATOR permission in some application", async () => {
const user: User = structures.users.user({ roles: { app1: "CREATOR" } })
expect(await isCreator(user)).toBe(true)
})
it("User is a creator if it has ADMIN permission in some application", async () => {
const user: User = structures.users.user({ roles: { app1: "ADMIN" } })
expect(await isCreator(user)).toBe(true)
})
it("User is a creator if it remains to a group with ADMIN permissions", async () => {
const usersInGroup = 10
const groupId = "gr_17abffe89e0b40268e755b952f101a59"
const group: UserGroup = {
...structures.userGroups.userGroup(),
...{ _id: groupId, roles: { app1: "ADMIN" } },
}
const users: User[] = []
for (const _ of Array.from({ length: usersInGroup })) {
const userId = `us_${generator.guid()}`
const user: User = structures.users.user({
_id: userId,
userGroups: [groupId],
})
users.push(user)
}
await config.doInTenant(async () => {
const db = getGlobalDB()
await db.put(group)
for (let user of users) {
await db.put(user)
const creator = await isCreator(user)
expect(creator).toBe(true)
}
})
})
})

View file

@ -309,7 +309,8 @@ export async function getCreatorCount() {
let creators = 0
async function iterate(startPage?: string) {
const page = await paginatedUsers({ bookmark: startPage })
creators += page.data.filter(isCreator).length
const creatorsEval = await Promise.all(page.data.map(isCreator))
creators += creatorsEval.filter(creator => !!creator).length
if (page.hasNextPage) {
await iterate(page.nextPage)
}

View file

@ -1,4 +1,4 @@
import { CloudAccount } from "@budibase/types"
import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types"
import * as accountSdk from "../accounts"
import env from "../environment"
import { getPlatformUser } from "./lookup"
@ -6,17 +6,48 @@ import { EmailUnavailableError } from "../errors"
import { getTenantId } from "../context"
import { sdk } from "@budibase/shared-core"
import { getAccountByTenantId } from "../accounts"
import { BUILTIN_ROLE_IDS } from "../security/roles"
import * as context from "../context"
// extract from shared-core to make easily accessible from backend-core
export const isBuilder = sdk.users.isBuilder
export const isAdmin = sdk.users.isAdmin
export const isCreator = sdk.users.isCreator
export const isGlobalBuilder = sdk.users.isGlobalBuilder
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
export const hasAdminPermissions = sdk.users.hasAdminPermissions
export const hasBuilderPermissions = sdk.users.hasBuilderPermissions
export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions
export async function isCreator(user?: User | ContextUser) {
const isCreatorByUserDefinition = sdk.users.isCreator(user)
if (!isCreatorByUserDefinition && user) {
return await isCreatorByGroupMembership(user)
}
return isCreatorByUserDefinition
}
async function isCreatorByGroupMembership(user?: User | ContextUser) {
const userGroups = user?.userGroups || []
if (userGroups.length > 0) {
const db = context.getGlobalDB()
const groups: UserGroup[] = []
for (let groupId of userGroups) {
try {
const group = await db.get<UserGroup>(groupId)
groups.push(group)
} catch (e: any) {
if (e.error !== "not_found") {
throw e
}
}
}
return groups.some(group =>
Object.values(group.roles || {}).includes(BUILTIN_ROLE_IDS.ADMIN)
)
}
return false
}
export async function validateUniqueUser(email: string, tenantId: string) {
// check budibase users in other tenants
if (env.MULTI_TENANCY) {

View file

@ -18,7 +18,6 @@ export default function positionDropdown(element, opts) {
useAnchorWidth,
offset = 5,
customUpdate,
offsetBelow,
} = opts
if (!anchor) {
return
@ -48,7 +47,7 @@ export default function positionDropdown(element, opts) {
styles.top = anchorBounds.top - elementBounds.height - offset
styles.maxHeight = maxHeight || 240
} else {
styles.top = anchorBounds.bottom + (offsetBelow || offset)
styles.top = anchorBounds.bottom + offset
styles.maxHeight =
maxHeight || window.innerHeight - anchorBounds.bottom - 20
}

View file

@ -15,8 +15,6 @@
export let autoWidth = false
export let searchTerm = null
export let customPopoverHeight
export let customPopoverOffsetBelow
export let customPopoverMaxHeight
export let open = false
export let loading
@ -98,7 +96,5 @@
{sort}
{autoWidth}
{customPopoverHeight}
{customPopoverOffsetBelow}
{customPopoverMaxHeight}
{loading}
/>

View file

@ -37,8 +37,6 @@
export let sort = false
export let searchTerm = null
export let customPopoverHeight
export let customPopoverOffsetBelow
export let customPopoverMaxHeight
export let align = "left"
export let footer = null
export let customAnchor = null
@ -156,9 +154,7 @@
on:close={() => (open = false)}
useAnchorWidth={!autoWidth}
maxWidth={autoWidth ? 400 : null}
maxHeight={customPopoverMaxHeight}
customHeight={customPopoverHeight}
offsetBelow={customPopoverOffsetBelow}
>
<div
class="popover-content"

View file

@ -24,8 +24,6 @@
export let footer = null
export let open = false
export let tag = null
export let customPopoverOffsetBelow
export let customPopoverMaxHeight
export let searchTerm = null
export let loading
@ -35,15 +33,20 @@
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
function compareOptionAndValue(option, value) {
return typeof compare === "function"
? compare(option, value)
: option === value
}
const getFieldAttribute = (getAttribute, value, options) => {
// Wait for options to load if there is a value but no options
if (!options?.length) {
return ""
}
const index = options.findIndex((option, idx) => {
const opt = getOptionValue(option, idx)
return typeof compare === "function" ? compare(opt, value) : opt === value
})
const index = options.findIndex((option, idx) =>
compareOptionAndValue(getOptionValue(option, idx), value)
)
return index !== -1 ? getAttribute(options[index], index) : null
}
@ -92,11 +95,9 @@
{autocomplete}
{sort}
{tag}
{customPopoverOffsetBelow}
{customPopoverMaxHeight}
isPlaceholder={value == null || value === ""}
placeholderOption={placeholder === false ? null : placeholder}
isOptionSelected={option => compare(option, value)}
isOptionSelected={option => compareOptionAndValue(option, value)}
onSelectOption={selectOption}
{loading}
/>

View file

@ -18,7 +18,6 @@
export let useAnchorWidth = false
export let dismissible = true
export let offset = 5
export let offsetBelow
export let customHeight
export let animate = true
export let customZindex
@ -89,7 +88,6 @@
maxWidth,
useAnchorWidth,
offset,
offsetBelow,
customUpdate: handlePostionUpdate,
}}
use:clickOutside={{

View file

@ -9,6 +9,7 @@ import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core"
import { createHistoryStore } from "builderStore/store/history"
import { cloneDeep } from "lodash/fp"
import { getHoverStore } from "./store/hover"
export const store = getFrontendStore()
export const automationStore = getAutomationStore()
@ -16,6 +17,7 @@ export const themeStore = getThemeStore()
export const temporalStore = getTemporalStore()
export const userStore = getUserStore()
export const deploymentStore = getDeploymentStore()
export const hoverStore = getHoverStore()
// Setup history for screens
export const screenHistoryStore = createHistoryStore({

View file

@ -92,9 +92,6 @@ const INITIAL_FRONTEND_STATE = {
// Onboarding
onboarding: false,
tourNodes: null,
// UI state
hoveredComponentId: null,
}
export const getFrontendStore = () => {
@ -1415,18 +1412,6 @@ export const getFrontendStore = () => {
return state
})
},
hover: (componentId, notifyClient = true) => {
if (componentId === get(store).hoveredComponentId) {
return
}
store.update(state => {
state.hoveredComponentId = componentId
return state
})
if (notifyClient) {
store.actions.preview.sendEvent("hover-component", componentId)
}
},
},
links: {
save: async (url, title) => {

View file

@ -0,0 +1,27 @@
import { get, writable } from "svelte/store"
import { store as builder } from "builderStore"
export const getHoverStore = () => {
const initialValue = {
componentId: null,
}
const store = writable(initialValue)
const update = (componentId, notifyClient = true) => {
if (componentId === get(store).componentId) {
return
}
store.update(state => {
state.componentId = componentId
return state
})
if (notifyClient) {
builder.actions.preview.sendEvent("hover-component", componentId)
}
}
return {
subscribe: store.subscribe,
actions: { update },
}
}

View file

@ -88,8 +88,12 @@
hasValidated = false
})
}
$: valid =
getErrorCount(errors) === 0 && allRequiredAttributesSet(relationshipType)
getErrorCount(errors) === 0 &&
allRequiredAttributesSet(relationshipType) &&
fromId &&
toId
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
$: isManyToOne =
relationshipType === RelationshipType.MANY_TO_ONE ||

View file

@ -5,6 +5,7 @@
import { store } from "builderStore"
import { Helpers } from "@budibase/bbui"
import { getEventContextBindings } from "builderStore/dataBinding"
import { cloneDeep, isEqual } from "lodash/fp"
export let componentInstance
export let componentBindings
@ -17,8 +18,13 @@
const dispatch = createEventDispatcher()
let focusItem
let cachedValue
$: buttonList = sanitizeValue(value) || []
$: if (!isEqual(value, cachedValue)) {
cachedValue = cloneDeep(value)
}
$: buttonList = sanitizeValue(cachedValue) || []
$: buttonCount = buttonList.length
$: eventContextBindings = getEventContextBindings({
componentInstance,

View file

@ -1,7 +1,7 @@
<script>
import { get } from "svelte/store"
import { onMount, onDestroy } from "svelte"
import { store, selectedScreen, currentAsset } from "builderStore"
import { store, selectedScreen, currentAsset, hoverStore } from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import {
ProgressCircle,
@ -118,7 +118,7 @@
} else if (type === "select-component" && data.id) {
$store.selectedComponentId = data.id
} else if (type === "hover-component") {
store.actions.components.hover(data.id, false)
hoverStore.actions.update(data.id, false)
} else if (type === "update-prop") {
await store.actions.components.updateSetting(data.prop, data.value)
} else if (type === "update-styles") {

View file

@ -5,6 +5,7 @@
selectedComponentPath,
selectedComponent,
selectedScreen,
hoverStore,
} from "builderStore"
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import NavItem from "components/common/NavItem.svelte"
@ -90,7 +91,7 @@
return findComponentPath($selectedComponent, component._id)?.length > 0
}
const hover = store.actions.components.hover
const hover = hoverStore.actions.update
</script>
<ul>
@ -111,7 +112,7 @@
on:dragover={dragover(component, index)}
on:iconClick={() => toggleNodeOpen(component._id)}
on:drop={onDrop}
hovering={$store.hoveredComponentId === component._id}
hovering={$hoverStore.componentId === component._id}
on:mouseenter={() => hover(component._id)}
on:mouseleave={() => hover(null)}
text={getComponentText(component)}

View file

@ -1,7 +1,12 @@
<script>
import { notifications, Icon, Body } from "@budibase/bbui"
import { isActive, goto } from "@roxi/routify"
import { store, selectedScreen, userSelectedResourceMap } from "builderStore"
import {
store,
selectedScreen,
userSelectedResourceMap,
hoverStore,
} from "builderStore"
import NavItem from "components/common/NavItem.svelte"
import ComponentTree from "./ComponentTree.svelte"
import { dndStore, DropPosition } from "./dndStore.js"
@ -36,7 +41,7 @@
scrolling = e.target.scrollTop !== 0
}
const hover = store.actions.components.hover
const hover = hoverStore.actions.update
</script>
<div class="components">
@ -60,7 +65,7 @@
icon="WebPage"
on:drop={onDrop}
on:click={() => ($store.selectedComponentId = screenComponentId)}
hovering={$store.hoveredComponentId === screenComponentId}
hovering={$hoverStore.componentId === screenComponentId}
on:mouseenter={() => hover(screenComponentId)}
on:mouseleave={() => hover(null)}
id="component-screen"
@ -79,7 +84,7 @@
: "VisibilityOff"}
on:drop={onDrop}
on:click={() => ($store.selectedComponentId = navComponentId)}
hovering={$store.hoveredComponentId === navComponentId}
hovering={$hoverStore.componentId === navComponentId}
on:mouseenter={() => hover(navComponentId)}
on:mouseleave={() => hover(null)}
id="component-nav"

View file

@ -3969,6 +3969,12 @@
"key": "allowManualEntry",
"defaultValue": false
},
{
"type": "boolean",
"label": "Auto confirm",
"key": "autoConfirm",
"defaultValue": false
},
{
"type": "boolean",
"label": "Play sound on scan",

View file

@ -14,11 +14,13 @@
export let value
export let disabled = false
export let allowManualEntry = false
export let autoConfirm = false
export let scanButtonText = "Scan code"
export let beepOnScan = false
export let beepFrequency = 2637
export let customFrequency = 1046
export let preferredCamera = "environment"
export let validator
const dispatch = createEventDispatcher()
@ -41,6 +43,9 @@
beep()
}
dispatch("change", decodedText)
if (autoConfirm && !validator?.(decodedText)) {
camModal?.hide()
}
}
}
@ -127,7 +132,11 @@
<div class="scanner-video-wrapper">
{#if value && !manualMode}
<div class="scanner-value field-display">
<StatusLight positive />
{#if validator?.(value)}
<StatusLight negative />
{:else}
<StatusLight positive />
{/if}
{value}
</div>
{/if}
@ -183,11 +192,16 @@
</div>
{#if cameraEnabled === true}
<div class="code-wrap">
{#if value}
{#if value && !validator?.(value)}
<div class="scanner-value">
<StatusLight positive />
{value}
</div>
{:else if value && validator?.(value)}
<div class="scanner-value">
<StatusLight negative />
{value}
</div>
{:else}
<div class="scanner-value">
<StatusLight neutral />

View file

@ -11,6 +11,7 @@
export let defaultValue = ""
export let onChange
export let allowManualEntry
export let autoConfirm
export let scanButtonText
export let beepOnScan
export let beepFrequency
@ -49,11 +50,13 @@
on:change={handleUpdate}
disabled={fieldState.disabled || fieldState.readonly}
{allowManualEntry}
{autoConfirm}
scanButtonText={scanText}
{beepOnScan}
{beepFrequency}
{customFrequency}
{preferredCamera}
validator={fieldState.validator}
/>
{/if}
</Field>

View file

@ -108,10 +108,13 @@
}
}
$: forceFetchRows(filter)
$: forceFetchRows(filter, fieldApi)
$: debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
const forceFetchRows = async () => {
if (!fieldApi) {
return
}
// if the filter has changed, then we need to reset the options, clear the selection, and re-fetch
optionsObj = {}
fieldApi.setValue([])
@ -236,7 +239,6 @@
bind:searchTerm
loading={$fetch.loading}
bind:open
customPopoverMaxHeight={400}
/>
{/if}
</Field>

View file

@ -34,7 +34,7 @@ const checkAuthorized = async (
const isCreatorApi = permType === PermissionType.CREATOR
const isBuilderApi = permType === PermissionType.BUILDER
const isGlobalBuilder = users.isGlobalBuilder(ctx.user)
const isCreator = users.isCreator(ctx.user)
const isCreator = await users.isCreator(ctx.user)
const isBuilder = appId
? users.isBuilder(ctx.user, appId)
: users.hasBuilderPermissions(ctx.user)

View file

@ -84,7 +84,7 @@ describe("syncGlobalUsers", () => {
await syncGlobalUsers()
const metadata = await rawUserMetadata()
expect(metadata).toHaveLength(3)
expect(metadata).toHaveLength(2)
expect(metadata).toContainEqual(
expect.objectContaining({
_id: db.generateUserMetadataID(user1._id!),
@ -121,7 +121,7 @@ describe("syncGlobalUsers", () => {
await syncGlobalUsers()
const metadata = await rawUserMetadata()
expect(metadata).toHaveLength(0)
expect(metadata).toHaveLength(1) //ADMIN user created in test bootstrap still in the application
})
})
})

View file

@ -70,7 +70,7 @@ export function hasAppCreatorPermissions(user?: User | ContextUser): boolean {
return _.flow(
_.get("roles"),
_.values,
_.find(x => x === "CREATOR"),
_.find(x => ["CREATOR", "ADMIN"].includes(x)),
x => !!x
)(user)
}

View file

@ -91,6 +91,9 @@ export async function getSelf(ctx: any) {
id: userId,
}
// Adjust creators quotas (prevents wrong creators count if user has changed the plan)
await groups.adjustGroupCreatorsQuotas()
// get the main body of the user
const user = await userSdk.db.getUser(userId)
ctx.body = await groups.enrichUserRolesFromGroups(user)