1
0
Fork 0
mirror of synced 2024-06-28 02:50:50 +12:00

Merge branch 'develop' of github.com:Budibase/budibase into feature/multi-tenants

This commit is contained in:
mike12345567 2021-07-27 10:32:18 +01:00
commit e2e796cd9d
35 changed files with 724 additions and 216 deletions

View file

@ -1,5 +1,5 @@
{
"version": "0.9.79-alpha.4",
"version": "0.9.80-alpha.9",
"npmClient": "yarn",
"packages": [
"packages/*"

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/auth",
"version": "0.9.79-alpha.4",
"version": "0.9.80-alpha.9",
"description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js",
"author": "Budibase",

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "0.9.79-alpha.4",
"version": "0.9.80-alpha.9",
"license": "AGPL-3.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",

View file

@ -115,7 +115,6 @@
}
.spectrum-Modal {
background: var(--background);
overflow: visible;
max-height: none;
margin: 40px 0;

View file

@ -88,7 +88,6 @@
display: grid;
position: relative;
gap: var(--spacing-xl);
color: var(--ink);
}
.spectrum-Dialog-content {
@ -106,13 +105,8 @@
position: absolute;
top: 15px;
right: 15px;
color: var(--ink);
font-size: var(--font-size-m);
}
.close-icon:hover {
color: var(--grey-6);
cursor: pointer;
}
.close-icon :global(svg) {
margin-right: 0;
}

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.9.79-alpha.4",
"version": "0.9.80-alpha.9",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@ -65,10 +65,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^0.9.79-alpha.4",
"@budibase/client": "^0.9.79-alpha.4",
"@budibase/bbui": "^0.9.80-alpha.9",
"@budibase/client": "^0.9.80-alpha.9",
"@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^0.9.79-alpha.4",
"@budibase/string-templates": "^0.9.80-alpha.9",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",

View file

@ -530,6 +530,11 @@ export const getFrontendStore = () => {
selected._styles = { normal: {}, hover: {}, active: {} }
await store.actions.preview.saveSelected()
},
updateConditions: async conditions => {
const selected = get(selectedComponent)
selected._conditions = conditions
await store.actions.preview.saveSelected()
},
updateProp: async (name, value) => {
let component = get(selectedComponent)
if (!name || !component) {

View file

@ -6,8 +6,10 @@
import ErrorsBox from "components/common/ErrorsBox.svelte"
import { roles } from "stores/backend"
const BASE_ROLE = { _id: "", inherits: "BASIC", permissionId: "Read/Write" }
let basePermissions = []
let selectedRole = {}
let selectedRole = BASE_ROLE
let errors = []
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
// Don't allow editing of public role
@ -15,6 +17,11 @@
$: selectedRoleId = selectedRole._id
$: otherRoles = editableRoles.filter(role => role._id !== selectedRoleId)
$: isCreating = selectedRoleId == null || selectedRoleId === ""
$: valid =
selectedRole.name &&
selectedRole.inherits &&
selectedRole.permissionId &&
!builtInRoles.includes(selectedRole.name)
const fetchBasePermissions = async () => {
const permissionsResponse = await api.get("/api/permission/builtin")
@ -32,7 +39,7 @@
permissionId: role.permissionId ?? "",
}
} else {
selectedRole = { _id: "", inherits: "", permissionId: "" }
selectedRole = BASE_ROLE
}
errors = []
}
@ -88,6 +95,7 @@
title="Edit Roles"
confirmText={isCreating ? "Create" : "Save"}
onConfirm={saveRole}
disabled={!valid}
>
{#if errors.length}
<ErrorsBox {errors} />
@ -115,7 +123,7 @@
options={otherRoles}
getOptionValue={role => role._id}
getOptionLabel={role => role.name}
placeholder="None"
disabled={builtInRoles.includes(selectedRole.name)}
/>
<Select
label="Base Permissions"
@ -123,7 +131,7 @@
options={basePermissions}
getOptionValue={x => x._id}
getOptionLabel={x => x.name}
placeholder="Choose permissions"
disabled={builtInRoles.includes(selectedRole.name)}
/>
{/if}
<div slot="footer">

View file

@ -1,32 +1,12 @@
<script>
import { isEmpty } from "lodash/fp"
import { Checkbox, Input, Select, DetailSummary } from "@budibase/bbui"
import { Input, DetailSummary } from "@budibase/bbui"
import { store } from "builderStore"
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
import TableSelect from "./PropertyControls/TableSelect.svelte"
import DataSourceSelect from "./PropertyControls/DataSourceSelect.svelte"
import DataProviderSelect from "./PropertyControls/DataProviderSelect.svelte"
import FieldSelect from "./PropertyControls/FieldSelect.svelte"
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
import SectionSelect from "./PropertyControls/SectionSelect.svelte"
import NavigationEditor from "./PropertyControls/NavigationEditor/NavigationEditor.svelte"
import EventsEditor from "./PropertyControls/EventsEditor"
import FilterEditor from "./PropertyControls/FilterEditor/FilterEditor.svelte"
import { IconSelect } from "./PropertyControls/IconSelect"
import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte"
import NumberFieldSelect from "./PropertyControls/NumberFieldSelect.svelte"
import OptionsFieldSelect from "./PropertyControls/OptionsFieldSelect.svelte"
import BooleanFieldSelect from "./PropertyControls/BooleanFieldSelect.svelte"
import LongFormFieldSelect from "./PropertyControls/LongFormFieldSelect.svelte"
import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte"
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte"
import ResetFieldsButton from "./PropertyControls/ResetFieldsButton.svelte"
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
import URLSelect from "./PropertyControls/URLSelect.svelte"
import { getComponentForSettingType } from "./PropertyControls/componentSettings"
export let componentDefinition
export let componentInstance
@ -45,40 +25,9 @@
$: assetDefinition = isLayout ? layoutDefinition : screenDefinition
const updateProp = store.actions.components.updateProp
const controlMap = {
text: Input,
select: Select,
dataSource: DataSourceSelect,
dataProvider: DataProviderSelect,
boolean: Checkbox,
number: Input,
event: EventsEditor,
table: TableSelect,
color: ColorPicker,
icon: IconSelect,
field: FieldSelect,
multifield: MultiFieldSelect,
schema: SchemaSelect,
section: SectionSelect,
navigation: NavigationEditor,
filter: FilterEditor,
url: URLSelect,
"field/string": StringFieldSelect,
"field/number": NumberFieldSelect,
"field/options": OptionsFieldSelect,
"field/boolean": BooleanFieldSelect,
"field/longform": LongFormFieldSelect,
"field/datetime": DateTimeFieldSelect,
"field/attachment": AttachmentFieldSelect,
"field/link": RelationshipFieldSelect,
}
const getControl = type => {
return controlMap[type]
}
const canRenderControl = setting => {
const control = getControl(setting?.type)
const control = getComponentForSettingType(setting?.type)
if (!control) {
return false
}
@ -101,11 +50,11 @@
/>
{/if}
{#if settings && settings.length > 0}
{#each settings as setting (`${componentInstance._id}-${setting.key}`)}
{#each settings as setting}
{#if canRenderControl(setting)}
<PropertyControl
type={setting.type}
control={getControl(setting.type)}
control={getComponentForSettingType(setting.type)}
label={setting.label}
key={setting.key}
value={componentInstance[setting.key] ??

View file

@ -0,0 +1,36 @@
<script>
import { DetailSummary, ActionButton, Drawer, Button } from "@budibase/bbui"
import { store } from "builderStore"
import ConditionalUIDrawer from "./PropertyControls/ConditionalUIDrawer.svelte"
export let componentInstance
let tempValue
let drawer
const openDrawer = () => {
tempValue = JSON.parse(JSON.stringify(componentInstance?._conditions ?? []))
drawer.show()
}
const save = () => {
store.actions.components.updateConditions(tempValue)
drawer.hide()
}
</script>
<DetailSummary
name={`Conditions${componentInstance?._conditions ? " *" : ""}`}
collapsible={false}
>
<div>
<ActionButton on:click={openDrawer}>Configure conditions</ActionButton>
</div>
</DetailSummary>
<Drawer bind:this={drawer} title="Conditions">
<svelte:fragment slot="description">
Show, hide and update components in response to conditions being met.
</svelte:fragment>
<Button cta slot="buttons" on:click={() => save()}>Save</Button>
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} />
</Drawer>

View file

@ -5,6 +5,7 @@
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
import DesignSection from "./DesignSection.svelte"
import CustomStylesSection from "./CustomStylesSection.svelte"
import ConditionalUISection from "./ConditionalUISection.svelte"
$: componentInstance = $selectedComponent
$: componentDefinition = store.actions.components.getDefinition(
@ -15,10 +16,13 @@
<Tabs selected="Settings" noPadding>
<Tab title="Settings">
<div class="container">
<ScreenSettingsSection {componentInstance} {componentDefinition} />
<ComponentSettingsSection {componentInstance} {componentDefinition} />
<DesignSection {componentInstance} {componentDefinition} />
<CustomStylesSection {componentInstance} {componentDefinition} />
{#key componentInstance?._id}
<ScreenSettingsSection {componentInstance} {componentDefinition} />
<ComponentSettingsSection {componentInstance} {componentDefinition} />
<DesignSection {componentInstance} {componentDefinition} />
<CustomStylesSection {componentInstance} {componentDefinition} />
<ConditionalUISection {componentInstance} {componentDefinition} />
{/key}
</div>
</Tab>
</Tabs>

View file

@ -0,0 +1,290 @@
<script>
import {
Button,
Body,
Icon,
DrawerContent,
Layout,
Select,
DatePicker,
} from "@budibase/bbui"
import { flip } from "svelte/animate"
import { dndzone } from "svelte-dnd-action"
import { generate } from "shortid"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { OperatorOptions, getValidOperatorsForType } from "helpers/lucene"
import { getBindableProperties } from "builderStore/dataBinding"
import { currentAsset, selectedComponent, store } from "builderStore"
import { getComponentForSettingType } from "./componentSettings"
import PropertyControl from "./PropertyControl.svelte"
export let conditions = []
const flipDurationMs = 150
const actionOptions = [
{
label: "Hide component",
value: "hide",
},
{
label: "Show component",
value: "show",
},
{
label: "Update setting",
value: "update",
},
]
const valueTypeOptions = [
{
value: "string",
label: "Binding",
},
{
value: "number",
label: "Number",
},
{
value: "datetime",
label: "Date",
},
{
value: "boolean",
label: "Boolean",
},
]
let dragDisabled = true
$: definition = store.actions.components.getDefinition(
$selectedComponent?._component
)
$: settings = (definition?.settings ?? []).map(setting => {
return {
label: setting.label,
value: setting.key,
}
})
$: bindableProperties = getBindableProperties(
$currentAsset,
$store.selectedComponentId
)
$: conditions.forEach(link => {
if (!link.id) {
link.id = generate()
}
})
const getSettingDefinition = key => {
return definition?.settings?.find(setting => {
return setting.key === key
})
}
const getComponentForSetting = key => {
const settingDefinition = getSettingDefinition(key)
return getComponentForSettingType(settingDefinition?.type || "text")
}
const addCondition = () => {
conditions = [
...conditions,
{
valueType: "string",
id: generate(),
action: "hide",
operator: OperatorOptions.Equals.value,
},
]
}
const removeCondition = id => {
conditions = conditions.filter(link => link.id !== id)
}
const handleFinalize = e => {
updateConditions(e)
dragDisabled = true
}
const updateConditions = e => {
conditions = e.detail.items
}
const getOperatorOptions = condition => {
return getValidOperatorsForType(condition.valueType)
}
const onOperatorChange = (condition, newOperator) => {
const noValueOptions = [
OperatorOptions.Empty.value,
OperatorOptions.NotEmpty.value,
]
condition.noValue = noValueOptions.includes(newOperator)
if (condition.noValue) {
condition.referenceValue = null
condition.valueType = "string"
}
}
const onValueTypeChange = (condition, newType) => {
condition.referenceValue = null
// Ensure a valid operator is set
const validOperators = getValidOperatorsForType(newType).map(x => x.value)
if (!validOperators.includes(condition.operator)) {
condition.operator = validOperators[0] ?? OperatorOptions.Equals.value
onOperatorChange(condition, condition.operator)
}
}
</script>
<DrawerContent>
<div class="container">
<Layout noPadding>
{#if conditions?.length}
<div
class="conditions"
use:dndzone={{
items: conditions,
flipDurationMs,
dropTargetStyle: { outline: "none" },
dragDisabled,
}}
on:finalize={handleFinalize}
on:consider={updateConditions}
>
{#each conditions as condition (condition.id)}
<div
class="condition"
class:update={condition.action === "update"}
animate:flip={{ duration: flipDurationMs }}
>
<div
class="handle"
aria-label="drag-handle"
style={dragDisabled ? "cursor: grab" : "cursor: grabbing"}
on:mousedown={() => (dragDisabled = false)}
>
<Icon name="DragHandle" size="XL" />
</div>
<Select
placeholder={null}
options={actionOptions}
bind:value={condition.action}
/>
{#if condition.action === "update"}
<Select options={settings} bind:value={condition.setting} />
<div>TO</div>
{#if getSettingDefinition(condition.setting)}
<PropertyControl
type={getSettingDefinition(condition.setting).type}
control={getComponentForSetting(condition.setting)}
key={getSettingDefinition(condition.setting).key}
value={condition.settingValue}
componentInstance={$selectedComponent}
onChange={val => (condition.settingValue = val)}
props={{
options: getSettingDefinition(condition.setting).options,
placeholder: getSettingDefinition(condition.setting)
.placeholder,
}}
/>
{:else}
<Select disabled placeholder=" " />
{/if}
{/if}
<div>IF</div>
<DrawerBindableInput
bindings={bindableProperties}
placeholder="Value"
value={condition.newValue}
on:change={e => (condition.newValue = e.detail)}
/>
<Select
placeholder={null}
options={getOperatorOptions(condition)}
bind:value={condition.operator}
on:change={e => onOperatorChange(condition, e.detail)}
/>
<Select
disabled={condition.noValue}
options={valueTypeOptions}
bind:value={condition.valueType}
placeholder={null}
on:change={e => onValueTypeChange(condition, e.detail)}
/>
{#if ["string", "number"].includes(condition.valueType)}
<DrawerBindableInput
disabled={condition.noValue}
bindings={bindableProperties}
placeholder="Value"
value={condition.referenceValue}
on:change={e => (condition.referenceValue = e.detail)}
/>
{:else if condition.valueType === "datetime"}
<DatePicker
placeholder="Value"
disabled={condition.noValue}
bind:value={condition.referenceValue}
/>
{:else if condition.valueType === "boolean"}
<Select
placeholder="Value"
disabled={condition.noValue}
options={["True", "False"]}
bind:value={condition.referenceValue}
/>
{/if}
<Icon
name="Close"
hoverable
size="S"
on:click={() => removeCondition(condition.id)}
/>
</div>
{/each}
</div>
{:else}
<Body size="S">Add your first condition to get started.</Body>
{/if}
<div>
<Button secondary icon="Add" on:click={addCondition}>
Add condition
</Button>
</div>
</Layout>
</div>
</DrawerContent>
<style>
.container {
width: 100%;
max-width: 1400px;
margin: 0 auto;
}
.conditions {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-m);
}
.condition {
gap: var(--spacing-l);
display: grid;
align-items: center;
grid-template-columns: auto 1fr auto 1fr 1fr 1fr 1fr auto;
border-radius: var(--border-radius-s);
transition: background-color ease-in-out 130ms;
}
.condition.update {
grid-template-columns: auto 1fr 1fr auto 1fr auto 1fr 1fr 1fr 1fr auto;
}
.condition:hover {
background-color: var(--spectrum-global-color-gray-100);
}
.handle {
display: grid;
place-items: center;
}
</style>

View file

@ -11,44 +11,11 @@
import { getBindableProperties } from "builderStore/dataBinding"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { generate } from "shortid"
import { OperatorOptions, getValidOperatorsForType } from "helpers/lucene"
export let schemaFields
export let value
const OperatorOptions = {
Equals: {
value: "equal",
label: "Equals",
},
NotEquals: {
value: "notEqual",
label: "Not equals",
},
Empty: {
value: "empty",
label: "Is empty",
},
NotEmpty: {
value: "notEmpty",
label: "Is not empty",
},
StartsWith: {
value: "string",
label: "Starts with",
},
Like: {
value: "fuzzy",
label: "Like",
},
MoreThan: {
value: "rangeLow",
label: "More than",
},
LessThan: {
value: "rangeHigh",
label: "Less than",
},
}
const BannedTypes = ["link", "attachment"]
$: bindableProperties = getBindableProperties(
$currentAsset,
@ -75,61 +42,16 @@
value = value.filter(field => field.id !== id)
}
const getValidOperatorsForType = type => {
const Op = OperatorOptions
if (type === "string") {
return [
Op.Equals,
Op.NotEquals,
Op.StartsWith,
Op.Like,
Op.Empty,
Op.NotEmpty,
]
} else if (type === "number") {
return [
Op.Equals,
Op.NotEquals,
Op.MoreThan,
Op.LessThan,
Op.Empty,
Op.NotEmpty,
]
} else if (type === "options") {
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
} else if (type === "boolean") {
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
} else if (type === "longform") {
return [
Op.Equals,
Op.NotEquals,
Op.StartsWith,
Op.Like,
Op.Empty,
Op.NotEmpty,
]
} else if (type === "datetime") {
return [
Op.Equals,
Op.NotEquals,
Op.MoreThan,
Op.LessThan,
Op.Empty,
Op.NotEmpty,
]
}
return []
}
const onFieldChange = (expression, field) => {
// Update the field type
expression.type = schemaFields.find(x => x.name === field)?.type
// Ensure a valid operator is set
const validOperators = getValidOperatorsForType(expression.type)
const validOperators = getValidOperatorsForType(expression.type).map(
x => x.value
)
if (!validOperators.includes(expression.operator)) {
expression.operator =
validOperators[0]?.value ?? OperatorOptions.Equals.value
expression.operator = validOperators[0] ?? OperatorOptions.Equals.value
onOperatorChange(expression, expression.operator)
}
}

View file

@ -15,13 +15,13 @@
export let links = []
const flipDurationMs = 150
let dragDisabled = true
$: links.forEach(link => {
if (!link.id) {
link.id = generate()
}
})
$: urlOptions = $store.screens
.map(screen => screen.routing?.route)
.filter(x => x != null)
@ -37,6 +37,11 @@
const updateLinks = e => {
links = e.detail.items
}
const handleFinalize = e => {
updateLinks(e)
dragDisabled = true
}
</script>
<DrawerContent>
@ -49,13 +54,21 @@
items: links,
flipDurationMs,
dropTargetStyle: { outline: "none" },
dragDisabled,
}}
on:finalize={updateLinks}
on:finalize={handleFinalize}
on:consider={updateLinks}
>
{#each links as link (link.id)}
<div class="link" animate:flip={{ duration: flipDurationMs }}>
<Icon name="DragHandle" size="XL" />
<div
class="handle"
aria-label="drag-handle"
style={dragDisabled ? "cursor: grab" : "cursor: grabbing"}
on:mousedown={() => (dragDisabled = false)}
>
<Icon name="DragHandle" size="XL" />
</div>
<Input bind:value={link.text} placeholder="Text" />
<Combobox
bind:value={link.url}
@ -90,7 +103,7 @@
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-s);
gap: var(--spacing-m);
}
.link {
gap: var(--spacing-l);
@ -108,4 +121,8 @@
flex: 1 1 auto;
width: 0;
}
.handle {
display: grid;
place-items: center;
}
</style>

View file

@ -70,7 +70,7 @@
</script>
<div class="property-control" bind:this={anchor} data-cy={`setting-${key}`}>
{#if type !== "boolean"}
{#if type !== "boolean" && label}
<div class="label">
<Label>{label}</Label>
</div>

View file

@ -0,0 +1,54 @@
import { Checkbox, Input, Select } from "@budibase/bbui"
import DataSourceSelect from "./DataSourceSelect.svelte"
import DataProviderSelect from "./DataProviderSelect.svelte"
import EventsEditor from "./EventsEditor"
import TableSelect from "./TableSelect.svelte"
import ColorPicker from "./ColorPicker.svelte"
import { IconSelect } from "./IconSelect"
import FieldSelect from "./FieldSelect.svelte"
import MultiFieldSelect from "./MultiFieldSelect.svelte"
import SchemaSelect from "./SchemaSelect.svelte"
import SectionSelect from "./SectionSelect.svelte"
import NavigationEditor from "./NavigationEditor/NavigationEditor.svelte"
import FilterEditor from "./FilterEditor/FilterEditor.svelte"
import URLSelect from "./URLSelect.svelte"
import StringFieldSelect from "./StringFieldSelect.svelte"
import NumberFieldSelect from "./NumberFieldSelect.svelte"
import OptionsFieldSelect from "./OptionsFieldSelect.svelte"
import BooleanFieldSelect from "./BooleanFieldSelect.svelte"
import LongFormFieldSelect from "./LongFormFieldSelect.svelte"
import DateTimeFieldSelect from "./DateTimeFieldSelect.svelte"
import AttachmentFieldSelect from "./AttachmentFieldSelect.svelte"
import RelationshipFieldSelect from "./RelationshipFieldSelect.svelte"
const componentMap = {
text: Input,
select: Select,
dataSource: DataSourceSelect,
dataProvider: DataProviderSelect,
boolean: Checkbox,
number: Input,
event: EventsEditor,
table: TableSelect,
color: ColorPicker,
icon: IconSelect,
field: FieldSelect,
multifield: MultiFieldSelect,
schema: SchemaSelect,
section: SectionSelect,
navigation: NavigationEditor,
filter: FilterEditor,
url: URLSelect,
"field/string": StringFieldSelect,
"field/number": NumberFieldSelect,
"field/options": OptionsFieldSelect,
"field/boolean": BooleanFieldSelect,
"field/longform": LongFormFieldSelect,
"field/datetime": DateTimeFieldSelect,
"field/attachment": AttachmentFieldSelect,
"field/link": RelationshipFieldSelect,
}
export const getComponentForSettingType = type => {
return componentMap[type]
}

View file

@ -0,0 +1,80 @@
export const OperatorOptions = {
Equals: {
value: "equal",
label: "Equals",
},
NotEquals: {
value: "notEqual",
label: "Not equals",
},
Empty: {
value: "empty",
label: "Is empty",
},
NotEmpty: {
value: "notEmpty",
label: "Is not empty",
},
StartsWith: {
value: "string",
label: "Starts with",
},
Like: {
value: "fuzzy",
label: "Like",
},
MoreThan: {
value: "rangeLow",
label: "More than",
},
LessThan: {
value: "rangeHigh",
label: "Less than",
},
}
export const getValidOperatorsForType = type => {
const Op = OperatorOptions
if (type === "string") {
return [
Op.Equals,
Op.NotEquals,
Op.StartsWith,
Op.Like,
Op.Empty,
Op.NotEmpty,
]
} else if (type === "number") {
return [
Op.Equals,
Op.NotEquals,
Op.MoreThan,
Op.LessThan,
Op.Empty,
Op.NotEmpty,
]
} else if (type === "options") {
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
} else if (type === "boolean") {
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
} else if (type === "longform") {
return [
Op.Equals,
Op.NotEquals,
Op.StartsWith,
Op.Like,
Op.Empty,
Op.NotEmpty,
]
} else if (type === "datetime") {
return [
Op.Equals,
Op.NotEquals,
Op.MoreThan,
Op.LessThan,
Op.Empty,
Op.NotEmpty,
]
}
return []
}

View file

@ -58,5 +58,6 @@
align-items: stretch;
gap: var(--spacing-l);
background-color: var(--background);
overflow-y: auto;
}
</style>

View file

@ -19,7 +19,8 @@
import { fetchData } from "helpers"
import { users, auth } from "stores/portal"
import TagsRenderer from "./_components/TagsTableRenderer.svelte"
import TagsRenderer from "./_components/RolesTagsTableRenderer.svelte"
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte"
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
@ -36,7 +37,8 @@
$: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : "BASIC"
// Merge the Apps list and the roles response to get something that makes sense for the table
$: appList = Object.keys($apps?.data).map(id => {
const role = $userFetch?.data?.roles?.[id] || defaultRoleId
const roleId = $userFetch?.data?.roles?.[id] || defaultRoleId
const role = $apps?.data?.[id].roles.find(role => role._id === roleId)
return {
...$apps?.data?.[id],
_id: id,

View file

@ -0,0 +1,8 @@
<script>
import TagsTableRenderer from "./TagsTableRenderer.svelte"
export let value
$: roles = value?.filter(role => role != null).map(role => role.name) ?? []
</script>
<TagsTableRenderer value={roles} />

View file

@ -4,9 +4,9 @@
const displayLimit = 5
$: roles = value?.filter(role => role != null) ?? []
$: tags = roles.slice(0, displayLimit)
$: leftover = roles.length - tags.length
$: values = value?.filter(value => value != null) ?? []
$: tags = values.slice(0, displayLimit)
$: leftover = values.length - tags.length
</script>
<div class="tag-renderer">

View file

@ -10,8 +10,8 @@
const roles = app.roles
let options = roles
.filter(role => role._id !== "PUBLIC")
.map(role => ({ value: role._id, label: role.name }))
.filter(role => role.value !== "PUBLIC")
let selectedRole = user?.roles?.[app?._id]
async function updateUserRoles() {
@ -48,5 +48,7 @@
on:change
{options}
label="Role"
getOptionLabel={role => role.name}
getOptionValue={role => role._id}
/>
</ModalContent>

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "0.9.79-alpha.4",
"version": "0.9.80-alpha.9",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js",
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.9.79-alpha.4",
"version": "0.9.80-alpha.9",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -18,9 +18,9 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "^0.9.79-alpha.4",
"@budibase/standard-components": "^0.9.79-alpha.4",
"@budibase/string-templates": "^0.9.79-alpha.4",
"@budibase/bbui": "^0.9.80-alpha.9",
"@budibase/standard-components": "^0.9.80-alpha.9",
"@budibase/string-templates": "^0.9.80-alpha.9",
"regexparam": "^1.3.0",
"shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5"

View file

@ -8,11 +8,18 @@
import { hashString } from "../utils/hash"
import Manifest from "@budibase/standard-components/manifest.json"
import { Placeholder } from "@budibase/standard-components"
import {
getActiveConditions,
reduceConditionActions,
} from "../utils/conditions"
export let instance = {}
// Props that will be passed to the component instance
let componentProps
// The enriched component settings
let enrichedSettings
// Any prop overrides that need to be applied due to conditional UI
let conditionalSettings
// Props are hashed when inside the builder preview and used as a key, so that
// components fully remount whenever any props change
@ -28,6 +35,9 @@
let lastContextKey
let lastInstanceKey
// Visibility flag used by conditional UI
let visible = true
// Get contexts
const context = getContext("context")
const insideScreenslot = !!getContext("screenslot")
@ -54,6 +64,8 @@
$builderStore.inBuilder &&
$builderStore.selectedComponentId === instance._id
$: interactive = $builderStore.previewType === "layout" || insideScreenslot
$: evaluateConditions(enrichedSettings?._conditions)
$: componentSettings = { ...enrichedSettings, ...conditionalSettings }
// Update component context
$: componentStore.set({
@ -62,14 +74,14 @@
styles: { ...instance._styles, id, empty, interactive },
empty,
selected,
props: componentProps,
props: componentSettings,
name,
})
const getRawProps = instance => {
let validProps = {}
Object.entries(instance)
.filter(([name]) => !name.startsWith("_"))
.filter(([name]) => name === "_conditions" || !name.startsWith("_"))
.forEach(([key, value]) => {
validProps[key] = value
})
@ -123,34 +135,55 @@
return
}
let propsChanged = false
if (!componentProps) {
componentProps = {}
if (!enrichedSettings) {
enrichedSettings = {}
propsChanged = true
}
Object.keys(enrichedProps).forEach(key => {
if (!propsAreSame(enrichedProps[key], componentProps[key])) {
if (!propsAreSame(enrichedProps[key], enrichedSettings[key])) {
propsChanged = true
componentProps[key] = enrichedProps[key]
enrichedSettings[key] = enrichedProps[key]
}
})
// Update the hash if we're in the builder so we can fully remount this
// component
if (get(builderStore).inBuilder && propsChanged) {
propsHash = hashString(JSON.stringify(componentProps))
propsHash = hashString(JSON.stringify(enrichedSettings))
}
}
const evaluateConditions = conditions => {
if (!conditions?.length) {
return
}
// Default visible to false if there is a show condition
let nextVisible = !conditions.find(condition => condition.action === "show")
// Execute conditions and determine settings and visibility changes
const activeConditions = getActiveConditions(conditions)
const result = reduceConditionActions(activeConditions)
if (result.visible != null) {
nextVisible = result.visible
}
// Update state from condition results
conditionalSettings = result.settingUpdates
visible = nextVisible
}
</script>
<div
class={`component ${id}`}
data-type={interactive ? "component" : ""}
data-id={id}
data-name={name}
>
{#key propsHash}
{#if constructor && componentProps}
<svelte:component this={constructor} {...componentProps}>
{#key propsHash}
{#if constructor && componentSettings && visible}
<div
class={`component ${id}`}
data-type={interactive ? "component" : ""}
data-id={id}
data-name={name}
class:hidden={!visible}
>
<svelte:component this={constructor} {...componentSettings}>
{#if children.length}
{#each children as child (child._id)}
<svelte:self instance={child} />
@ -159,9 +192,9 @@
<Placeholder />
{/if}
</svelte:component>
{/if}
{/key}
</div>
</div>
{/if}
{/key}
<style>
.component {

View file

@ -6,8 +6,15 @@ const createAppStore = () => {
// Fetches the app definition including screens, layouts and theme
const fetchAppDefinition = async () => {
const appDefinition = await API.fetchAppPackage(get(store).appId)
store.set(appDefinition)
const appId = get(store)?.appId
if (!appId) {
throw "Cannot fetch app definition without app ID set"
}
const appDefinition = await API.fetchAppPackage(appId)
store.set({
...appDefinition,
appId: appDefinition?.application?.appId,
})
}
// Sets the initial app ID

View file

@ -10,9 +10,14 @@ const createAuthStore = () => {
store.set(user)
}
const logOut = async () => {
window.document.cookie = `budibase:auth=; budibase:currentapp=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`
window.location = "/builder/auth/login"
}
return {
subscribe: store.subscribe,
actions: { fetchUser },
actions: { fetchUser, logOut },
}
}

View file

@ -1,10 +1,15 @@
import { get } from "svelte/store"
import { routeStore, builderStore, confirmationStore } from "../store"
import {
routeStore,
builderStore,
confirmationStore,
authStore,
} from "../store"
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
import { ActionTypes } from "../constants"
const saveRowHandler = async (action, context) => {
const { fields, providerId } = action.parameters
const { fields, providerId, tableId } = action.parameters
if (providerId) {
let draft = context[providerId]
if (fields) {
@ -12,6 +17,9 @@ const saveRowHandler = async (action, context) => {
draft[field] = value
}
}
if (tableId) {
draft.tableId = tableId
}
await saveRow(draft)
}
}
@ -74,6 +82,10 @@ const refreshDatasourceHandler = async (action, context) => {
)
}
const logoutHandler = async () => {
await authStore.actions.logOut()
}
const handlerMap = {
["Save Row"]: saveRowHandler,
["Delete Row"]: deleteRowHandler,
@ -82,6 +94,7 @@ const handlerMap = {
["Trigger Automation"]: triggerAutomationHandler,
["Validate Form"]: validateFormHandler,
["Refresh Datasource"]: refreshDatasourceHandler,
["Log Out"]: logoutHandler,
}
const confirmTextMap = {

View file

@ -43,5 +43,17 @@ export const enrichProps = (props, context) => {
)
}
// Enrich any click actions in conditions
if (enrichedProps._conditions) {
enrichedProps._conditions.forEach(condition => {
if (condition.setting === "onClick") {
condition.settingValue = enrichButtonActions(
condition.settingValue,
totalContext
)
}
})
}
return enrichedProps
}

View file

@ -0,0 +1,60 @@
import {
buildLuceneQuery,
luceneQuery,
} from "../../../standard-components/src/lucene"
export const getActiveConditions = conditions => {
if (!conditions?.length) {
return []
}
return conditions.filter(condition => {
// Parse values into correct types
if (condition.valueType === "number") {
condition.referenceValue = parseFloat(condition.referenceValue)
condition.newValue = parseFloat(condition.newValue)
} else if (condition.valueType === "datetime") {
if (condition.referenceValue) {
condition.referenceValue = new Date(
condition.referenceValue
).toISOString()
}
if (condition.newValue) {
condition.newValue = new Date(condition.newValue).toISOString()
}
} else if (condition.valueType === "boolean") {
condition.referenceValue =
`${condition.referenceValue}`.toLowerCase() === "true"
condition.newValue = `${condition.newValue}`.toLowerCase() === "true"
}
// Build lucene compatible condition expression
const luceneCondition = {
...condition,
type: condition.valueType,
field: "newValue",
value: condition.referenceValue,
}
const query = buildLuceneQuery([luceneCondition])
const result = luceneQuery([luceneCondition], query)
return result.length > 0
})
}
export const reduceConditionActions = conditions => {
let settingUpdates = {}
let visible = null
conditions?.forEach(condition => {
if (condition.action === "show") {
visible = true
} else if (condition.action === "hide") {
visible = false
} else if (condition.setting) {
settingUpdates[condition.setting] = condition.settingValue
}
})
return { settingUpdates, visible }
}

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "0.9.79-alpha.4",
"version": "0.9.80-alpha.9",
"description": "Budibase Web Server",
"main": "src/index.js",
"repository": {
@ -9,7 +9,7 @@
"url": "https://github.com/Budibase/budibase.git"
},
"scripts": {
"build": "rm -rf dist/ && tsc && mv dist/src/* dist/ && rmdir dist/src/ && yarn postbuild",
"build": "rimraf dist/ && tsc && mv dist/src/* dist/ && rmdir dist/src/ && yarn postbuild",
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
"test": "jest --coverage --maxWorkers=2",
"test:watch": "jest --watch",
@ -62,9 +62,9 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.9.79-alpha.4",
"@budibase/client": "^0.9.79-alpha.4",
"@budibase/string-templates": "^0.9.79-alpha.4",
"@budibase/auth": "^0.9.80-alpha.9",
"@budibase/client": "^0.9.80-alpha.9",
"@budibase/string-templates": "^0.9.80-alpha.9",
"@elastic/elasticsearch": "7.10.0",
"@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1",
@ -117,7 +117,7 @@
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/preset-env": "^7.14.4",
"@budibase/standard-components": "^0.9.79-alpha.4",
"@budibase/standard-components": "^0.9.80-alpha.9",
"@jest/test-sequencer": "^24.8.0",
"@types/bull": "^3.15.1",
"@types/jest": "^26.0.23",
@ -134,6 +134,7 @@
"nodemon": "^2.0.4",
"pouchdb-adapter-memory": "^7.2.1",
"prettier": "^2.3.1",
"rimraf": "^3.0.2",
"supertest": "^4.0.2",
"ts-jest": "^27.0.3",
"ts-node": "^10.0.0",

View file

@ -29,11 +29,11 @@
"keywords": [
"svelte"
],
"version": "0.9.79-alpha.4",
"version": "0.9.80-alpha.9",
"license": "MIT",
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc",
"dependencies": {
"@budibase/bbui": "^0.9.79-alpha.4",
"@budibase/bbui": "^0.9.80-alpha.9",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/link": "^3.1.3",
"@spectrum-css/page": "^3.0.1",

View file

@ -23,7 +23,7 @@ export const buildLuceneQuery = filter => {
value = parseFloat(value)
}
if (type === "boolean") {
value = value?.toLowerCase() === "true"
value = `${value}`?.toLowerCase() === "true"
}
if (operator.startsWith("range")) {
if (!query.range[field]) {
@ -91,6 +91,11 @@ export const luceneQuery = (docs, query) => {
return !doc[key] || !doc[key].startsWith(value)
})
// Process a fuzzy match (treat the same as starts with when running locally)
const fuzzyMatch = match("fuzzy", (key, value, doc) => {
return !doc[key] || !doc[key].startsWith(value)
})
// Process a range match
const rangeMatch = match("range", (key, value, doc) => {
return !doc[key] || doc[key] < value.low || doc[key] > value.high
@ -120,6 +125,7 @@ export const luceneQuery = (docs, query) => {
const docMatch = doc => {
return (
stringMatch(doc) &&
fuzzyMatch(doc) &&
rangeMatch(doc) &&
equalMatch(doc) &&
notEqualMatch(doc) &&

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "0.9.79-alpha.4",
"version": "0.9.80-alpha.9",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
"version": "0.9.79-alpha.4",
"version": "0.9.80-alpha.9",
"description": "Budibase background service",
"main": "src/index.js",
"repository": {
@ -23,8 +23,8 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.9.79-alpha.4",
"@budibase/string-templates": "^0.9.79-alpha.4",
"@budibase/auth": "^0.9.80-alpha.9",
"@budibase/string-templates": "^0.9.80-alpha.9",
"@koa/router": "^8.0.0",
"@techpass/passport-openidconnect": "^0.3.0",
"aws-sdk": "^2.811.0",