1
0
Fork 0
mirror of synced 2024-07-07 15:25:52 +12:00

Merge remote-tracking branch 'origin/master' into feature/change-global-admin-to-creator

This commit is contained in:
Dean 2023-10-27 10:00:02 +01:00
commit 4b1e7359b0
28 changed files with 798 additions and 238 deletions

View file

@ -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

View file

@ -33,7 +33,6 @@
"build:sdk": "lerna run --stream build:sdk", "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", "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": "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", "restore": "yarn run clean && yarn && yarn run build",
"nuke": "yarn run nuke:packages && yarn run nuke:docker", "nuke": "yarn run nuke:packages && yarn run nuke:docker",
"nuke:packages": "yarn run restore", "nuke:packages": "yarn run restore",

View file

@ -24,17 +24,23 @@
let selectedRows = [] let selectedRows = []
let customRenderers = [] 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) $: selectedRows, dispatch("selectionUpdated", selectedRows)
$: isUsersTable = tableId === TableNames.USERS $: isUsersTable = tableId === TableNames.USERS
$: data && resetSelectedRows() $: data && resetSelectedRows()
$: {
Object.values(schema || {}).forEach(col => {
if (!canBeSortColumn(col.type)) {
col.sortable = false
}
})
}
$: { $: {
if (isUsersTable) { if (isUsersTable) {
customRenderers = [ customRenderers = [
@ -44,24 +50,24 @@
}, },
] ]
UNEDITABLE_USER_FIELDS.forEach(field => { UNEDITABLE_USER_FIELDS.forEach(field => {
if (schema[field]) { if (parsedSchema[field]) {
schema[field].editable = false parsedSchema[field].editable = false
} }
}) })
if (schema.email) { if (parsedSchema.email) {
schema.email.displayName = "Email" parsedSchema.email.displayName = "Email"
} }
if (schema.roleId) { if (parsedSchema.roleId) {
schema.roleId.displayName = "Role" parsedSchema.roleId.displayName = "Role"
} }
if (schema.firstName) { if (parsedSchema.firstName) {
schema.firstName.displayName = "First Name" parsedSchema.firstName.displayName = "First Name"
} }
if (schema.lastName) { if (parsedSchema.lastName) {
schema.lastName.displayName = "Last Name" parsedSchema.lastName.displayName = "Last Name"
} }
if (schema.status) { if (parsedSchema.status) {
schema.status.displayName = "Status" parsedSchema.status.displayName = "Status"
} }
} }
} }
@ -97,7 +103,7 @@
<div class="table-wrapper"> <div class="table-wrapper">
<Table <Table
{data} {data}
{schema} schema={parsedSchema}
{loading} {loading}
{customRenderers} {customRenderers}
{rowCount} {rowCount}

View file

@ -20,6 +20,7 @@
let type = "internal" let type = "internal"
$: name = view.name $: name = view.name
$: schema = view.schema
$: calculation = view.calculation $: calculation = view.calculation
$: supportedFormats = Object.values(ROW_EXPORT_FORMATS).filter(key => { $: supportedFormats = Object.values(ROW_EXPORT_FORMATS).filter(key => {
@ -61,7 +62,7 @@
<Table <Table
title={decodeURI(name)} title={decodeURI(name)}
schema={view.schema} {schema}
tableId={view.tableId} tableId={view.tableId}
{data} {data}
{loading} {loading}

View file

@ -23,6 +23,7 @@ import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
import GridColumnEditor from "./controls/ColumnEditor/GridColumnEditor.svelte" import GridColumnEditor from "./controls/ColumnEditor/GridColumnEditor.svelte"
import BarButtonList from "./controls/BarButtonList.svelte" import BarButtonList from "./controls/BarButtonList.svelte"
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte" import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
import ButtonConfiguration from "./controls/ButtonConfiguration/ButtonConfiguration.svelte"
import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte" import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte"
const componentMap = { const componentMap = {
@ -48,6 +49,7 @@ const componentMap = {
"filter/relationship": RelationshipFilterEditor, "filter/relationship": RelationshipFilterEditor,
url: URLSelect, url: URLSelect,
fieldConfiguration: FieldConfiguration, fieldConfiguration: FieldConfiguration,
buttonConfiguration: ButtonConfiguration,
columns: ColumnEditor, columns: ColumnEditor,
"columns/basic": BasicColumnEditor, "columns/basic": BasicColumnEditor,
"columns/grid": GridColumnEditor, "columns/grid": GridColumnEditor,

View file

@ -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>

View file

@ -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>

View file

@ -1,10 +1,10 @@
<script> <script>
import { Icon } from "@budibase/bbui"
import { dndzone } from "svelte-dnd-action" import { dndzone } from "svelte-dnd-action"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { generate } from "shortid" import { generate } from "shortid"
import { setContext } from "svelte" 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 items = []
export let showHandle = true export let showHandle = true
@ -12,6 +12,7 @@
export let listTypeProps = {} export let listTypeProps = {}
export let listItemKey export let listItemKey
export let draggable = true export let draggable = true
export let focus
let store = writable({ let store = writable({
selected: null, selected: null,
@ -27,6 +28,10 @@
setContext("draggable", store) setContext("draggable", store)
$: if (focus && store) {
get(store).actions.select(focus)
}
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const flipDurationMs = 150 const flipDurationMs = 150
@ -82,13 +87,16 @@
> >
{#each draggableItems as draggable (draggable.id)} {#each draggableItems as draggable (draggable.id)}
<li <li
on:mousedown={() => {
get(store).actions.select()
}}
bind:this={anchors[draggable.id]} bind:this={anchors[draggable.id]}
class:highlighted={draggable.id === $store.selected} class:highlighted={draggable.id === $store.selected}
> >
<div class="left-content"> <div class="left-content">
{#if showHandle} {#if showHandle}
<div class="handle" aria-label="drag-handle"> <div class="handle">
<Icon name="DragHandle" size="XL" /> <DragHandle />
</div> </div>
{/if} {/if}
</div> </div>
@ -142,8 +150,9 @@
border-top-right-radius: 4px; border-top-right-radius: 4px;
} }
.list-wrap > li:last-child { .list-wrap > li:last-child {
border-top-left-radius: var(--spectrum-table-regular-border-radius); border-bottom-left-radius: 4px;
border-top-right-radius: var(--spectrum-table-regular-border-radius); border-bottom-right-radius: 4px;
border-bottom: 0px;
} }
.right-content { .right-content {
flex: 1; flex: 1;
@ -153,4 +162,15 @@
padding-left: var(--spacing-s); padding-left: var(--spacing-s);
padding-right: 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> </style>

View file

@ -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

View file

@ -3,31 +3,35 @@
import { store } from "builderStore" import { store } from "builderStore"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { createEventDispatcher } from "svelte" 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" import { getContext } from "svelte"
export let anchor export let anchor
export let field export let componentInstance
export let componentBindings export let componentBindings
export let bindings export let bindings
export let parseSettings
const draggable = getContext("draggable") const draggable = getContext("draggable")
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let popover let popover
let drawers = [] let drawers = []
let pseudoComponentInstance
let open = false 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() popover.hide()
} }
$: if (field) { // Open automatically if the component is marked as selected
pseudoComponentInstance = field $: if (!open && $draggable.selected === componentInstance._id && popover) {
popover.show()
open = true
} }
$: componentDef = store.actions.components.getDefinition( $: componentDef = store.actions.components.getDefinition(
pseudoComponentInstance._component componentInstance._component
) )
$: parsedComponentDef = processComponentDefinitionSettings(componentDef) $: parsedComponentDef = processComponentDefinitionSettings(componentDef)
@ -36,17 +40,16 @@
return {} return {}
} }
const clone = cloneDeep(componentDef) const clone = cloneDeep(componentDef)
const updatedSettings = clone.settings
.filter(setting => setting.key !== "field") if (typeof parseSettings === "function") {
.map(setting => { clone.settings = parseSettings(clone.settings)
return { ...setting, nested: true } }
})
clone.settings = updatedSettings
return clone return clone
} }
const updateSetting = async (setting, value) => { const updateSetting = async (setting, value) => {
const nestedComponentInstance = cloneDeep(pseudoComponentInstance) const nestedComponentInstance = cloneDeep(componentInstance)
const patchFn = store.actions.components.updateComponentSetting( const patchFn = store.actions.components.updateComponentSetting(
setting.key, setting.key,
@ -54,12 +57,26 @@
) )
patchFn(nestedComponentInstance) patchFn(nestedComponentInstance)
const update = { dispatch("change", nestedComponentInstance)
...nestedComponentInstance, }
active: pseudoComponentInstance.active,
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
} }
dispatch("change", update) return { ...cfg, left, top }
} }
</script> </script>
@ -79,11 +96,11 @@
bind:this={popover} bind:this={popover}
on:open={() => { on:open={() => {
drawers = [] drawers = []
$draggable.actions.select(field._id) $draggable.actions.select(componentInstance._id)
}} }}
on:close={() => { on:close={() => {
open = false open = false
if ($draggable.selected == field._id) { if ($draggable.selected == componentInstance._id) {
$draggable.actions.select() $draggable.actions.select()
} }
}} }}
@ -92,33 +109,13 @@
showPopover={drawers.length == 0} showPopover={drawers.length == 0}
clickOutsideOverride={drawers.length > 0} clickOutsideOverride={drawers.length > 0}
maxHeight={600} maxHeight={600}
handlePostionUpdate={(anchorBounds, eleBounds, cfg) => { handlePostionUpdate={customPositionHandler}
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 }
}}
> >
<span class="popover-wrap"> <span class="popover-wrap">
<Layout noPadding noGap> <Layout noPadding noGap>
<div class="type-icon"> <slot name="header" />
<Icon name={parsedComponentDef.icon} />
<span>{field.field}</span>
</div>
<ComponentSettingsSection <ComponentSettingsSection
componentInstance={pseudoComponentInstance} {componentInstance}
componentDefinition={parsedComponentDef} componentDefinition={parsedComponentDef}
isScreen={false} isScreen={false}
onUpdateSetting={updateSetting} onUpdateSetting={updateSetting}
@ -141,20 +138,4 @@
.popover-wrap { .popover-wrap {
background-color: var(--spectrum-alias-background-color-primary); 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> </style>

View file

@ -7,7 +7,7 @@
getComponentBindableProperties, getComponentBindableProperties,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { currentAsset } from "builderStore" import { currentAsset } from "builderStore"
import DraggableList from "../DraggableList.svelte" import DraggableList from "../DraggableList/DraggableList.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { store, selectedScreen } from "builderStore" import { store, selectedScreen } from "builderStore"
import FieldSetting from "./FieldSetting.svelte" import FieldSetting from "./FieldSetting.svelte"
@ -50,7 +50,7 @@
updateSanitsedFields(sanitisedValue) updateSanitsedFields(sanitisedValue)
unconfigured = buildUnconfiguredOptions(schema, sanitisedFields) unconfigured = buildUnconfiguredOptions(schema, sanitisedFields)
fieldList = [...sanitisedFields, ...unconfigured] fieldList = [...sanitisedFields, ...unconfigured]
.map(buildSudoInstance) .map(buildPseudoInstance)
.filter(x => x != null) .filter(x => x != null)
} }
@ -104,7 +104,7 @@
}) })
} }
const buildSudoInstance = instance => { const buildPseudoInstance = instance => {
if (instance._component) { if (instance._component) {
return instance return instance
} }

View file

@ -1,8 +1,11 @@
<script> <script>
import EditFieldPopover from "./EditFieldPopover.svelte" import EditComponentPopover from "../EditComponentPopover.svelte"
import { Toggle } from "@budibase/bbui" import { Toggle, Icon } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { cloneDeep } from "lodash/fp" 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 item
export let componentBindings export let componentBindings
@ -16,18 +19,43 @@
dispatch("change", { ...cloneDeep(item), active: e.detail }) 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> </script>
<div class="list-item-body"> <div class="list-item-body">
<div class="list-item-left"> <div class="list-item-left">
<EditFieldPopover <EditComponentPopover
{anchor} {anchor}
field={item} componentInstance={item}
{componentBindings} {componentBindings}
{bindings} {bindings}
{parseSettings}
on:change 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>
<div class="list-item-right"> <div class="list-item-right">
<Toggle on:change={onToggle(item)} text="" value={item.active} thin /> <Toggle on:change={onToggle(item)} text="" value={item.active} thin />
@ -53,4 +81,20 @@
.list-item-body { .list-item-body {
justify-content: space-between; 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> </style>

View file

@ -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() { async function runQuery() {
try { try {
await validateQuery()
response = await queries.preview(buildQuery()) response = await queries.preview(buildQuery())
if (response.rows.length === 0) { if (response.rows.length === 0) {
notifications.info("Request did not return any data") notifications.info("Request did not return any data")

View file

@ -91,7 +91,12 @@
/> />
{/if} {/if}
{#if section == "styles"} {#if section == "styles"}
<DesignSection {componentInstance} {componentDefinition} {bindings} /> <DesignSection
{componentInstance}
{componentBindings}
{componentDefinition}
{bindings}
/>
<CustomStylesSection <CustomStylesSection
{componentInstance} {componentInstance}
{componentDefinition} {componentDefinition}

View file

@ -16,18 +16,32 @@
export let isScreen = false export let isScreen = false
export let onUpdateSetting export let onUpdateSetting
export let showSectionTitle = true 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 settings = definition?.settings ?? []
const generalSettings = settings.filter(setting => !setting.section) const generalSettings = settings.filter(
const customSections = settings.filter(setting => setting.section) setting => !setting.section && setting.tag === tag
)
const customSections = settings.filter(
setting => setting.section && setting.tag === tag
)
let sections = [ let sections = [
{ ...(generalSettings?.length
name: "General", ? [
settings: generalSettings, {
}, name: "General",
settings: generalSettings,
},
]
: []),
...(customSections || []), ...(customSections || []),
] ]
@ -132,7 +146,7 @@
<div class="section-info"> <div class="section-info">
<InfoDisplay body={section.info} /> <InfoDisplay body={section.info} />
</div> </div>
{:else if idx === 0 && section.name === "General" && componentDefinition.info} {:else if idx === 0 && section.name === "General" && componentDefinition?.info && !tag}
<InfoDisplay <InfoDisplay
title={componentDefinition.name} title={componentDefinition.name}
body={componentDefinition.info} body={componentDefinition.info}
@ -181,7 +195,7 @@
</DetailSummary> </DetailSummary>
{/if} {/if}
{/each} {/each}
{#if componentDefinition?.block} {#if componentDefinition?.block && !tag}
<DetailSummary name="Eject" collapsible={false}> <DetailSummary name="Eject" collapsible={false}>
<EjectBlockButton /> <EjectBlockButton />
</DetailSummary> </DetailSummary>

View file

@ -1,10 +1,12 @@
<script> <script>
import StyleSection from "./StyleSection.svelte" import StyleSection from "./StyleSection.svelte"
import * as ComponentStyles from "./componentStyles" import * as ComponentStyles from "./componentStyles"
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
export let componentDefinition export let componentDefinition
export let componentInstance export let componentInstance
export let bindings export let bindings
export let componentBindings
const getStyles = def => { const getStyles = def => {
if (!def?.styles?.length) { if (!def?.styles?.length) {
@ -22,6 +24,19 @@
$: styles = getStyles(componentDefinition) $: styles = getStyles(componentDefinition)
</script> </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} {#if styles?.length > 0}
{#each styles as style} {#each styles as style}
<StyleSection <StyleSection

View file

@ -36,6 +36,7 @@
"heading", "heading",
"text", "text",
"button", "button",
"buttongroup",
"tag", "tag",
"spectrumcard", "spectrumcard",
"cardstat", "cardstat",

View file

@ -258,6 +258,186 @@
"description": "Contains your app screens", "description": "Contains your app screens",
"static": true "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": { "button": {
"name": "Button", "name": "Button",
"description": "A basic html button that is ready for styling", "description": "A basic html button that is ready for styling",
@ -2409,7 +2589,6 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{ {
"type": "text", "type": "text",
"label": "Initial form step", "label": "Initial form step",
@ -5391,38 +5570,6 @@
"section": true, "section": true,
"name": "Fields", "name": "Fields",
"settings": [ "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", "type": "fieldConfiguration",
"key": "fields", "key": "fields",
@ -5442,6 +5589,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": [ "context": [
@ -5743,4 +5924,4 @@
} }
] ]
} }
} }

View file

@ -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>

View file

@ -19,6 +19,7 @@ export { default as dataprovider } from "./DataProvider.svelte"
export { default as divider } from "./Divider.svelte" export { default as divider } from "./Divider.svelte"
export { default as screenslot } from "./ScreenSlot.svelte" export { default as screenslot } from "./ScreenSlot.svelte"
export { default as button } from "./Button.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 repeater } from "./Repeater.svelte"
export { default as text } from "./Text.svelte" export { default as text } from "./Text.svelte"
export { default as layout } from "./Layout.svelte" export { default as layout } from "./Layout.svelte"

View file

@ -44,7 +44,7 @@ RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
WORKDIR /string-templates WORKDIR /string-templates
COPY packages/string-templates/package.json package.json COPY packages/string-templates/package.json package.json
RUN ../scripts/removeWorkspaceDependencies.sh 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 . COPY packages/string-templates .
@ -57,7 +57,7 @@ COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
RUN ./scripts/removeWorkspaceDependencies.sh 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 \
# Remove unneeded data from file system to reduce image size # Remove unneeded data from file system to reduce image size
&& yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python jq \ && 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 && rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp

View file

@ -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 { getUserMetadataParams, InternalTables } from "../../db/utils"
import { Database, Role, UserCtx, UserRoles } from "@budibase/types" import { Database, Role, UserCtx, UserRoles } from "@budibase/types"
import { sdk as sharedSdk } from "@budibase/shared-core" import { sdk as sharedSdk } from "@budibase/shared-core"
@ -143,4 +149,20 @@ export async function accessible(ctx: UserCtx) {
} else { } else {
ctx.body = await roles.getUserRoleIdHierarchy(roleId!) 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]
}
} }

View file

@ -158,5 +158,25 @@ describe("/roles", () => {
expect(res.body.length).toBe(1) expect(res.body.length).toBe(1)
expect(res.body[0]).toBe("PUBLIC") 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")
})
}) })
}) })

View file

@ -1,5 +1,5 @@
const setup = require("./utilities") const setup = require("./utilities")
const { basicScreen } = setup.structures const { basicScreen, powerScreen } = setup.structures
const { checkBuilderEndpoint, runInProd } = require("./utilities/TestFunctions") const { checkBuilderEndpoint, runInProd } = require("./utilities/TestFunctions")
const { roles } = require("@budibase/backend-core") const { roles } = require("@budibase/backend-core")
const { BUILTIN_ROLE_IDS } = roles const { BUILTIN_ROLE_IDS } = roles
@ -12,19 +12,14 @@ const route = "/test"
describe("/routing", () => { describe("/routing", () => {
let request = setup.getRequest() let request = setup.getRequest()
let config = setup.getConfig() let config = setup.getConfig()
let screen, screen2 let basic, power
afterAll(setup.afterAll) afterAll(setup.afterAll)
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
screen = basicScreen() basic = await config.createScreen(basicScreen(route))
screen.routing.route = route power = await config.createScreen(powerScreen(route))
screen = await config.createScreen(screen)
screen2 = basicScreen()
screen2.routing.roleId = BUILTIN_ROLE_IDS.POWER
screen2.routing.route = route
screen2 = await config.createScreen(screen2)
await config.publish() await config.publish()
}) })
@ -61,8 +56,8 @@ describe("/routing", () => {
expect(res.body.routes[route]).toEqual({ expect(res.body.routes[route]).toEqual({
subpaths: { subpaths: {
[route]: { [route]: {
screenId: screen._id, screenId: basic._id,
roleId: screen.routing.roleId roleId: basic.routing.roleId
} }
} }
}) })
@ -80,8 +75,8 @@ describe("/routing", () => {
expect(res.body.routes[route]).toEqual({ expect(res.body.routes[route]).toEqual({
subpaths: { subpaths: {
[route]: { [route]: {
screenId: screen2._id, screenId: power._id,
roleId: screen2.routing.roleId roleId: power.routing.roleId
} }
} }
}) })
@ -101,8 +96,8 @@ describe("/routing", () => {
expect(res.body.routes).toBeDefined() expect(res.body.routes).toBeDefined()
expect(res.body.routes[route].subpaths[route]).toBeDefined() expect(res.body.routes[route].subpaths[route]).toBeDefined()
const subpath = res.body.routes[route].subpaths[route] const subpath = res.body.routes[route].subpaths[route]
expect(subpath.screens[screen2.routing.roleId]).toEqual(screen2._id) expect(subpath.screens[power.routing.roleId]).toEqual(power._id)
expect(subpath.screens[screen.routing.roleId]).toEqual(screen._id) expect(subpath.screens[basic.routing.roleId]).toEqual(basic._id)
}) })
it("make sure it is a builder only endpoint", async () => { it("make sure it is a builder only endpoint", async () => {

View file

@ -1,7 +1,15 @@
import { roles } from "@budibase/backend-core" import { roles } from "@budibase/backend-core"
import { BASE_LAYOUT_PROP_IDS } from "./layouts" 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 { return {
description: "", description: "",
url: "", url: "",
@ -40,8 +48,8 @@ export function createHomeScreen() {
gap: "M", gap: "M",
}, },
routing: { routing: {
route: "/", route: config.route,
roleId: roles.BUILTIN_ROLE_IDS.BASIC, roleId: config.roleId,
}, },
name: "home-screen", name: "home-screen",
} }

View file

@ -20,6 +20,7 @@ import {
SourceName, SourceName,
Table, Table,
} from "@budibase/types" } from "@budibase/types"
const { BUILTIN_ROLE_IDS } = roles
export function basicTable(): Table { export function basicTable(): Table {
return { return {
@ -322,8 +323,22 @@ export function basicUser(role: string) {
} }
} }
export function basicScreen() { export function basicScreen(route: string = "/") {
return createHomeScreen() 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() { export function basicLayout() {

View file

@ -19,7 +19,7 @@ RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
WORKDIR /string-templates WORKDIR /string-templates
COPY packages/string-templates/package.json package.json COPY packages/string-templates/package.json package.json
RUN ../scripts/removeWorkspaceDependencies.sh 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 . 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 ../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 # Remove unneeded data from file system to reduce image size
RUN apk del .gyp \ RUN apk del .gyp \
&& yarn cache clean && yarn cache clean

View file

@ -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