Merge branch 'master' into refactor/remove-field-types
This commit is contained in:
commit
50b3138acb
|
@ -33,13 +33,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -50,14 +50,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -80,7 +80,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
@ -92,14 +92,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -116,14 +116,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -140,14 +140,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -165,14 +165,14 @@ jobs:
|
|||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -189,13 +189,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: yarn
|
||||
|
@ -219,7 +219,7 @@ jobs:
|
|||
if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase')
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
@ -249,7 +249,7 @@ jobs:
|
|||
|
||||
- name: Check submodule merged to base branch
|
||||
if: ${{ steps.get_pro_commits.outputs.base_commit != '' }}
|
||||
uses: actions/github-script@v4
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
@ -269,7 +269,7 @@ jobs:
|
|||
if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase')
|
||||
steps:
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
@ -299,7 +299,7 @@ jobs:
|
|||
|
||||
- name: Check submodule merged to base branch
|
||||
if: ${{ steps.get_accountportal_commits.outputs.base_commit != '' }}
|
||||
uses: actions/github-script@v4
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
github.event.label.name == 'feature-branch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }}
|
||||
|
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
contains(github.event.pull_request.labels.*.name, 'feature-branch')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
||||
|
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
run: |
|
||||
echo "Ref is not master, you must run this job from master."
|
||||
exit 1
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
@ -53,7 +53,7 @@ jobs:
|
|||
needs: [tag-release]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.15.5",
|
||||
"version": "2.15.7",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 05c90ce55144e260da6688335c16783eab79bf96
|
||||
Subproject commit e9af6686ba135c367e9145a53d26c68325b9bf68
|
|
@ -179,6 +179,7 @@ const environment = {
|
|||
...getPackageJsonFields(),
|
||||
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
||||
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
||||
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
|
||||
_set(key: any, value: any) {
|
||||
process.env[key] = value
|
||||
// @ts-ignore
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
const redis = require("../redis/init")
|
||||
const { v4: uuidv4 } = require("uuid")
|
||||
const { logWarn } = require("../logging")
|
||||
|
||||
import * as redis from "../redis/init"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { logWarn } from "../logging"
|
||||
import env from "../environment"
|
||||
import { Duration } from "../utils"
|
||||
import {
|
||||
Session,
|
||||
ScannedSession,
|
||||
|
@ -10,8 +10,10 @@ import {
|
|||
CreateSession,
|
||||
} from "@budibase/types"
|
||||
|
||||
// a week in seconds
|
||||
const EXPIRY_SECONDS = 86400 * 7
|
||||
// a week expiry is the default
|
||||
const EXPIRY_SECONDS = env.SESSION_EXPIRY_SECONDS
|
||||
? parseInt(env.SESSION_EXPIRY_SECONDS)
|
||||
: Duration.fromDays(7).toSeconds()
|
||||
|
||||
function makeSessionID(userId: string, sessionId: string) {
|
||||
return `${userId}/${sessionId}`
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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={{
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
}
|
|
@ -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 ||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -3969,6 +3969,12 @@
|
|||
"key": "allowManualEntry",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Auto confirm",
|
||||
"key": "autoConfirm",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Play sound on scan",
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
const forceFetchRows = async () => {
|
||||
// if the filter has changed, then we need to reset the options, clear the selection, and re-fetch
|
||||
optionsObj = {}
|
||||
fieldApi.setValue([])
|
||||
fieldApi?.setValue([])
|
||||
selectedValue = []
|
||||
debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||
}
|
||||
|
@ -236,7 +236,6 @@
|
|||
bind:searchTerm
|
||||
loading={$fetch.loading}
|
||||
bind:open
|
||||
customPopoverMaxHeight={400}
|
||||
/>
|
||||
{/if}
|
||||
</Field>
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit ce7722ed4474718596b465dcfd49bef36cab2e42
|
||||
Subproject commit eb9565f568cfef14b336b14eee753119acfdd43b
|
|
@ -60,6 +60,7 @@ const environment = {
|
|||
PLUGINS_DIR: process.env.PLUGINS_DIR || "/plugins",
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
MAX_IMPORT_SIZE_MB: process.env.MAX_IMPORT_SIZE_MB,
|
||||
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
|
||||
// flags
|
||||
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
|
||||
DISABLE_THREADING: process.env.DISABLE_THREADING,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -55,6 +55,7 @@ const environment = {
|
|||
CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600,
|
||||
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
|
||||
ENCRYPTED_TEST_PUBLIC_API_KEY: process.env.ENCRYPTED_TEST_PUBLIC_API_KEY,
|
||||
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
|
||||
/**
|
||||
* Mock the email service in use - links to ethereal hosted emails are logged instead.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue