Merge branch 'master' of github.com:Budibase/budibase into global-bindings
This commit is contained in:
commit
1ad30d45ba
|
@ -1,72 +0,0 @@
|
|||
name: Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
REGISTRY_URL: registry.hub.docker.com
|
||||
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
jobs:
|
||||
build:
|
||||
name: "build"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "yarn"
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Setup Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Run Yarn
|
||||
run: yarn
|
||||
- name: Run Yarn Build
|
||||
run: yarn build --scope @budibase/server --scope @budibase/worker
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_API_KEY }}
|
||||
- name: Get the latest release version
|
||||
id: version
|
||||
run: |
|
||||
release_version=$(cat lerna.json | jq -r '.version')
|
||||
echo $release_version
|
||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||
- name: Tag and release Budibase service docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
pull: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: BUDIBASE_VERSION=0.0.0+test
|
||||
tags: budibase/budibase-test:test
|
||||
file: ./hosting/single/Dockerfile.v2
|
||||
cache-from: type=registry,ref=budibase/budibase-test:test
|
||||
cache-to: type=inline
|
||||
- name: Tag and release Budibase Azure App Service docker image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
TARGETBUILD=aas
|
||||
BUDIBASE_VERSION=0.0.0+test
|
||||
tags: budibase/budibase-test:aas
|
||||
file: ./hosting/single/Dockerfile.v2
|
|
@ -33,7 +33,6 @@
|
|||
"build:sdk": "lerna run --stream build:sdk",
|
||||
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
|
||||
"release": "lerna publish from-package --yes --force-publish --no-git-tag-version --no-push --no-git-reset",
|
||||
"release:develop": "yarn release --dist-tag develop",
|
||||
"restore": "yarn run clean && yarn && yarn run build",
|
||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||
"nuke:packages": "yarn run restore",
|
||||
|
|
|
@ -24,17 +24,23 @@
|
|||
|
||||
let selectedRows = []
|
||||
let customRenderers = []
|
||||
let parsedSchema = {}
|
||||
|
||||
$: if (schema) {
|
||||
parsedSchema = Object.keys(schema).reduce((acc, key) => {
|
||||
acc[key] =
|
||||
typeof schema[key] === "string" ? { type: schema[key] } : schema[key]
|
||||
|
||||
if (!canBeSortColumn(acc[key].type)) {
|
||||
acc[key].sortable = false
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
$: selectedRows, dispatch("selectionUpdated", selectedRows)
|
||||
$: isUsersTable = tableId === TableNames.USERS
|
||||
$: data && resetSelectedRows()
|
||||
$: {
|
||||
Object.values(schema || {}).forEach(col => {
|
||||
if (!canBeSortColumn(col.type)) {
|
||||
col.sortable = false
|
||||
}
|
||||
})
|
||||
}
|
||||
$: {
|
||||
if (isUsersTable) {
|
||||
customRenderers = [
|
||||
|
@ -44,24 +50,24 @@
|
|||
},
|
||||
]
|
||||
UNEDITABLE_USER_FIELDS.forEach(field => {
|
||||
if (schema[field]) {
|
||||
schema[field].editable = false
|
||||
if (parsedSchema[field]) {
|
||||
parsedSchema[field].editable = false
|
||||
}
|
||||
})
|
||||
if (schema.email) {
|
||||
schema.email.displayName = "Email"
|
||||
if (parsedSchema.email) {
|
||||
parsedSchema.email.displayName = "Email"
|
||||
}
|
||||
if (schema.roleId) {
|
||||
schema.roleId.displayName = "Role"
|
||||
if (parsedSchema.roleId) {
|
||||
parsedSchema.roleId.displayName = "Role"
|
||||
}
|
||||
if (schema.firstName) {
|
||||
schema.firstName.displayName = "First Name"
|
||||
if (parsedSchema.firstName) {
|
||||
parsedSchema.firstName.displayName = "First Name"
|
||||
}
|
||||
if (schema.lastName) {
|
||||
schema.lastName.displayName = "Last Name"
|
||||
if (parsedSchema.lastName) {
|
||||
parsedSchema.lastName.displayName = "Last Name"
|
||||
}
|
||||
if (schema.status) {
|
||||
schema.status.displayName = "Status"
|
||||
if (parsedSchema.status) {
|
||||
parsedSchema.status.displayName = "Status"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +103,7 @@
|
|||
<div class="table-wrapper">
|
||||
<Table
|
||||
{data}
|
||||
{schema}
|
||||
schema={parsedSchema}
|
||||
{loading}
|
||||
{customRenderers}
|
||||
{rowCount}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
let type = "internal"
|
||||
|
||||
$: name = view.name
|
||||
$: schema = view.schema
|
||||
$: calculation = view.calculation
|
||||
|
||||
$: supportedFormats = Object.values(ROW_EXPORT_FORMATS).filter(key => {
|
||||
|
@ -61,7 +62,7 @@
|
|||
|
||||
<Table
|
||||
title={decodeURI(name)}
|
||||
schema={view.schema}
|
||||
{schema}
|
||||
tableId={view.tableId}
|
||||
{data}
|
||||
{loading}
|
||||
|
|
|
@ -39,7 +39,15 @@
|
|||
allowCreator
|
||||
) => {
|
||||
if (allowedRoles?.length) {
|
||||
return roles.filter(role => allowedRoles.includes(role._id))
|
||||
const filteredRoles = roles.filter(role =>
|
||||
allowedRoles.includes(role._id)
|
||||
)
|
||||
return [
|
||||
...filteredRoles,
|
||||
...(allowedRoles.includes(Constants.Roles.CREATOR)
|
||||
? [{ _id: Constants.Roles.CREATOR, name: "Creator", enabled: false }]
|
||||
: []),
|
||||
]
|
||||
}
|
||||
let newRoles = [...roles]
|
||||
|
||||
|
@ -129,8 +137,9 @@
|
|||
getOptionColour={getColor}
|
||||
getOptionIcon={getIcon}
|
||||
isOptionEnabled={option =>
|
||||
option._id !== Constants.Roles.CREATOR ||
|
||||
$licensing.perAppBuildersEnabled}
|
||||
(option._id !== Constants.Roles.CREATOR ||
|
||||
$licensing.perAppBuildersEnabled) &&
|
||||
option.enabled !== false}
|
||||
{placeholder}
|
||||
{error}
|
||||
/>
|
||||
|
|
|
@ -23,6 +23,7 @@ import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
|||
import GridColumnEditor from "./controls/ColumnEditor/GridColumnEditor.svelte"
|
||||
import BarButtonList from "./controls/BarButtonList.svelte"
|
||||
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
||||
import ButtonConfiguration from "./controls/ButtonConfiguration/ButtonConfiguration.svelte"
|
||||
import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte"
|
||||
|
||||
const componentMap = {
|
||||
|
@ -48,6 +49,7 @@ const componentMap = {
|
|||
"filter/relationship": RelationshipFilterEditor,
|
||||
url: URLSelect,
|
||||
fieldConfiguration: FieldConfiguration,
|
||||
buttonConfiguration: ButtonConfiguration,
|
||||
columns: ColumnEditor,
|
||||
"columns/basic": BasicColumnEditor,
|
||||
"columns/grid": GridColumnEditor,
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
<script>
|
||||
import DraggableList from "../DraggableList/DraggableList.svelte"
|
||||
import ButtonSetting from "./ButtonSetting.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { store } from "builderStore"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
|
||||
export let componentBindings
|
||||
export let bindings
|
||||
export let value
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let focusItem
|
||||
|
||||
$: buttonList = sanitizeValue(value) || []
|
||||
$: buttonCount = buttonList.length
|
||||
$: itemProps = {
|
||||
componentBindings: componentBindings || [],
|
||||
bindings,
|
||||
removeButton,
|
||||
canRemove: buttonCount > 1,
|
||||
}
|
||||
|
||||
const sanitizeValue = val => {
|
||||
return val?.map(button => {
|
||||
return button._component ? button : buildPseudoInstance(button)
|
||||
})
|
||||
}
|
||||
|
||||
const processItemUpdate = e => {
|
||||
const updatedField = e.detail
|
||||
const newButtonList = [...buttonList]
|
||||
const fieldIdx = newButtonList.findIndex(pSetting => {
|
||||
return pSetting._id === updatedField?._id
|
||||
})
|
||||
if (fieldIdx === -1) {
|
||||
newButtonList.push(updatedField)
|
||||
} else {
|
||||
newButtonList[fieldIdx] = updatedField
|
||||
}
|
||||
dispatch("change", newButtonList)
|
||||
}
|
||||
|
||||
const listUpdated = e => {
|
||||
dispatch("change", [...e.detail])
|
||||
}
|
||||
|
||||
const buildPseudoInstance = cfg => {
|
||||
return store.actions.components.createInstance(
|
||||
`@budibase/standard-components/button`,
|
||||
{
|
||||
_instanceName: Helpers.uuid(),
|
||||
text: cfg.text,
|
||||
type: cfg.type || "primary",
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
const addButton = () => {
|
||||
const newButton = buildPseudoInstance({
|
||||
text: `Button ${buttonCount + 1}`,
|
||||
})
|
||||
dispatch("change", [...buttonList, newButton])
|
||||
focusItem = newButton._id
|
||||
}
|
||||
|
||||
const removeButton = id => {
|
||||
dispatch(
|
||||
"change",
|
||||
buttonList.filter(button => button._id !== id)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="button-configuration">
|
||||
{#if buttonCount}
|
||||
<DraggableList
|
||||
on:change={listUpdated}
|
||||
on:itemChange={processItemUpdate}
|
||||
items={buttonList}
|
||||
listItemKey={"_id"}
|
||||
listType={ButtonSetting}
|
||||
listTypeProps={itemProps}
|
||||
focus={focusItem}
|
||||
draggable={buttonCount > 1}
|
||||
/>
|
||||
|
||||
<div class="list-footer" on:click={addButton}>
|
||||
<div class="add-button">Add button</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.button-configuration :global(.spectrum-ActionButton) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-configuration :global(.list-wrap > li:last-child),
|
||||
.button-configuration :global(.list-wrap) {
|
||||
border-bottom-left-radius: unset;
|
||||
border-bottom-right-radius: unset;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.list-footer {
|
||||
width: 100%;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: var(
|
||||
--spectrum-table-background-color,
|
||||
var(--spectrum-global-color-gray-50)
|
||||
);
|
||||
transition: background-color ease-in-out 130ms;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border: 1px solid
|
||||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
margin: var(--spacing-s);
|
||||
}
|
||||
|
||||
.list-footer:hover {
|
||||
background-color: var(
|
||||
--spectrum-table-row-background-color-hover,
|
||||
var(--spectrum-alias-highlight-hover)
|
||||
);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,64 @@
|
|||
<script>
|
||||
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { runtimeToReadableBinding } from "builderStore/dataBinding"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
||||
export let item
|
||||
export let componentBindings
|
||||
export let bindings
|
||||
export let anchor
|
||||
export let removeButton
|
||||
export let canRemove
|
||||
|
||||
$: readableText = isJSBinding(item.text)
|
||||
? "(JavaScript function)"
|
||||
: runtimeToReadableBinding([...bindings, componentBindings], item.text)
|
||||
</script>
|
||||
|
||||
<div class="list-item-body">
|
||||
<div class="list-item-left">
|
||||
<EditComponentPopover
|
||||
{anchor}
|
||||
componentInstance={item}
|
||||
{componentBindings}
|
||||
{bindings}
|
||||
on:change
|
||||
/>
|
||||
<div class="field-label">{readableText || "Button"}</div>
|
||||
</div>
|
||||
<div class="list-item-right">
|
||||
<Icon
|
||||
disabled={!canRemove}
|
||||
size="S"
|
||||
name="Close"
|
||||
hoverable
|
||||
on:click={() => removeButton(item._id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.field-label {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.list-item-body,
|
||||
.list-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
min-width: 0;
|
||||
}
|
||||
.list-item-body {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.list-item-right :global(div.spectrum-Switch) {
|
||||
margin: 0px;
|
||||
}
|
||||
.list-item-body {
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { dndzone } from "svelte-dnd-action"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { generate } from "shortid"
|
||||
import { setContext } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
import { writable, get } from "svelte/store"
|
||||
import DragHandle from "./drag-handle.svelte"
|
||||
|
||||
export let items = []
|
||||
export let showHandle = true
|
||||
|
@ -12,6 +12,7 @@
|
|||
export let listTypeProps = {}
|
||||
export let listItemKey
|
||||
export let draggable = true
|
||||
export let focus
|
||||
|
||||
let store = writable({
|
||||
selected: null,
|
||||
|
@ -27,6 +28,10 @@
|
|||
|
||||
setContext("draggable", store)
|
||||
|
||||
$: if (focus && store) {
|
||||
get(store).actions.select(focus)
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const flipDurationMs = 150
|
||||
|
||||
|
@ -82,13 +87,16 @@
|
|||
>
|
||||
{#each draggableItems as draggable (draggable.id)}
|
||||
<li
|
||||
on:mousedown={() => {
|
||||
get(store).actions.select()
|
||||
}}
|
||||
bind:this={anchors[draggable.id]}
|
||||
class:highlighted={draggable.id === $store.selected}
|
||||
>
|
||||
<div class="left-content">
|
||||
{#if showHandle}
|
||||
<div class="handle" aria-label="drag-handle">
|
||||
<Icon name="DragHandle" size="XL" />
|
||||
<div class="handle">
|
||||
<DragHandle />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -142,8 +150,9 @@
|
|||
border-top-right-radius: 4px;
|
||||
}
|
||||
.list-wrap > li:last-child {
|
||||
border-top-left-radius: var(--spectrum-table-regular-border-radius);
|
||||
border-top-right-radius: var(--spectrum-table-regular-border-radius);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
.right-content {
|
||||
flex: 1;
|
||||
|
@ -153,4 +162,15 @@
|
|||
padding-left: var(--spacing-s);
|
||||
padding-right: var(--spacing-s);
|
||||
}
|
||||
.handle {
|
||||
display: flex;
|
||||
height: var(--spectrum-global-dimension-size-150);
|
||||
}
|
||||
.handle :global(svg) {
|
||||
fill: var(--spectrum-global-color-gray-500);
|
||||
margin-right: var(--spacing-m);
|
||||
margin-left: 2px;
|
||||
width: var(--spectrum-global-dimension-size-65);
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,31 @@
|
|||
<svg
|
||||
class="drag-handle spectrum-Icon spectrum-Icon--sizeS"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m1,11c0.55228,0 1,-0.4477 1,-1c0,-0.5523 -0.44772,-1 -1,-1c-0.55228,0 -1,0.4477 -1,1c0,0.5523 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m1,8c0.55228,0 1,-0.4477 1,-1c0,-0.55228 -0.44772,-1 -1,-1c-0.55228,0 -1,0.44772 -1,1c0,0.5523 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m1,5c0.55228,0 1,-0.44772 1,-1c0,-0.55228 -0.44772,-1 -1,-1c-0.55228,0 -1,0.44772 -1,1c0,0.55228 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m1,2c0.55228,0 1,-0.44772 1,-1c0,-0.55228 -0.44772,-1 -1,-1c-0.55228,0 -1,0.44772 -1,1c0,0.55228 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m4,11c0.5523,0 1,-0.4477 1,-1c0,-0.5523 -0.4477,-1 -1,-1c-0.55228,0 -1,0.4477 -1,1c0,0.5523 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m4,8c0.5523,0 1,-0.4477 1,-1c0,-0.55228 -0.4477,-1 -1,-1c-0.55228,0 -1,0.44772 -1,1c0,0.5523 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m4,5c0.5523,0 1,-0.44772 1,-1c0,-0.55228 -0.4477,-1 -1,-1c-0.55228,0 -1,0.44772 -1,1c0,0.55228 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m4,2c0.5523,0 1,-0.44772 1,-1c0,-0.55228 -0.4477,-1 -1,-1c-0.55228,0 -1,0.44772 -1,1c0,0.55228 0.44772,1 1,1z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -3,31 +3,35 @@
|
|||
import { store } from "builderStore"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import ComponentSettingsSection from "../../../../../pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
|
||||
import ComponentSettingsSection from "../../../../pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
export let anchor
|
||||
export let field
|
||||
export let componentInstance
|
||||
export let componentBindings
|
||||
export let bindings
|
||||
export let parseSettings
|
||||
|
||||
const draggable = getContext("draggable")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let popover
|
||||
let drawers = []
|
||||
let pseudoComponentInstance
|
||||
let open = false
|
||||
|
||||
$: if (open && $draggable.selected && $draggable.selected != field._id) {
|
||||
// Auto hide the component when another item is selected
|
||||
$: if (open && $draggable.selected != componentInstance._id) {
|
||||
popover.hide()
|
||||
}
|
||||
|
||||
$: if (field) {
|
||||
pseudoComponentInstance = field
|
||||
// Open automatically if the component is marked as selected
|
||||
$: if (!open && $draggable.selected === componentInstance._id && popover) {
|
||||
popover.show()
|
||||
open = true
|
||||
}
|
||||
|
||||
$: componentDef = store.actions.components.getDefinition(
|
||||
pseudoComponentInstance._component
|
||||
componentInstance._component
|
||||
)
|
||||
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
|
||||
|
||||
|
@ -36,17 +40,16 @@
|
|||
return {}
|
||||
}
|
||||
const clone = cloneDeep(componentDef)
|
||||
const updatedSettings = clone.settings
|
||||
.filter(setting => setting.key !== "field")
|
||||
.map(setting => {
|
||||
return { ...setting, nested: true }
|
||||
})
|
||||
clone.settings = updatedSettings
|
||||
|
||||
if (typeof parseSettings === "function") {
|
||||
clone.settings = parseSettings(clone.settings)
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
const updateSetting = async (setting, value) => {
|
||||
const nestedComponentInstance = cloneDeep(pseudoComponentInstance)
|
||||
const nestedComponentInstance = cloneDeep(componentInstance)
|
||||
|
||||
const patchFn = store.actions.components.updateComponentSetting(
|
||||
setting.key,
|
||||
|
@ -54,12 +57,26 @@
|
|||
)
|
||||
patchFn(nestedComponentInstance)
|
||||
|
||||
const update = {
|
||||
...nestedComponentInstance,
|
||||
active: pseudoComponentInstance.active,
|
||||
dispatch("change", nestedComponentInstance)
|
||||
}
|
||||
|
||||
dispatch("change", update)
|
||||
const customPositionHandler = (anchorBounds, eleBounds, cfg) => {
|
||||
let { left, top } = cfg
|
||||
let percentageOffset = 30
|
||||
// left-outside
|
||||
left = anchorBounds.left - eleBounds.width - 18
|
||||
|
||||
// shift up from the anchor, if space allows
|
||||
let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset
|
||||
let defaultTop = anchorBounds.top - offsetPos
|
||||
|
||||
if (window.innerHeight - defaultTop < eleBounds.height) {
|
||||
top = window.innerHeight - eleBounds.height - 5
|
||||
} else {
|
||||
top = anchorBounds.top - offsetPos
|
||||
}
|
||||
|
||||
return { ...cfg, left, top }
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -79,11 +96,11 @@
|
|||
bind:this={popover}
|
||||
on:open={() => {
|
||||
drawers = []
|
||||
$draggable.actions.select(field._id)
|
||||
$draggable.actions.select(componentInstance._id)
|
||||
}}
|
||||
on:close={() => {
|
||||
open = false
|
||||
if ($draggable.selected == field._id) {
|
||||
if ($draggable.selected == componentInstance._id) {
|
||||
$draggable.actions.select()
|
||||
}
|
||||
}}
|
||||
|
@ -92,33 +109,13 @@
|
|||
showPopover={drawers.length == 0}
|
||||
clickOutsideOverride={drawers.length > 0}
|
||||
maxHeight={600}
|
||||
handlePostionUpdate={(anchorBounds, eleBounds, cfg) => {
|
||||
let { left, top } = cfg
|
||||
let percentageOffset = 30
|
||||
// left-outside
|
||||
left = anchorBounds.left - eleBounds.width - 18
|
||||
|
||||
// shift up from the anchor, if space allows
|
||||
let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset
|
||||
let defaultTop = anchorBounds.top - offsetPos
|
||||
|
||||
if (window.innerHeight - defaultTop < eleBounds.height) {
|
||||
top = window.innerHeight - eleBounds.height - 5
|
||||
} else {
|
||||
top = anchorBounds.top - offsetPos
|
||||
}
|
||||
|
||||
return { ...cfg, left, top }
|
||||
}}
|
||||
handlePostionUpdate={customPositionHandler}
|
||||
>
|
||||
<span class="popover-wrap">
|
||||
<Layout noPadding noGap>
|
||||
<div class="type-icon">
|
||||
<Icon name={parsedComponentDef.icon} />
|
||||
<span>{field.field}</span>
|
||||
</div>
|
||||
<slot name="header" />
|
||||
<ComponentSettingsSection
|
||||
componentInstance={pseudoComponentInstance}
|
||||
{componentInstance}
|
||||
componentDefinition={parsedComponentDef}
|
||||
isScreen={false}
|
||||
onUpdateSetting={updateSetting}
|
||||
|
@ -141,20 +138,4 @@
|
|||
.popover-wrap {
|
||||
background-color: var(--spectrum-alias-background-color-primary);
|
||||
}
|
||||
.type-icon {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
margin: var(--spacing-xl);
|
||||
margin-bottom: 0px;
|
||||
height: var(--spectrum-alias-item-height-m);
|
||||
padding: 0px var(--spectrum-alias-item-padding-m);
|
||||
border-width: var(--spectrum-actionbutton-border-size);
|
||||
border-radius: var(--spectrum-alias-border-radius-regular);
|
||||
border: 1px solid
|
||||
var(
|
||||
--spectrum-actionbutton-m-border-color,
|
||||
var(--spectrum-alias-border-color)
|
||||
);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -7,7 +7,7 @@
|
|||
getComponentBindableProperties,
|
||||
} from "builderStore/dataBinding"
|
||||
import { currentAsset } from "builderStore"
|
||||
import DraggableList from "../DraggableList.svelte"
|
||||
import DraggableList from "../DraggableList/DraggableList.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { store, selectedScreen } from "builderStore"
|
||||
import FieldSetting from "./FieldSetting.svelte"
|
||||
|
@ -50,7 +50,7 @@
|
|||
updateSanitsedFields(sanitisedValue)
|
||||
unconfigured = buildUnconfiguredOptions(schema, sanitisedFields)
|
||||
fieldList = [...sanitisedFields, ...unconfigured]
|
||||
.map(buildSudoInstance)
|
||||
.map(buildPseudoInstance)
|
||||
.filter(x => x != null)
|
||||
}
|
||||
|
||||
|
@ -104,7 +104,7 @@
|
|||
})
|
||||
}
|
||||
|
||||
const buildSudoInstance = instance => {
|
||||
const buildPseudoInstance = instance => {
|
||||
if (instance._component) {
|
||||
return instance
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
<script>
|
||||
import EditFieldPopover from "./EditFieldPopover.svelte"
|
||||
import { Toggle } from "@budibase/bbui"
|
||||
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||
import { Toggle, Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { store } from "builderStore"
|
||||
import { runtimeToReadableBinding } from "builderStore/dataBinding"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
||||
export let item
|
||||
export let componentBindings
|
||||
|
@ -16,18 +19,43 @@
|
|||
dispatch("change", { ...cloneDeep(item), active: e.detail })
|
||||
}
|
||||
}
|
||||
const getReadableText = () => {
|
||||
if (item.label) {
|
||||
return isJSBinding(item.label)
|
||||
? "(JavaScript function)"
|
||||
: runtimeToReadableBinding([...bindings, componentBindings], item.label)
|
||||
}
|
||||
return item.field
|
||||
}
|
||||
|
||||
const parseSettings = settings => {
|
||||
return settings
|
||||
.filter(setting => setting.key !== "field")
|
||||
.map(setting => {
|
||||
return { ...setting, nested: true }
|
||||
})
|
||||
}
|
||||
|
||||
$: readableText = getReadableText(item)
|
||||
$: componentDef = store.actions.components.getDefinition(item._component)
|
||||
</script>
|
||||
|
||||
<div class="list-item-body">
|
||||
<div class="list-item-left">
|
||||
<EditFieldPopover
|
||||
<EditComponentPopover
|
||||
{anchor}
|
||||
field={item}
|
||||
componentInstance={item}
|
||||
{componentBindings}
|
||||
{bindings}
|
||||
{parseSettings}
|
||||
on:change
|
||||
/>
|
||||
<div class="field-label">{item.label || item.field}</div>
|
||||
>
|
||||
<div slot="header" class="type-icon">
|
||||
<Icon name={componentDef.icon} />
|
||||
<span>{item.field}</span>
|
||||
</div>
|
||||
</EditComponentPopover>
|
||||
<div class="field-label">{readableText}</div>
|
||||
</div>
|
||||
<div class="list-item-right">
|
||||
<Toggle on:change={onToggle(item)} text="" value={item.active} thin />
|
||||
|
@ -53,4 +81,20 @@
|
|||
.list-item-body {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.type-icon {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
margin: var(--spacing-xl);
|
||||
margin-bottom: 0px;
|
||||
height: var(--spectrum-alias-item-height-m);
|
||||
padding: 0px var(--spectrum-alias-item-padding-m);
|
||||
border-width: var(--spectrum-actionbutton-border-size);
|
||||
border-radius: var(--spectrum-alias-border-radius-regular);
|
||||
border: 1px solid
|
||||
var(
|
||||
--spectrum-actionbutton-m-border-color,
|
||||
var(--spectrum-alias-border-color)
|
||||
);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -196,8 +196,36 @@
|
|||
}
|
||||
}
|
||||
|
||||
const validateQuery = async () => {
|
||||
const forbiddenBindings = /{{\s?user(\.(\w|\$)*\s?|\s?)}}/g
|
||||
const bindingError = new Error(
|
||||
"'user' is a protected binding and cannot be used"
|
||||
)
|
||||
|
||||
if (forbiddenBindings.test(url)) {
|
||||
throw bindingError
|
||||
}
|
||||
|
||||
if (forbiddenBindings.test(query.fields.requestBody ?? "")) {
|
||||
throw bindingError
|
||||
}
|
||||
|
||||
Object.values(requestBindings).forEach(bindingValue => {
|
||||
if (forbiddenBindings.test(bindingValue)) {
|
||||
throw bindingError
|
||||
}
|
||||
})
|
||||
|
||||
Object.values(query.fields.headers).forEach(headerValue => {
|
||||
if (forbiddenBindings.test(headerValue)) {
|
||||
throw bindingError
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function runQuery() {
|
||||
try {
|
||||
await validateQuery()
|
||||
response = await queries.preview(buildQuery())
|
||||
if (response.rows.length === 0) {
|
||||
notifications.info("Request did not return any data")
|
||||
|
|
|
@ -516,6 +516,13 @@
|
|||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const parseRole = user => {
|
||||
if (user.isAdminOrGlobalBuilder) {
|
||||
return Constants.Roles.CREATOR
|
||||
}
|
||||
return user.role
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
|
@ -725,7 +732,7 @@
|
|||
<RoleSelect
|
||||
footer={getRoleFooter(user)}
|
||||
placeholder={false}
|
||||
value={user.role}
|
||||
value={parseRole(user)}
|
||||
allowRemove={user.role && !user.group}
|
||||
allowPublic={false}
|
||||
allowCreator={true}
|
||||
|
@ -744,7 +751,7 @@
|
|||
autoWidth
|
||||
align="right"
|
||||
allowedRoles={user.isAdminOrGlobalBuilder
|
||||
? [Constants.Roles.ADMIN]
|
||||
? [Constants.Roles.CREATOR]
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -91,7 +91,12 @@
|
|||
/>
|
||||
{/if}
|
||||
{#if section == "styles"}
|
||||
<DesignSection {componentInstance} {componentDefinition} {bindings} />
|
||||
<DesignSection
|
||||
{componentInstance}
|
||||
{componentBindings}
|
||||
{componentDefinition}
|
||||
{bindings}
|
||||
/>
|
||||
<CustomStylesSection
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
|
|
|
@ -16,18 +16,32 @@
|
|||
export let isScreen = false
|
||||
export let onUpdateSetting
|
||||
export let showSectionTitle = true
|
||||
export let tag
|
||||
|
||||
$: sections = getSections(componentInstance, componentDefinition, isScreen)
|
||||
$: sections = getSections(
|
||||
componentInstance,
|
||||
componentDefinition,
|
||||
isScreen,
|
||||
tag
|
||||
)
|
||||
|
||||
const getSections = (instance, definition, isScreen) => {
|
||||
const getSections = (instance, definition, isScreen, tag) => {
|
||||
const settings = definition?.settings ?? []
|
||||
const generalSettings = settings.filter(setting => !setting.section)
|
||||
const customSections = settings.filter(setting => setting.section)
|
||||
const generalSettings = settings.filter(
|
||||
setting => !setting.section && setting.tag === tag
|
||||
)
|
||||
const customSections = settings.filter(
|
||||
setting => setting.section && setting.tag === tag
|
||||
)
|
||||
let sections = [
|
||||
...(generalSettings?.length
|
||||
? [
|
||||
{
|
||||
name: "General",
|
||||
settings: generalSettings,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(customSections || []),
|
||||
]
|
||||
|
||||
|
@ -132,7 +146,7 @@
|
|||
<div class="section-info">
|
||||
<InfoDisplay body={section.info} />
|
||||
</div>
|
||||
{:else if idx === 0 && section.name === "General" && componentDefinition.info}
|
||||
{:else if idx === 0 && section.name === "General" && componentDefinition?.info && !tag}
|
||||
<InfoDisplay
|
||||
title={componentDefinition.name}
|
||||
body={componentDefinition.info}
|
||||
|
@ -181,7 +195,7 @@
|
|||
</DetailSummary>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if componentDefinition?.block}
|
||||
{#if componentDefinition?.block && !tag}
|
||||
<DetailSummary name="Eject" collapsible={false}>
|
||||
<EjectBlockButton />
|
||||
</DetailSummary>
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<script>
|
||||
import StyleSection from "./StyleSection.svelte"
|
||||
import * as ComponentStyles from "./componentStyles"
|
||||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
||||
|
||||
export let componentDefinition
|
||||
export let componentInstance
|
||||
export let bindings
|
||||
export let componentBindings
|
||||
|
||||
const getStyles = def => {
|
||||
if (!def?.styles?.length) {
|
||||
|
@ -22,6 +24,19 @@
|
|||
$: styles = getStyles(componentDefinition)
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Load any general settings or sections tagged as "style"
|
||||
-->
|
||||
<ComponentSettingsSection
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
isScreen={false}
|
||||
showInstanceName={false}
|
||||
{bindings}
|
||||
{componentBindings}
|
||||
tag="style"
|
||||
/>
|
||||
|
||||
{#if styles?.length > 0}
|
||||
{#each styles as style}
|
||||
<StyleSection
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
"heading",
|
||||
"text",
|
||||
"button",
|
||||
"buttongroup",
|
||||
"tag",
|
||||
"spectrumcard",
|
||||
"cardstat",
|
||||
|
|
|
@ -258,6 +258,186 @@
|
|||
"description": "Contains your app screens",
|
||||
"static": true
|
||||
},
|
||||
"buttongroup": {
|
||||
"name": "Button group",
|
||||
"icon": "Button",
|
||||
"hasChildren": false,
|
||||
"settings": [
|
||||
{
|
||||
"section": true,
|
||||
"name": "Buttons",
|
||||
"settings": [
|
||||
{
|
||||
"type": "buttonConfiguration",
|
||||
"key": "buttons",
|
||||
"nested": true,
|
||||
"defaultValue": [
|
||||
{
|
||||
"type": "cta",
|
||||
"text": "Button 1"
|
||||
},
|
||||
{
|
||||
"type": "primary",
|
||||
"text": "Button 2"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Layout",
|
||||
"settings": [
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Direction",
|
||||
"key": "direction",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Column",
|
||||
"value": "column",
|
||||
"barIcon": "ViewColumn",
|
||||
"barTitle": "Column layout"
|
||||
},
|
||||
{
|
||||
"label": "Row",
|
||||
"value": "row",
|
||||
"barIcon": "ViewRow",
|
||||
"barTitle": "Row layout"
|
||||
}
|
||||
],
|
||||
"defaultValue": "row"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Horiz. align",
|
||||
"key": "hAlign",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Left",
|
||||
"value": "left",
|
||||
"barIcon": "AlignLeft",
|
||||
"barTitle": "Align left"
|
||||
},
|
||||
{
|
||||
"label": "Center",
|
||||
"value": "center",
|
||||
"barIcon": "AlignCenter",
|
||||
"barTitle": "Align center"
|
||||
},
|
||||
{
|
||||
"label": "Right",
|
||||
"value": "right",
|
||||
"barIcon": "AlignRight",
|
||||
"barTitle": "Align right"
|
||||
},
|
||||
{
|
||||
"label": "Stretch",
|
||||
"value": "stretch",
|
||||
"barIcon": "MoveLeftRight",
|
||||
"barTitle": "Align stretched horizontally"
|
||||
}
|
||||
],
|
||||
"defaultValue": "left"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Vert. align",
|
||||
"key": "vAlign",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Top",
|
||||
"value": "top",
|
||||
"barIcon": "AlignTop",
|
||||
"barTitle": "Align top"
|
||||
},
|
||||
{
|
||||
"label": "Middle",
|
||||
"value": "middle",
|
||||
"barIcon": "AlignMiddle",
|
||||
"barTitle": "Align middle"
|
||||
},
|
||||
{
|
||||
"label": "Bottom",
|
||||
"value": "bottom",
|
||||
"barIcon": "AlignBottom",
|
||||
"barTitle": "Align bottom"
|
||||
},
|
||||
{
|
||||
"label": "Stretch",
|
||||
"value": "stretch",
|
||||
"barIcon": "MoveUpDown",
|
||||
"barTitle": "Align stretched vertically"
|
||||
}
|
||||
],
|
||||
"defaultValue": "top"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Size",
|
||||
"key": "size",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Shrink",
|
||||
"value": "shrink",
|
||||
"barIcon": "Minimize",
|
||||
"barTitle": "Shrink container"
|
||||
},
|
||||
{
|
||||
"label": "Grow",
|
||||
"value": "grow",
|
||||
"barIcon": "Maximize",
|
||||
"barTitle": "Grow container"
|
||||
}
|
||||
],
|
||||
"defaultValue": "shrink"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Gap",
|
||||
"key": "gap",
|
||||
"showInBar": true,
|
||||
"barStyle": "picker",
|
||||
"options": [
|
||||
{
|
||||
"label": "None",
|
||||
"value": "N"
|
||||
},
|
||||
{
|
||||
"label": "Small",
|
||||
"value": "S"
|
||||
},
|
||||
{
|
||||
"label": "Medium",
|
||||
"value": "M"
|
||||
},
|
||||
{
|
||||
"label": "Large",
|
||||
"value": "L"
|
||||
}
|
||||
],
|
||||
"defaultValue": "M"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Wrap",
|
||||
"key": "wrap",
|
||||
"showInBar": true,
|
||||
"barIcon": "ModernGridView",
|
||||
"barTitle": "Wrap"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"button": {
|
||||
"name": "Button",
|
||||
"description": "A basic html button that is ready for styling",
|
||||
|
@ -2404,7 +2584,6 @@
|
|||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Initial form step",
|
||||
|
@ -5382,38 +5561,6 @@
|
|||
"section": true,
|
||||
"name": "Fields",
|
||||
"settings": [
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Align labels",
|
||||
"key": "labelPosition",
|
||||
"defaultValue": "left",
|
||||
"options": [
|
||||
{
|
||||
"label": "Left",
|
||||
"value": "left"
|
||||
},
|
||||
{
|
||||
"label": "Above",
|
||||
"value": "above"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Size",
|
||||
"key": "size",
|
||||
"options": [
|
||||
{
|
||||
"label": "Medium",
|
||||
"value": "spectrum--medium"
|
||||
},
|
||||
{
|
||||
"label": "Large",
|
||||
"value": "spectrum--large"
|
||||
}
|
||||
],
|
||||
"defaultValue": "spectrum--medium"
|
||||
},
|
||||
{
|
||||
"type": "fieldConfiguration",
|
||||
"key": "fields",
|
||||
|
@ -5433,6 +5580,40 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "style",
|
||||
"type": "select",
|
||||
"label": "Align labels",
|
||||
"key": "labelPosition",
|
||||
"defaultValue": "left",
|
||||
"options": [
|
||||
{
|
||||
"label": "Left",
|
||||
"value": "left"
|
||||
},
|
||||
{
|
||||
"label": "Above",
|
||||
"value": "above"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "style",
|
||||
"type": "select",
|
||||
"label": "Size",
|
||||
"key": "size",
|
||||
"options": [
|
||||
{
|
||||
"label": "Medium",
|
||||
"value": "spectrum--medium"
|
||||
},
|
||||
{
|
||||
"label": "Large",
|
||||
"value": "spectrum--large"
|
||||
}
|
||||
],
|
||||
"defaultValue": "spectrum--medium"
|
||||
}
|
||||
],
|
||||
"context": [
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import BlockComponent from "../BlockComponent.svelte"
|
||||
import Block from "../Block.svelte"
|
||||
|
||||
export let buttons = []
|
||||
export let direction
|
||||
export let hAlign
|
||||
export let vAlign
|
||||
export let gap = "S"
|
||||
</script>
|
||||
|
||||
<Block>
|
||||
<BlockComponent
|
||||
type="container"
|
||||
props={{
|
||||
direction,
|
||||
hAlign,
|
||||
vAlign,
|
||||
gap,
|
||||
wrap: true,
|
||||
}}
|
||||
>
|
||||
{#each buttons as { text, type, quiet, disabled, onClick, size }}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
text: text || "Button",
|
||||
onClick,
|
||||
type,
|
||||
quiet,
|
||||
disabled,
|
||||
size,
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</BlockComponent>
|
||||
</Block>
|
|
@ -19,6 +19,7 @@ export { default as dataprovider } from "./DataProvider.svelte"
|
|||
export { default as divider } from "./Divider.svelte"
|
||||
export { default as screenslot } from "./ScreenSlot.svelte"
|
||||
export { default as button } from "./Button.svelte"
|
||||
export { default as buttongroup } from "./ButtonGroup.svelte"
|
||||
export { default as repeater } from "./Repeater.svelte"
|
||||
export { default as text } from "./Text.svelte"
|
||||
export { default as layout } from "./Layout.svelte"
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 044bec6447066b215932d6726c437e7ec5a9e42e
|
||||
Subproject commit 5ed0ee2aca9d754d80cd46bae412b24621afa47e
|
|
@ -44,7 +44,7 @@ RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
|||
WORKDIR /string-templates
|
||||
COPY packages/string-templates/package.json package.json
|
||||
RUN ../scripts/removeWorkspaceDependencies.sh package.json
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
|
||||
COPY packages/string-templates .
|
||||
|
||||
|
||||
|
@ -57,7 +57,7 @@ COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.
|
|||
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
||||
RUN ./scripts/removeWorkspaceDependencies.sh package.json
|
||||
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true \
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000 \
|
||||
# Remove unneeded data from file system to reduce image size
|
||||
&& yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python jq \
|
||||
&& rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { context, db as dbCore, events, roles } from "@budibase/backend-core"
|
||||
import {
|
||||
context,
|
||||
db as dbCore,
|
||||
events,
|
||||
roles,
|
||||
Header,
|
||||
} from "@budibase/backend-core"
|
||||
import { getUserMetadataParams, InternalTables } from "../../db/utils"
|
||||
import { Database, Role, UserCtx, UserRoles } from "@budibase/types"
|
||||
import { sdk as sharedSdk } from "@budibase/shared-core"
|
||||
|
@ -143,4 +149,20 @@ export async function accessible(ctx: UserCtx) {
|
|||
} else {
|
||||
ctx.body = await roles.getUserRoleIdHierarchy(roleId!)
|
||||
}
|
||||
|
||||
// If a custom role is provided in the header, filter out higher level roles
|
||||
const roleHeader = ctx.header?.[Header.PREVIEW_ROLE] as string
|
||||
if (roleHeader && !Object.keys(roles.BUILTIN_ROLE_IDS).includes(roleHeader)) {
|
||||
const inherits = (await roles.getRole(roleHeader))?.inherits
|
||||
const orderedRoles = ctx.body.reverse()
|
||||
let filteredRoles = [roleHeader]
|
||||
for (let role of orderedRoles) {
|
||||
filteredRoles = [role, ...filteredRoles]
|
||||
if (role === inherits) {
|
||||
break
|
||||
}
|
||||
}
|
||||
filteredRoles.pop()
|
||||
ctx.body = [roleHeader, ...filteredRoles]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,128 +11,24 @@ const { PermissionType, PermissionLevel } = permissions
|
|||
const router: Router = new Router()
|
||||
|
||||
router
|
||||
/**
|
||||
* @api {get} /api/:sourceId/:rowId/enrich Get an enriched row
|
||||
* @apiName Get an enriched row
|
||||
* @apiGroup rows
|
||||
* @apiPermission table read access
|
||||
* @apiDescription This API is only useful when dealing with rows that have relationships.
|
||||
* Normally when a row is a returned from the API relationships will only have the structure
|
||||
* `{ primaryDisplay: "name", _id: ... }` but this call will return the full related rows
|
||||
* for each relationship instead.
|
||||
*
|
||||
* @apiParam {string} rowId The ID of the row which is to be retrieved and enriched.
|
||||
*
|
||||
* @apiSuccess {object} row The response body will be the enriched row.
|
||||
*/
|
||||
.get(
|
||||
"/api/:sourceId/:rowId/enrich",
|
||||
paramSubResource("sourceId", "rowId"),
|
||||
authorized(PermissionType.TABLE, PermissionLevel.READ),
|
||||
rowController.fetchEnrichedRow
|
||||
)
|
||||
/**
|
||||
* @api {get} /api/:sourceId/rows Get all rows in a table
|
||||
* @apiName Get all rows in a table
|
||||
* @apiGroup rows
|
||||
* @apiPermission table read access
|
||||
* @apiDescription This is a deprecated endpoint that should not be used anymore, instead use the search endpoint.
|
||||
* This endpoint gets all of the rows within the specified table - it is not heavily used
|
||||
* due to its lack of support for pagination. With SQL tables this will retrieve up to a limit and then
|
||||
* will simply stop.
|
||||
*
|
||||
* @apiParam {string} sourceId The ID of the table to retrieve all rows within.
|
||||
*
|
||||
* @apiSuccess {object[]} rows The response body will be an array of all rows found.
|
||||
*/
|
||||
.get(
|
||||
"/api/:sourceId/rows",
|
||||
paramResource("sourceId"),
|
||||
authorized(PermissionType.TABLE, PermissionLevel.READ),
|
||||
rowController.fetch
|
||||
)
|
||||
/**
|
||||
* @api {get} /api/:sourceId/rows/:rowId Retrieve a single row
|
||||
* @apiName Retrieve a single row
|
||||
* @apiGroup rows
|
||||
* @apiPermission table read access
|
||||
* @apiDescription This endpoint retrieves only the specified row. If you wish to retrieve
|
||||
* a row by anything other than its _id field, use the search endpoint.
|
||||
*
|
||||
* @apiParam {string} sourceId The ID of the table to retrieve a row from.
|
||||
* @apiParam {string} rowId The ID of the row to retrieve.
|
||||
*
|
||||
* @apiSuccess {object} body The response body will be the row that was found.
|
||||
*/
|
||||
.get(
|
||||
"/api/:sourceId/rows/:rowId",
|
||||
paramSubResource("sourceId", "rowId"),
|
||||
authorized(PermissionType.TABLE, PermissionLevel.READ),
|
||||
rowController.find
|
||||
)
|
||||
/**
|
||||
* @api {post} /api/:sourceId/search Search for rows in a table
|
||||
* @apiName Search for rows in a table
|
||||
* @apiGroup rows
|
||||
* @apiPermission table read access
|
||||
* @apiDescription This is the primary method of accessing rows in Budibase, the data provider
|
||||
* and data UI in the builder are built atop this. All filtering, sorting and pagination is
|
||||
* handled through this, for internal and external (datasource plus, e.g. SQL) tables.
|
||||
*
|
||||
* @apiParam {string} sourceId The ID of the table to retrieve rows from.
|
||||
*
|
||||
* @apiParam (Body) {boolean} [paginate] If pagination is required then this should be set to true,
|
||||
* defaults to false.
|
||||
* @apiParam (Body) {object} [query] This contains a set of filters which should be applied, if none
|
||||
* specified then the request will be unfiltered. An example with all of the possible query
|
||||
* options has been supplied below.
|
||||
* @apiParam (Body) {number} [limit] This sets a limit for the number of rows that will be returned,
|
||||
* this will be implemented at the database level if supported for performance reasons. This
|
||||
* is useful when paginating to set exactly how many rows per page.
|
||||
* @apiParam (Body) {string} [bookmark] If pagination is enabled then a bookmark will be returned
|
||||
* with each successful search request, this should be supplied back to get the next page.
|
||||
* @apiParam (Body) {object} [sort] If sort is desired this should contain the name of the column to
|
||||
* sort on.
|
||||
* @apiParam (Body) {string} [sortOrder] If sort is enabled then this can be either "descending" or
|
||||
* "ascending" as required.
|
||||
* @apiParam (Body) {string} [sortType] If sort is enabled then you must specify the type of search
|
||||
* being used, either "string" or "number". This is only used for internal tables.
|
||||
*
|
||||
* @apiParamExample {json} Example:
|
||||
* {
|
||||
* "tableId": "ta_70260ff0b85c467ca74364aefc46f26d",
|
||||
* "query": {
|
||||
* "string": {},
|
||||
* "fuzzy": {},
|
||||
* "range": {
|
||||
* "columnName": {
|
||||
* "high": 20,
|
||||
* "low": 10,
|
||||
* }
|
||||
* },
|
||||
* "equal": {
|
||||
* "columnName": "someValue"
|
||||
* },
|
||||
* "notEqual": {},
|
||||
* "empty": {},
|
||||
* "notEmpty": {},
|
||||
* "oneOf": {
|
||||
* "columnName": ["value"]
|
||||
* }
|
||||
* },
|
||||
* "limit": 10,
|
||||
* "sort": "name",
|
||||
* "sortOrder": "descending",
|
||||
* "sortType": "string",
|
||||
* "paginate": true
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {object[]} rows An array of rows that was found based on the supplied parameters.
|
||||
* @apiSuccess {boolean} hasNextPage If pagination was enabled then this specifies whether or
|
||||
* not there is another page after this request.
|
||||
* @apiSuccess {string} bookmark The bookmark to be sent with the next request to get the next
|
||||
* page.
|
||||
*/
|
||||
.post(
|
||||
"/api/:sourceId/search",
|
||||
internalSearchValidator(),
|
||||
|
@ -148,30 +44,6 @@ router
|
|||
authorized(PermissionType.TABLE, PermissionLevel.READ),
|
||||
rowController.search
|
||||
)
|
||||
/**
|
||||
* @api {post} /api/:sourceId/rows Creates a new row
|
||||
* @apiName Creates a new row
|
||||
* @apiGroup rows
|
||||
* @apiPermission table write access
|
||||
* @apiDescription This API will create a new row based on the supplied body. If the
|
||||
* body includes an "_id" field then it will update an existing row if the field
|
||||
* links to one. Please note that "_id", "_rev" and "tableId" are fields that are
|
||||
* already used by Budibase tables and cannot be used for columns.
|
||||
*
|
||||
* @apiParam {string} sourceId The ID of the table to save a row to.
|
||||
*
|
||||
* @apiParam (Body) {string} [_id] If the row exists already then an ID for the row must be provided.
|
||||
* @apiParam (Body) {string} [_rev] If working with an existing row for an internal table its revision
|
||||
* must also be provided.
|
||||
* @apiParam (Body) {string} tableId The ID of the table should also be specified in the row body itself.
|
||||
* @apiParam (Body) {any} [any] Any field supplied in the body will be assessed to see if it matches
|
||||
* a column in the specified table. All other fields will be dropped and not stored.
|
||||
*
|
||||
* @apiSuccess {string} _id The ID of the row that was just saved, if it was just created this
|
||||
* is the rows new ID.
|
||||
* @apiSuccess {string} [_rev] If saving to an internal table a revision will also be returned.
|
||||
* @apiSuccess {object} body The contents of the row that was saved will be returned as well.
|
||||
*/
|
||||
.post(
|
||||
"/api/:sourceId/rows",
|
||||
paramResource("sourceId"),
|
||||
|
@ -179,14 +51,6 @@ router
|
|||
trimViewRowInfo,
|
||||
rowController.save
|
||||
)
|
||||
/**
|
||||
* @api {patch} /api/:sourceId/rows Updates a row
|
||||
* @apiName Update a row
|
||||
* @apiGroup rows
|
||||
* @apiPermission table write access
|
||||
* @apiDescription This endpoint is identical to the row creation endpoint but instead it will
|
||||
* error if an _id isn't provided, it will only function for existing rows.
|
||||
*/
|
||||
.patch(
|
||||
"/api/:sourceId/rows",
|
||||
paramResource("sourceId"),
|
||||
|
@ -194,52 +58,12 @@ router
|
|||
trimViewRowInfo,
|
||||
rowController.patch
|
||||
)
|
||||
/**
|
||||
* @api {post} /api/:sourceId/rows/validate Validate inputs for a row
|
||||
* @apiName Validate inputs for a row
|
||||
* @apiGroup rows
|
||||
* @apiPermission table write access
|
||||
* @apiDescription When attempting to save a row you may want to check if the row is valid
|
||||
* given the table schema, this will iterate through all the constraints on the table and
|
||||
* check if the request body is valid.
|
||||
*
|
||||
* @apiParam {string} sourceId The ID of the table the row is to be validated for.
|
||||
*
|
||||
* @apiParam (Body) {any} [any] Any fields provided in the request body will be tested
|
||||
* against the table schema and constraints.
|
||||
*
|
||||
* @apiSuccess {boolean} valid If inputs provided are acceptable within the table schema this
|
||||
* will be true, if it is not then then errors property will be populated.
|
||||
* @apiSuccess {object} [errors] A key value map of information about fields on the input
|
||||
* which do not match the table schema. The key name will be the column names that have breached
|
||||
* the schema.
|
||||
*/
|
||||
.post(
|
||||
"/api/:sourceId/rows/validate",
|
||||
paramResource("sourceId"),
|
||||
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
||||
rowController.validate
|
||||
)
|
||||
/**
|
||||
* @api {delete} /api/:sourceId/rows Delete rows
|
||||
* @apiName Delete rows
|
||||
* @apiGroup rows
|
||||
* @apiPermission table write access
|
||||
* @apiDescription This endpoint can delete a single row, or delete them in a bulk
|
||||
* fashion.
|
||||
*
|
||||
* @apiParam {string} sourceId The ID of the table the row is to be deleted from.
|
||||
*
|
||||
* @apiParam (Body) {object[]} [rows] If bulk deletion is desired then provide the rows in this
|
||||
* key of the request body that are to be deleted.
|
||||
* @apiParam (Body) {string} [_id] If deleting a single row then provide its ID in this field.
|
||||
* @apiParam (Body) {string} [_rev] If deleting a single row from an internal table then provide its
|
||||
* revision here.
|
||||
*
|
||||
* @apiSuccess {object[]|object} body If deleting bulk then the response body will be an array
|
||||
* of the deleted rows, if deleting a single row then the body will contain a "row" property which
|
||||
* is the deleted row.
|
||||
*/
|
||||
.delete(
|
||||
"/api/:sourceId/rows",
|
||||
paramResource("sourceId"),
|
||||
|
@ -247,20 +71,6 @@ router
|
|||
trimViewRowInfo,
|
||||
rowController.destroy
|
||||
)
|
||||
|
||||
/**
|
||||
* @api {post} /api/:sourceId/rows/exportRows Export Rows
|
||||
* @apiName Export rows
|
||||
* @apiGroup rows
|
||||
* @apiPermission table write access
|
||||
* @apiDescription This API can export a number of provided rows
|
||||
*
|
||||
* @apiParam {string} sourceId The ID of the table the row is to be deleted from.
|
||||
*
|
||||
* @apiParam (Body) {object[]} [rows] The row IDs which are to be exported
|
||||
*
|
||||
* @apiSuccess {object[]|object}
|
||||
*/
|
||||
.post(
|
||||
"/api/:sourceId/rows/exportRows",
|
||||
paramResource("sourceId"),
|
||||
|
|
|
@ -9,99 +9,13 @@ const { BUILDER, PermissionLevel, PermissionType } = permissions
|
|||
const router: Router = new Router()
|
||||
|
||||
router
|
||||
/**
|
||||
* @api {get} /api/tables Fetch all tables
|
||||
* @apiName Fetch all tables
|
||||
* @apiGroup tables
|
||||
* @apiPermission table read access
|
||||
* @apiDescription This endpoint retrieves all of the tables which have been created in
|
||||
* an app. This includes all of the external and internal tables; to tell the difference
|
||||
* between these look for the "type" property on each table, either being "internal" or "external".
|
||||
*
|
||||
* @apiSuccess {object[]} body The response body will be the list of tables that was found - as
|
||||
* this does not take any parameters the only error scenario is no access.
|
||||
*/
|
||||
.get("/api/tables", authorized(BUILDER), tableController.fetch)
|
||||
/**
|
||||
* @api {get} /api/tables/:id Fetch a single table
|
||||
* @apiName Fetch a single table
|
||||
* @apiGroup tables
|
||||
* @apiPermission table read access
|
||||
* @apiDescription Retrieves a single table this could be be internal or external based on
|
||||
* the provided table ID.
|
||||
*
|
||||
* @apiParam {string} id The ID of the table which is to be retrieved.
|
||||
*
|
||||
* @apiSuccess {object[]} body The response body will be the table that was found.
|
||||
*/
|
||||
.get(
|
||||
"/api/tables/:tableId",
|
||||
paramResource("tableId"),
|
||||
authorized(PermissionType.TABLE, PermissionLevel.READ, { schema: true }),
|
||||
tableController.find
|
||||
)
|
||||
/**
|
||||
* @api {post} /api/tables Save a table
|
||||
* @apiName Save a table
|
||||
* @apiGroup tables
|
||||
* @apiPermission builder
|
||||
* @apiDescription Create or update a table with this endpoint, this will function for both internal
|
||||
* external tables.
|
||||
*
|
||||
* @apiParam (Body) {string} [_id] If updating an existing table then the ID of the table must be specified.
|
||||
* @apiParam (Body) {string} [_rev] If updating an existing internal table then the revision must also be specified.
|
||||
* @apiParam (Body) {string} type] This should either be "internal" or "external" depending on the table type -
|
||||
* this will default to internal.
|
||||
* @apiParam (Body) {string} [sourceId] If creating an external table then this should be set to the datasource ID. If
|
||||
* building an internal table this does not need to be set, although it will be returned as "bb_internal".
|
||||
* @apiParam (Body) {string} name The name of the table, this will be used in the UI. To rename the table simply
|
||||
* supply the table structure to this endpoint with the name changed.
|
||||
* @apiParam (Body) {object} schema A key value object which has all of the columns in the table as the keys in this
|
||||
* object. For each column a "type" and "constraints" must be specified, with some types requiring further information.
|
||||
* More information about the schema structure can be found in the Typescript definitions.
|
||||
* @apiParam (Body) {string} [primaryDisplay] The name of the column which should be used when displaying rows
|
||||
* from this table as relationships.
|
||||
* @apiParam (Body) {object[]} [indexes] Specifies the search indexes - this is deprecated behaviour with the introduction
|
||||
* of lucene indexes. This functionality is only available for internal tables.
|
||||
* @apiParam (Body) {object} [_rename] If a column is to be renamed then the "old" column name should be set in this
|
||||
* structure, and the "updated", new column name should also be supplied. The schema should also be updated, this field
|
||||
* lets the server know that a field hasn't just been deleted, that the data has moved to a new name, this will fix
|
||||
* the rows in the table. This functionality is only available for internal tables.
|
||||
* @apiParam (Body) {object[]} [rows] When creating a table using a compatible data source, an array of objects to be imported into the new table can be provided.
|
||||
*
|
||||
* @apiParamExample {json} Example:
|
||||
* {
|
||||
* "_id": "ta_05541307fa0f4044abee071ca2a82119",
|
||||
* "_rev": "10-0fbe4e78f69b255d79f1017e2eeef807",
|
||||
* "type": "internal",
|
||||
* "views": {},
|
||||
* "name": "tableName",
|
||||
* "schema": {
|
||||
* "column": {
|
||||
* "type": "string",
|
||||
* "constraints": {
|
||||
* "type": "string",
|
||||
* "length": {
|
||||
* "maximum": null
|
||||
* },
|
||||
* "presence": false
|
||||
* },
|
||||
* "name": "column"
|
||||
* },
|
||||
* },
|
||||
* "primaryDisplay": "column",
|
||||
* "indexes": [],
|
||||
* "sourceId": "bb_internal",
|
||||
* "_rename": {
|
||||
* "old": "columnName",
|
||||
* "updated": "newColumnName",
|
||||
* },
|
||||
* "rows": []
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {object} table The response body will contain the table structure after being cleaned up and
|
||||
* saved to the database.
|
||||
*/
|
||||
.post(
|
||||
"/api/tables",
|
||||
// allows control over updating a table
|
||||
|
@ -125,41 +39,12 @@ router
|
|||
authorized(BUILDER),
|
||||
tableController.validateExistingTableImport
|
||||
)
|
||||
/**
|
||||
* @api {post} /api/tables/:tableId/:revId Delete a table
|
||||
* @apiName Delete a table
|
||||
* @apiGroup tables
|
||||
* @apiPermission builder
|
||||
* @apiDescription This endpoint will delete a table and all of its associated data, for this reason it is
|
||||
* quite dangerous - it will work for internal and external tables.
|
||||
*
|
||||
* @apiParam {string} tableId The ID of the table which is to be deleted.
|
||||
* @apiParam {string} [revId] If deleting an internal table then the revision must also be supplied (_rev), for
|
||||
* external tables this can simply be set to anything, e.g. "external".
|
||||
*
|
||||
* @apiSuccess {string} message A message stating that the table was deleted successfully.
|
||||
*/
|
||||
.delete(
|
||||
"/api/tables/:tableId/:revId",
|
||||
paramResource("tableId"),
|
||||
authorized(BUILDER),
|
||||
tableController.destroy
|
||||
)
|
||||
/**
|
||||
* @api {post} /api/tables/:tableId/:revId Import CSV to existing table
|
||||
* @apiName Import CSV to existing table
|
||||
* @apiGroup tables
|
||||
* @apiPermission builder
|
||||
* @apiDescription This endpoint will import data to existing tables, internal or external. It is used in combination
|
||||
* with the CSV validation endpoint. Take the output of the CSV validation endpoint and pass it to this endpoint to
|
||||
* import the data; please note this will only import fields that already exist on the table/match the type.
|
||||
*
|
||||
* @apiParam {string} tableId The ID of the table which the data should be imported to.
|
||||
*
|
||||
* @apiParam (Body) {object[]} rows An array of objects representing the rows to be imported, key-value pairs not matching the table schema will be ignored.
|
||||
*
|
||||
* @apiSuccess {string} message A message stating that the data was imported successfully.
|
||||
*/
|
||||
.post(
|
||||
"/api/tables/:tableId/import",
|
||||
paramResource("tableId"),
|
||||
|
|
|
@ -158,5 +158,25 @@ describe("/roles", () => {
|
|||
expect(res.body.length).toBe(1)
|
||||
expect(res.body[0]).toBe("PUBLIC")
|
||||
})
|
||||
|
||||
it("should not fetch higher level accessible roles when a custom role header is provided", async () => {
|
||||
await createRole({
|
||||
name: `CUSTOM_ROLE`,
|
||||
inherits: roles.BUILTIN_ROLE_IDS.BASIC,
|
||||
permissionId: permissions.BuiltinPermissionID.READ_ONLY,
|
||||
version: "name",
|
||||
})
|
||||
const res = await request
|
||||
.get("/api/roles/accessible")
|
||||
.set({
|
||||
...config.defaultHeaders(),
|
||||
"x-budibase-role": "CUSTOM_ROLE"
|
||||
})
|
||||
.expect(200)
|
||||
expect(res.body.length).toBe(3)
|
||||
expect(res.body[0]).toBe("CUSTOM_ROLE")
|
||||
expect(res.body[1]).toBe("BASIC")
|
||||
expect(res.body[2]).toBe("PUBLIC")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const setup = require("./utilities")
|
||||
const { basicScreen } = setup.structures
|
||||
const { basicScreen, powerScreen } = setup.structures
|
||||
const { checkBuilderEndpoint, runInProd } = require("./utilities/TestFunctions")
|
||||
const { roles } = require("@budibase/backend-core")
|
||||
const { BUILTIN_ROLE_IDS } = roles
|
||||
|
@ -12,19 +12,14 @@ const route = "/test"
|
|||
describe("/routing", () => {
|
||||
let request = setup.getRequest()
|
||||
let config = setup.getConfig()
|
||||
let screen, screen2
|
||||
let basic, power
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
screen = basicScreen()
|
||||
screen.routing.route = route
|
||||
screen = await config.createScreen(screen)
|
||||
screen2 = basicScreen()
|
||||
screen2.routing.roleId = BUILTIN_ROLE_IDS.POWER
|
||||
screen2.routing.route = route
|
||||
screen2 = await config.createScreen(screen2)
|
||||
basic = await config.createScreen(basicScreen(route))
|
||||
power = await config.createScreen(powerScreen(route))
|
||||
await config.publish()
|
||||
})
|
||||
|
||||
|
@ -61,8 +56,8 @@ describe("/routing", () => {
|
|||
expect(res.body.routes[route]).toEqual({
|
||||
subpaths: {
|
||||
[route]: {
|
||||
screenId: screen._id,
|
||||
roleId: screen.routing.roleId
|
||||
screenId: basic._id,
|
||||
roleId: basic.routing.roleId
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -80,8 +75,8 @@ describe("/routing", () => {
|
|||
expect(res.body.routes[route]).toEqual({
|
||||
subpaths: {
|
||||
[route]: {
|
||||
screenId: screen2._id,
|
||||
roleId: screen2.routing.roleId
|
||||
screenId: power._id,
|
||||
roleId: power.routing.roleId
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -101,8 +96,8 @@ describe("/routing", () => {
|
|||
expect(res.body.routes).toBeDefined()
|
||||
expect(res.body.routes[route].subpaths[route]).toBeDefined()
|
||||
const subpath = res.body.routes[route].subpaths[route]
|
||||
expect(subpath.screens[screen2.routing.roleId]).toEqual(screen2._id)
|
||||
expect(subpath.screens[screen.routing.roleId]).toEqual(screen._id)
|
||||
expect(subpath.screens[power.routing.roleId]).toEqual(power._id)
|
||||
expect(subpath.screens[basic.routing.roleId]).toEqual(basic._id)
|
||||
})
|
||||
|
||||
it("make sure it is a builder only endpoint", async () => {
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
import { roles } from "@budibase/backend-core"
|
||||
import { BASE_LAYOUT_PROP_IDS } from "./layouts"
|
||||
|
||||
export function createHomeScreen() {
|
||||
export function createHomeScreen(
|
||||
config: {
|
||||
roleId: string
|
||||
route: string
|
||||
} = {
|
||||
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
|
||||
route: "/",
|
||||
}
|
||||
) {
|
||||
return {
|
||||
description: "",
|
||||
url: "",
|
||||
|
@ -40,8 +48,8 @@ export function createHomeScreen() {
|
|||
gap: "M",
|
||||
},
|
||||
routing: {
|
||||
route: "/",
|
||||
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
|
||||
route: config.route,
|
||||
roleId: config.roleId,
|
||||
},
|
||||
name: "home-screen",
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
SourceName,
|
||||
Table,
|
||||
} from "@budibase/types"
|
||||
const { BUILTIN_ROLE_IDS } = roles
|
||||
|
||||
export function basicTable(): Table {
|
||||
return {
|
||||
|
@ -322,8 +323,22 @@ export function basicUser(role: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export function basicScreen() {
|
||||
return createHomeScreen()
|
||||
export function basicScreen(route: string = "/") {
|
||||
return createHomeScreen({
|
||||
roleId: BUILTIN_ROLE_IDS.BASIC,
|
||||
route,
|
||||
})
|
||||
}
|
||||
|
||||
export function powerScreen(route: string = "/") {
|
||||
return createHomeScreen({
|
||||
roleId: BUILTIN_ROLE_IDS.POWER,
|
||||
route,
|
||||
})
|
||||
}
|
||||
|
||||
export function customScreen(config: { roleId: string; route: string }) {
|
||||
return createHomeScreen(config)
|
||||
}
|
||||
|
||||
export function basicLayout() {
|
||||
|
|
|
@ -19,7 +19,7 @@ RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
|||
WORKDIR /string-templates
|
||||
COPY packages/string-templates/package.json package.json
|
||||
RUN ../scripts/removeWorkspaceDependencies.sh package.json
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
|
||||
COPY packages/string-templates .
|
||||
|
||||
|
||||
|
@ -30,7 +30,7 @@ RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-te
|
|||
|
||||
RUN ../scripts/removeWorkspaceDependencies.sh package.json
|
||||
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
|
||||
# Remove unneeded data from file system to reduce image size
|
||||
RUN apk del .gyp \
|
||||
&& yarn cache clean
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { events } from "@budibase/backend-core"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
import { structures, TestConfiguration, mocks } from "../../../../tests"
|
||||
import { UserGroup } from "@budibase/types"
|
||||
import { User, UserGroup } from "@budibase/types"
|
||||
|
||||
mocks.licenses.useGroups()
|
||||
|
||||
|
@ -231,4 +231,39 @@ describe("/api/global/groups", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("with global builder role", () => {
|
||||
let builder: User
|
||||
let group: UserGroup
|
||||
|
||||
beforeAll(async () => {
|
||||
builder = await config.createUser({
|
||||
builder: { global: true },
|
||||
admin: { global: false },
|
||||
})
|
||||
await config.createSession(builder)
|
||||
|
||||
let resp = await config.api.groups.saveGroup(
|
||||
structures.groups.UserGroup()
|
||||
)
|
||||
group = resp.body as UserGroup
|
||||
})
|
||||
|
||||
it("find should return 200", async () => {
|
||||
await config.withUser(builder, async () => {
|
||||
await config.api.groups.searchUsers(group._id!, {
|
||||
emailSearch: `user1`,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("update should return 200", async () => {
|
||||
await config.withUser(builder, async () => {
|
||||
await config.api.groups.updateGroupUsers(group._id!, {
|
||||
add: [builder._id!],
|
||||
remove: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -190,6 +190,16 @@ class TestConfiguration {
|
|||
}
|
||||
}
|
||||
|
||||
async withUser(user: User, f: () => Promise<void>) {
|
||||
const oldUser = this.user
|
||||
this.user = user
|
||||
try {
|
||||
await f()
|
||||
} finally {
|
||||
this.user = oldUser
|
||||
}
|
||||
}
|
||||
|
||||
authHeaders(user: User) {
|
||||
const authToken: AuthToken = {
|
||||
userId: user._id!,
|
||||
|
@ -257,9 +267,10 @@ class TestConfiguration {
|
|||
})
|
||||
}
|
||||
|
||||
async createUser(user?: User) {
|
||||
if (!user) {
|
||||
user = structures.users.user()
|
||||
async createUser(opts?: Partial<User>) {
|
||||
let user = structures.users.user()
|
||||
if (user) {
|
||||
user = { ...user, ...opts }
|
||||
}
|
||||
const response = await this._req(user, null, controllers.users.save)
|
||||
const body = response as SaveUserResponse
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { generator } from "@budibase/backend-core/tests"
|
||||
import { db } from "@budibase/backend-core"
|
||||
import { UserGroupRoles } from "@budibase/types"
|
||||
import { UserGroup as UserGroupType, UserGroupRoles } from "@budibase/types"
|
||||
|
||||
export const UserGroup = () => {
|
||||
export function UserGroup(): UserGroupType {
|
||||
const appsCount = generator.integer({ min: 0, max: 3 })
|
||||
const roles = Array.from({ length: appsCount }).reduce(
|
||||
(p: UserGroupRoles, v) => {
|
||||
|
@ -14,13 +14,11 @@ export const UserGroup = () => {
|
|||
{}
|
||||
)
|
||||
|
||||
let group = {
|
||||
apps: [],
|
||||
return {
|
||||
color: generator.color(),
|
||||
icon: generator.word(),
|
||||
name: generator.word(),
|
||||
roles: roles,
|
||||
users: [],
|
||||
}
|
||||
return group
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/bash
|
||||
version=$1
|
||||
echo "Setting version $version"
|
||||
yarn lerna exec "yarn version --no-git-tag-version --new-version=$version"
|
||||
echo "Updating dependencies"
|
||||
node scripts/syncLocalDependencies.js $version
|
||||
echo "Syncing yarn workspace"
|
||||
yarn
|
Loading…
Reference in New Issue