diff --git a/.github/workflows/deploy-qa.yml b/.github/workflows/deploy-qa.yml index 2a30e44def..1339ad2eb9 100644 --- a/.github/workflows/deploy-qa.yml +++ b/.github/workflows/deploy-qa.yml @@ -3,7 +3,7 @@ name: Deploy QA on: push: branches: - - v3-ui + - master workflow_dispatch: jobs: diff --git a/.gitignore b/.gitignore index 32d1416f4a..bac643e5df 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,10 @@ packages/server/runtime_apps/ .idea/ bb-airgapped.tar.gz *.iml - packages/server/build/oldClientVersions/**/* packages/builder/src/components/deploy/clientVersions.json - packages/server/src/integrations/tests/utils/*.lock +packages/builder/vite.config.mjs.timestamp* # Logs logs diff --git a/packages/backend-core/src/constants/misc.ts b/packages/backend-core/src/constants/misc.ts index aee099e10a..e2fd975e40 100644 --- a/packages/backend-core/src/constants/misc.ts +++ b/packages/backend-core/src/constants/misc.ts @@ -28,6 +28,7 @@ export enum Config { OIDC = "oidc", OIDC_LOGOS = "logos_oidc", SCIM = "scim", + AI = "AI", } export const MIN_VALID_DATE = new Date(-2147483647000) diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 7d3a9f18f5..4cb0a9c731 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -223,6 +223,8 @@ const environment = { BB_ADMIN_USER_EMAIL: process.env.BB_ADMIN_USER_EMAIL, BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD, OPENAI_API_KEY: process.env.OPENAI_API_KEY, + MIN_VERSION_WITHOUT_POWER_ROLE: + process.env.MIN_VERSION_WITHOUT_POWER_ROLE || "3.0.0", } export function setEnv(newEnvVars: Partial): () => void { diff --git a/packages/backend-core/src/security/permissions.ts b/packages/backend-core/src/security/permissions.ts index 4ed2cd3954..929ae92909 100644 --- a/packages/backend-core/src/security/permissions.ts +++ b/packages/backend-core/src/security/permissions.ts @@ -1,4 +1,8 @@ -import { PermissionLevel, PermissionType } from "@budibase/types" +import { + PermissionLevel, + PermissionType, + BuiltinPermissionID, +} from "@budibase/types" import flatten from "lodash/flatten" import cloneDeep from "lodash/fp/cloneDeep" @@ -57,14 +61,6 @@ export function getAllowedLevels(userPermLevel: PermissionLevel): string[] { } } -export enum BuiltinPermissionID { - PUBLIC = "public", - READ_ONLY = "read_only", - WRITE = "write", - ADMIN = "admin", - POWER = "power", -} - export const BUILTIN_PERMISSIONS: { [key in keyof typeof BuiltinPermissionID]: { _id: (typeof BuiltinPermissionID)[key] diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index 42a55c16c7..4076be93a0 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -1,5 +1,4 @@ import semver from "semver" -import { BuiltinPermissionID, PermissionLevel } from "./permissions" import { prefixRoleID, getRoleParams, @@ -14,10 +13,13 @@ import { RoleUIMetadata, Database, App, + BuiltinPermissionID, + PermissionLevel, } from "@budibase/types" import cloneDeep from "lodash/fp/cloneDeep" import { RoleColor, helpers } from "@budibase/shared-core" import { uniqBy } from "lodash" +import { default as env } from "../environment" export const BUILTIN_ROLE_IDS = { ADMIN: "ADMIN", @@ -50,7 +52,7 @@ export class Role implements RoleDoc { _id: string _rev?: string name: string - permissionId: string + permissionId: BuiltinPermissionID inherits?: string | string[] version?: string permissions: Record = {} @@ -59,7 +61,7 @@ export class Role implements RoleDoc { constructor( id: string, name: string, - permissionId: string, + permissionId: BuiltinPermissionID, uiMetadata?: RoleUIMetadata ) { this._id = id @@ -213,6 +215,22 @@ export function getBuiltinRole(roleId: string): Role | undefined { return cloneDeep(role) } +export function validInherits( + allRoles: RoleDoc[], + inherits?: string | string[] +): boolean { + if (!inherits) { + return false + } + const find = (id: string) => allRoles.find(r => roleIDsAreEqual(r._id!, id)) + if (Array.isArray(inherits)) { + const filtered = inherits.filter(roleId => find(roleId)) + return inherits.length !== 0 && filtered.length === inherits.length + } else { + return !!find(inherits) + } +} + /** * Works through the inheritance ranks to see how far up the builtin stack this ID is. */ @@ -220,8 +238,8 @@ export function builtinRoleToNumber(id: string) { const builtins = getBuiltinRoles() const MAX = Object.values(builtins).length + 1 if ( - compareRoleIds(id, BUILTIN_IDS.ADMIN) || - compareRoleIds(id, BUILTIN_IDS.BUILDER) + roleIDsAreEqual(id, BUILTIN_IDS.ADMIN) || + roleIDsAreEqual(id, BUILTIN_IDS.BUILDER) ) { return MAX } @@ -260,7 +278,7 @@ export async function roleToNumber(id: string) { const highestBuiltin: number | undefined = role.inherits .map(roleId => { const foundRole = hierarchy.find(role => - compareRoleIds(role._id!, roleId) + roleIDsAreEqual(role._id!, roleId) ) if (foundRole) { return findNumber(foundRole) + 1 @@ -295,7 +313,7 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string { : roleId1 } -export function compareRoleIds(roleId1: string, roleId2: string) { +export function roleIDsAreEqual(roleId1: string, roleId2: string) { // make sure both role IDs are prefixed correctly return prefixRoleID(roleId1) === prefixRoleID(roleId2) } @@ -328,7 +346,7 @@ export function findRole( roleId = prefixRoleID(roleId) } const dbRole = roles.find( - role => role._id && compareRoleIds(role._id, roleId) + role => role._id && roleIDsAreEqual(role._id, roleId) ) if (!dbRole && !isBuiltin(roleId) && opts?.defaultPublic) { return cloneDeep(BUILTIN_ROLES.PUBLIC) @@ -385,7 +403,7 @@ async function getAllUserRoles( ): Promise { const allRoles = await getAllRoles() // admins have access to all roles - if (compareRoleIds(userRoleId, BUILTIN_IDS.ADMIN)) { + if (roleIDsAreEqual(userRoleId, BUILTIN_IDS.ADMIN)) { return allRoles } @@ -497,7 +515,7 @@ export async function getAllRoles(appId?: string): Promise { for (let builtinRoleId of externalBuiltinRoles) { const builtinRole = builtinRoles[builtinRoleId] const dbBuiltin = roles.filter(dbRole => - compareRoleIds(dbRole._id!, builtinRoleId) + roleIDsAreEqual(dbRole._id!, builtinRoleId) )[0] if (dbBuiltin == null) { roles.push(builtinRole || builtinRoles.BASIC) @@ -537,7 +555,10 @@ async function shouldIncludePowerRole(db: Database) { return true } - const isGreaterThan3x = semver.gte(creationVersion, "3.0.0") + const isGreaterThan3x = semver.gte( + creationVersion, + env.MIN_VERSION_WITHOUT_POWER_ROLE + ) return !isGreaterThan3x } @@ -553,9 +574,9 @@ export class AccessController { if ( tryingRoleId == null || tryingRoleId === "" || - compareRoleIds(tryingRoleId, BUILTIN_IDS.BUILDER) || - compareRoleIds(userRoleId!, tryingRoleId) || - compareRoleIds(userRoleId!, BUILTIN_IDS.BUILDER) + roleIDsAreEqual(tryingRoleId, BUILTIN_IDS.BUILDER) || + roleIDsAreEqual(userRoleId!, tryingRoleId) || + roleIDsAreEqual(userRoleId!, BUILTIN_IDS.BUILDER) ) { return true } @@ -566,7 +587,7 @@ export class AccessController { } return ( - roleIds?.find(roleId => compareRoleIds(roleId, tryingRoleId)) !== + roleIds?.find(roleId => roleIDsAreEqual(roleId, tryingRoleId)) !== undefined ) } diff --git a/packages/backend-core/src/security/tests/permissions.spec.ts b/packages/backend-core/src/security/tests/permissions.spec.ts index 39348646fb..f98833c7cd 100644 --- a/packages/backend-core/src/security/tests/permissions.spec.ts +++ b/packages/backend-core/src/security/tests/permissions.spec.ts @@ -1,6 +1,7 @@ import cloneDeep from "lodash/cloneDeep" import * as permissions from "../permissions" import { BUILTIN_ROLE_IDS } from "../roles" +import { BuiltinPermissionID } from "@budibase/types" describe("levelToNumber", () => { it("should return 0 for EXECUTE", () => { @@ -77,7 +78,7 @@ describe("doesHaveBasePermission", () => { const rolesHierarchy = [ { roleId: BUILTIN_ROLE_IDS.ADMIN, - permissionId: permissions.BuiltinPermissionID.ADMIN, + permissionId: BuiltinPermissionID.ADMIN, }, ] expect( @@ -91,7 +92,7 @@ describe("doesHaveBasePermission", () => { const rolesHierarchy = [ { roleId: BUILTIN_ROLE_IDS.PUBLIC, - permissionId: permissions.BuiltinPermissionID.PUBLIC, + permissionId: BuiltinPermissionID.PUBLIC, }, ] expect( @@ -129,7 +130,7 @@ describe("getBuiltinPermissions", () => { describe("getBuiltinPermissionByID", () => { it("returns correct permission object for valid ID", () => { const expectedPermission = { - _id: permissions.BuiltinPermissionID.PUBLIC, + _id: BuiltinPermissionID.PUBLIC, name: "Public", permissions: [ new permissions.Permission( diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index 6043a8713e..e4b2b843af 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -179,12 +179,6 @@ class InternalBuilder { return this.table.schema[column] } - private supportsILike(): boolean { - return !( - this.client === SqlClient.ORACLE || this.client === SqlClient.SQL_LITE - ) - } - private quoteChars(): [string, string] { const wrapped = this.knexClient.wrapIdentifier("foo", {}) return [wrapped[0], wrapped[wrapped.length - 1]] @@ -216,8 +210,30 @@ class InternalBuilder { return formatter.wrap(value, false) } - private rawQuotedValue(value: string): Knex.Raw { - return this.knex.raw(this.quotedValue(value)) + private castIntToString(identifier: string | Knex.Raw): Knex.Raw { + switch (this.client) { + case SqlClient.ORACLE: { + return this.knex.raw("to_char(??)", [identifier]) + } + case SqlClient.POSTGRES: { + return this.knex.raw("??::TEXT", [identifier]) + } + case SqlClient.MY_SQL: + case SqlClient.MARIADB: { + return this.knex.raw("CAST(?? AS CHAR)", [identifier]) + } + case SqlClient.SQL_LITE: { + // Technically sqlite can actually represent numbers larger than a 64bit + // int as a string, but it does it using scientific notation (e.g. + // "1e+20") which is not what we want. Given that the external SQL + // databases are limited to supporting only 64bit ints, we settle for + // that here. + return this.knex.raw("printf('%d', ??)", [identifier]) + } + case SqlClient.MS_SQL: { + return this.knex.raw("CONVERT(NVARCHAR, ??)", [identifier]) + } + } } // Unfortuantely we cannot rely on knex's identifier escaping because it trims @@ -512,7 +528,7 @@ class InternalBuilder { if (!matchesTableName) { updatedKey = filterKey.replace( new RegExp(`^${relationship.column}.`), - `${aliases![relationship.tableName]}.` + `${aliases?.[relationship.tableName] || relationship.tableName}.` ) } else { updatedKey = filterKey @@ -1074,24 +1090,36 @@ class InternalBuilder { ) } } else { - query = query.count(`* as ${aggregation.name}`) + if (this.client === SqlClient.ORACLE) { + const field = this.convertClobs(`${tableName}.${aggregation.field}`) + query = query.select( + this.knex.raw(`COUNT(??) as ??`, [field, aggregation.name]) + ) + } else { + query = query.count(`${aggregation.field} as ${aggregation.name}`) + } } } else { - const field = `${tableName}.${aggregation.field} as ${aggregation.name}` - switch (op) { - case CalculationType.SUM: - query = query.sum(field) - break - case CalculationType.AVG: - query = query.avg(field) - break - case CalculationType.MIN: - query = query.min(field) - break - case CalculationType.MAX: - query = query.max(field) - break + const fieldSchema = this.getFieldSchema(aggregation.field) + if (!fieldSchema) { + // This should not happen in practice. + throw new Error( + `field schema missing for aggregation target: ${aggregation.field}` + ) } + + let aggregate = this.knex.raw("??(??)", [ + this.knex.raw(op), + this.rawQuotedIdentifier(`${tableName}.${aggregation.field}`), + ]) + + if (fieldSchema.type === FieldType.BIGINT) { + aggregate = this.castIntToString(aggregate) + } + + query = query.select( + this.knex.raw("?? as ??", [aggregate, aggregation.name]) + ) } } return query @@ -1434,7 +1462,8 @@ class InternalBuilder { schema.constraints?.presence === true || schema.type === FieldType.FORMULA || schema.type === FieldType.AUTO || - schema.type === FieldType.LINK + schema.type === FieldType.LINK || + schema.type === FieldType.AI ) { continue } @@ -1556,7 +1585,7 @@ class InternalBuilder { query = this.addFilters(query, filters, { relationship: true }) // handle relationships with a CTE for all others - if (relationships?.length) { + if (relationships?.length && aggregations.length === 0) { const mainTable = this.query.tableAliases?.[this.query.endpoint.entityId] || this.query.endpoint.entityId @@ -1571,10 +1600,8 @@ class InternalBuilder { // add JSON aggregations attached to the CTE return this.addJsonRelationships(cte, tableName, relationships) } - // no relationships found - return query - else { - return query - } + + return query } update(opts: QueryOptions): Knex.QueryBuilder { diff --git a/packages/backend-core/tests/core/utilities/mocks/licenses.ts b/packages/backend-core/tests/core/utilities/mocks/licenses.ts index bc9a3b635c..5ba6fb36a1 100644 --- a/packages/backend-core/tests/core/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/core/utilities/mocks/licenses.ts @@ -102,6 +102,14 @@ export const useAppBuilders = () => { return useFeature(Feature.APP_BUILDERS) } +export const useBudibaseAI = () => { + return useFeature(Feature.BUDIBASE_AI) +} + +export const useAICustomConfigs = () => { + return useFeature(Feature.AI_CUSTOM_CONFIGS) +} + // QUOTAS export const setAutomationLogsQuota = (value: number) => { diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte index d3cec0f307..2401354fbb 100644 --- a/packages/bbui/src/ActionButton/ActionButton.svelte +++ b/packages/bbui/src/ActionButton/ActionButton.svelte @@ -1,15 +1,11 @@ - - (showTooltip = true)} on:mouseleave={() => (showTooltip = false)} on:focus={() => (showTooltip = true)} + {disabled} + style={accentStyle} > - - + {#if icon} + + {/if} + {#if $$slots} + + {/if} + {#if tooltip && showTooltip} +
+ +
+ {/if} + diff --git a/packages/bbui/src/ActionMenu/ActionMenu.svelte b/packages/bbui/src/ActionMenu/ActionMenu.svelte index 75ddd679da..f27854bc04 100644 --- a/packages/bbui/src/ActionMenu/ActionMenu.svelte +++ b/packages/bbui/src/ActionMenu/ActionMenu.svelte @@ -1,14 +1,20 @@ -
+
diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index 21635592d2..e95c7dd1b6 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -151,9 +151,9 @@ export default function positionDropdown(element, opts) { // Determine X strategy if (align === "right") { applyXStrategy(Strategies.EndToEnd) - } else if (align === "right-outside") { + } else if (align === "right-outside" || align === "right-context-menu") { applyXStrategy(Strategies.StartToEnd) - } else if (align === "left-outside") { + } else if (align === "left-outside" || align === "left-context-menu") { applyXStrategy(Strategies.EndToStart) } else if (align === "center") { applyXStrategy(Strategies.MidPoint) @@ -164,6 +164,12 @@ export default function positionDropdown(element, opts) { // Determine Y strategy if (align === "right-outside" || align === "left-outside") { applyYStrategy(Strategies.MidPoint) + } else if ( + align === "right-context-menu" || + align === "left-context-menu" + ) { + applyYStrategy(Strategies.StartToStart) + styles.top -= 5 // Manual adjustment for action menu padding } else { applyYStrategy(Strategies.StartToEnd) } @@ -240,7 +246,7 @@ export default function positionDropdown(element, opts) { } // Apply initial styles which don't need to change - element.style.position = "absolute" + element.style.position = "fixed" element.style.zIndex = "9999" // Set up a scroll listener diff --git a/packages/bbui/src/Button/Button.svelte b/packages/bbui/src/Button/Button.svelte index 9e49d84d44..0a8917c3c1 100644 --- a/packages/bbui/src/Button/Button.svelte +++ b/packages/bbui/src/Button/Button.svelte @@ -17,6 +17,8 @@ export let tooltip = undefined export let newStyles = true export let id + export let ref + export let reverse = false const dispatch = createEventDispatcher() @@ -25,6 +27,7 @@ @@ -91,4 +97,11 @@ .spectrum-Button--secondary.new-styles.is-disabled { color: var(--spectrum-global-color-gray-500); } + .spectrum-Button .spectrum-Button-label + .spectrum-Icon { + margin-left: var(--spectrum-button-primary-icon-gap); + margin-right: calc( + -1 * (var(--spectrum-button-primary-textonly-padding-left-adjusted) - + var(--spectrum-button-primary-padding-left-adjusted)) + ); + } diff --git a/packages/bbui/src/ButtonGroup/CollapsedButtonGroup.svelte b/packages/bbui/src/ButtonGroup/CollapsedButtonGroup.svelte new file mode 100644 index 0000000000..d7aad5ccff --- /dev/null +++ b/packages/bbui/src/ButtonGroup/CollapsedButtonGroup.svelte @@ -0,0 +1,57 @@ + + + + + + {#each buttons as button} + handleClick(button)} disabled={button.disabled}> + {button.text || "Button"} + + {/each} + + diff --git a/packages/bbui/src/Form/Core/Switch.svelte b/packages/bbui/src/Form/Core/Switch.svelte index deffc19167..d7110a6e67 100644 --- a/packages/bbui/src/Form/Core/Switch.svelte +++ b/packages/bbui/src/Form/Core/Switch.svelte @@ -19,6 +19,7 @@ {disabled} on:change={onChange} on:click + on:click|stopPropagation {id} type="checkbox" class="spectrum-Switch-input" diff --git a/packages/bbui/src/Form/Core/TextField.svelte b/packages/bbui/src/Form/Core/TextField.svelte index 3335d3567b..917bb2a452 100644 --- a/packages/bbui/src/Form/Core/TextField.svelte +++ b/packages/bbui/src/Form/Core/TextField.svelte @@ -1,6 +1,6 @@ diff --git a/packages/bbui/src/Icon/Icon.svelte b/packages/bbui/src/Icon/Icon.svelte index 6ae1f4ca67..73ad8edd10 100644 --- a/packages/bbui/src/Icon/Icon.svelte +++ b/packages/bbui/src/Icon/Icon.svelte @@ -60,10 +60,11 @@ .newStyles { color: var(--spectrum-global-color-gray-700); } - + svg { + transition: color var(--spectrum-global-animation-duration-100, 130ms); + } svg.hoverable { pointer-events: all; - transition: color var(--spectrum-global-animation-duration-100, 130ms); } svg.hoverable:hover { color: var(--hover-color) !important; diff --git a/packages/bbui/src/InlineAlert/InlineAlert.svelte b/packages/bbui/src/InlineAlert/InlineAlert.svelte index 3b98936f62..edfa760eb8 100644 --- a/packages/bbui/src/InlineAlert/InlineAlert.svelte +++ b/packages/bbui/src/InlineAlert/InlineAlert.svelte @@ -8,6 +8,7 @@ export let onConfirm = undefined export let buttonText = "" export let cta = false + $: icon = selectIcon(type) // if newlines used, convert them to different elements $: split = message.split("\n") diff --git a/packages/bbui/src/List/ListItem.svelte b/packages/bbui/src/List/ListItem.svelte index 76b242cf9c..e979b2b684 100644 --- a/packages/bbui/src/List/ListItem.svelte +++ b/packages/bbui/src/List/ListItem.svelte @@ -1,55 +1,68 @@ - - -
- + diff --git a/packages/bbui/src/Menu/Item.svelte b/packages/bbui/src/Menu/Item.svelte index 05a33adda9..5e5f6d840c 100644 --- a/packages/bbui/src/Menu/Item.svelte +++ b/packages/bbui/src/Menu/Item.svelte @@ -27,7 +27,7 @@ const onClick = () => { if (actionMenu && !noClose) { - actionMenu.hide() + actionMenu.hideAll() } dispatch("click") } @@ -35,7 +35,7 @@
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index c88317c79f..873e769e21 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -3,7 +3,7 @@ automationStore, selectedAutomation, permissions, - selectedAutomationDisplayData, + tables, } from "stores/builder" import { Icon, @@ -17,6 +17,7 @@ AbsTooltip, InlineAlert, } from "@budibase/bbui" + import { sdk } from "@budibase/shared-core" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import ActionModal from "./ActionModal.svelte" @@ -51,7 +52,12 @@ $: isAppAction && setPermissions(role) $: isAppAction && getPermissions(automationId) - $: triggerInfo = $selectedAutomationDisplayData?.triggerInfo + $: triggerInfo = sdk.automations.isRowAction($selectedAutomation) && { + title: "Automation trigger", + tableName: $tables.list.find( + x => x._id === $selectedAutomation.definition.trigger.inputs?.tableId + )?.name, + } async function setPermissions(role) { if (!role || !automationId) { @@ -187,10 +193,10 @@ {block} {webhookModal} /> - {#if isTrigger && triggerInfo} + {#if triggerInfo} {/if} {#if lastStep} diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte index 6e4d7c0099..4e9ca5fd53 100644 --- a/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte @@ -17,11 +17,14 @@ let confirmDeleteDialog let updateAutomationDialog + $: isRowAction = sdk.automations.isRowAction(automation) + async function deleteAutomation() { try { await automationStore.actions.delete(automation) notifications.success("Automation deleted successfully") } catch (error) { + console.error(error) notifications.error("Error deleting automation") } } @@ -36,42 +39,7 @@ } const getContextMenuItems = () => { - const isRowAction = sdk.automations.isRowAction(automation) - const result = [] - if (!isRowAction) { - result.push( - ...[ - { - icon: "Delete", - name: "Delete", - keyBind: null, - visible: true, - disabled: false, - callback: confirmDeleteDialog.show, - }, - { - icon: "Edit", - name: "Edit", - keyBind: null, - visible: true, - disabled: !automation.definition.trigger, - callback: updateAutomationDialog.show, - }, - { - icon: "Duplicate", - name: "Duplicate", - keyBind: null, - visible: true, - disabled: - !automation.definition.trigger || - automation.definition.trigger?.name === "Webhook", - callback: duplicateAutomation, - }, - ] - ) - } - - result.push({ + const pause = { icon: automation.disabled ? "CheckmarkCircle" : "Cancel", name: automation.disabled ? "Activate" : "Pause", keyBind: null, @@ -83,8 +51,50 @@ automation.disabled ) }, - }) - return result + } + const del = { + icon: "Delete", + name: "Delete", + keyBind: null, + visible: true, + disabled: false, + callback: confirmDeleteDialog.show, + } + if (!isRowAction) { + return [ + { + icon: "Edit", + name: "Edit", + keyBind: null, + visible: true, + disabled: !automation.definition.trigger, + callback: updateAutomationDialog.show, + }, + { + icon: "Duplicate", + name: "Duplicate", + keyBind: null, + visible: true, + disabled: + !automation.definition.trigger || + automation.definition.trigger?.name === "Webhook", + callback: duplicateAutomation, + }, + pause, + del, + ] + } else { + return [ + { + icon: "Edit", + name: "Edit", + keyBind: null, + visible: true, + callback: updateAutomationDialog.show, + }, + del, + ] + } } const openContextMenu = e => { @@ -99,17 +109,17 @@ automationStore.actions.select(automation._id)} selectedBy={$userSelectedResourceMap[automation._id]} disabled={automation.disabled} > -
- -
+
{automation.name}? This action cannot be undone. - - + diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte index 58eebfdd3e..a26efdf243 100644 --- a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte @@ -3,13 +3,21 @@ import { Modal, notifications, Layout } from "@budibase/bbui" import NavHeader from "components/common/NavHeader.svelte" import { onMount } from "svelte" - import { automationStore } from "stores/builder" + import { automationStore, tables } from "stores/builder" import AutomationNavItem from "./AutomationNavItem.svelte" + import { TriggerStepID } from "constants/backend/automations" export let modal export let webhookModal let searchString + const dsTriggers = [ + TriggerStepID.ROW_SAVED, + TriggerStepID.ROW_UPDATED, + TriggerStepID.ROW_DELETED, + TriggerStepID.ROW_ACTION, + ] + $: filteredAutomations = $automationStore.automations .filter(automation => { return ( @@ -17,31 +25,53 @@ automation.name.toLowerCase().includes(searchString.toLowerCase()) ) }) - .map(automation => ({ - ...automation, - displayName: - $automationStore.automationDisplayData[automation._id]?.displayName || - automation.name, - })) .sort((a, b) => { - const lowerA = a.displayName.toLowerCase() - const lowerB = b.displayName.toLowerCase() + const lowerA = a.name.toLowerCase() + const lowerB = b.name.toLowerCase() return lowerA > lowerB ? 1 : -1 }) - $: groupedAutomations = filteredAutomations.reduce((acc, auto) => { - const catName = auto.definition?.trigger?.event || "No Trigger" - acc[catName] ??= { - icon: auto.definition?.trigger?.icon || "AlertCircle", - name: (auto.definition?.trigger?.name || "No Trigger").toUpperCase(), - entries: [], - } - acc[catName].entries.push(auto) - return acc - }, {}) + $: groupedAutomations = groupAutomations(filteredAutomations) $: showNoResults = searchString && !filteredAutomations.length + const groupAutomations = automations => { + let groups = {} + + for (let auto of automations) { + let category = null + let dataTrigger = false + + // Group by datasource if possible + if (dsTriggers.includes(auto.definition?.trigger?.stepId)) { + if (auto.definition.trigger.inputs?.tableId) { + const tableId = auto.definition.trigger.inputs?.tableId + category = $tables.list.find(x => x._id === tableId)?.name + } + } + // Otherwise group by trigger + if (!category) { + category = auto.definition?.trigger?.name || "No Trigger" + } else { + dataTrigger = true + } + groups[category] ??= { + icon: auto.definition?.trigger?.icon || "AlertCircle", + name: category.toUpperCase(), + entries: [], + dataTrigger, + } + groups[category].entries.push(auto) + } + + return Object.values(groups).sort((a, b) => { + if (a.dataTrigger === b.dataTrigger) { + return a.name < b.name ? -1 : 1 + } + return a.dataTrigger ? -1 : 1 + }) + } + onMount(async () => { try { await automationStore.actions.fetch() @@ -88,16 +118,22 @@ diff --git a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte deleted file mode 100644 index 684cbd6cf4..0000000000 --- a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - {#if view.calculation} - - {/if} - - - -
diff --git a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte deleted file mode 100644 index a35e1b034e..0000000000 --- a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - -
- - - - - - - - - -
- - diff --git a/packages/builder/src/components/backend/DataTable/buttons/EditRolesButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/EditRolesButton.svelte deleted file mode 100644 index 87ca2fa142..0000000000 --- a/packages/builder/src/components/backend/DataTable/buttons/EditRolesButton.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - Edit roles - - - - diff --git a/packages/builder/src/components/backend/DataTable/buttons/ExportButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/ExportButton.svelte index 4fa1d07abd..e9352045ea 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/ExportButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/ExportButton.svelte @@ -1,20 +1,144 @@ - - Export - - - - + + + + Export + + + + {#if selectedRows?.length} + + + {selectedRows?.length} + {`row${selectedRows?.length > 1 ? "s" : ""} will be exported.`} + + + {:else} + + + Exporting all rows. + + + {/if} + + + + + + + diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridScreensButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridScreensButton.svelte new file mode 100644 index 0000000000..db446b3c9e --- /dev/null +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridScreensButton.svelte @@ -0,0 +1,59 @@ + + + + + + Screens{screenCount ? `: ${screenCount}` : ""} + + + {#if !connectedScreens.length} + There aren't any screens connected to this data. + {:else} + The following screens are connected to this data. + + {#each connectedScreens as screen} + + {/each} + + {/if} +
+ +
+
diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridSizeButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridSizeButton.svelte new file mode 100644 index 0000000000..148d8b1d5f --- /dev/null +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridSizeButton.svelte @@ -0,0 +1,127 @@ + + + + + + Size + + +
+ +
+ {#each rowSizeOptions as option} + changeRowHeight(option.size)} + > + {option.label} + + {/each} +
+
+
+ +
+ {#each columnSizeOptions as option} + columns.actions.changeAllColumnWidths(option.size)} + selected={option.selected} + > + {option.label} + + {/each} + {#if custom} + Custom + {/if} +
+
+
+ + diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridSortButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridSortButton.svelte new file mode 100644 index 0000000000..244a1b1560 --- /dev/null +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridSortButton.svelte @@ -0,0 +1,79 @@ + + + + + + Sort + + + + {/if} + diff --git a/packages/builder/src/components/backend/DataTable/modals/grid/GridUsersTableButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridUsersTableButton.svelte similarity index 100% rename from packages/builder/src/components/backend/DataTable/modals/grid/GridUsersTableButton.svelte rename to packages/builder/src/components/backend/DataTable/buttons/grid/GridUsersTableButton.svelte diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte new file mode 100644 index 0000000000..7b279d3948 --- /dev/null +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridViewCalculationButton.svelte @@ -0,0 +1,267 @@ + + + + + + Configure calculations{count ? `: ${count}` : ""} + + + + {#if calculations.length} +
+ {#each calculations as calc, idx} + {idx === 0 ? "Calculate" : "and"} the + + deleteCalc(idx)} + color="var(--spectrum-global-color-gray-700)" + /> + {/each} + Group by +
+ +
+
+ {/if} +
+ = 5} + > + Add calculation + +
+ + +
+ +
+
+ + diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/magic-wand.svg b/packages/builder/src/components/backend/DataTable/buttons/grid/magic-wand.svg new file mode 100644 index 0000000000..1702c9470a --- /dev/null +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/magic-wand.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/builder/src/components/backend/DataTable/formula.js b/packages/builder/src/components/backend/DataTable/formula.js index 7220a5ba4f..a9491abfef 100644 --- a/packages/builder/src/components/backend/DataTable/formula.js +++ b/packages/builder/src/components/backend/DataTable/formula.js @@ -8,6 +8,7 @@ const MAX_DEPTH = 1 const TYPES_TO_SKIP = [ FieldType.FORMULA, + FieldType.AI, FieldType.LONGFORM, FieldType.SIGNATURE_SINGLE, FieldType.ATTACHMENTS, diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index a1bd54715b..d16bca3203 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -4,6 +4,7 @@ Button, Label, Select, + Multiselect, Toggle, Icon, DatePicker, @@ -25,6 +26,7 @@ import { createEventDispatcher, getContext, onMount } from "svelte" import { cloneDeep } from "lodash/fp" import { tables, datasources } from "stores/builder" + import { featureFlags } from "stores/portal" import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" import { FIELDS, @@ -34,6 +36,7 @@ } from "constants/backend" import { getAutoColumnInformation, buildAutoColumn } from "helpers/utils" import ConfirmDialog from "components/common/ConfirmDialog.svelte" + import AIFieldConfiguration from "components/common/AIFieldConfiguration.svelte" import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte" import { getBindings } from "components/backend/DataTable/formula" import JSONSchemaModal from "./JSONSchemaModal.svelte" @@ -49,18 +52,13 @@ import { isEnabled } from "helpers/featureFlags" import { getUserBindings } from "dataBinding" - const AUTO_TYPE = FieldType.AUTO - const FORMULA_TYPE = FieldType.FORMULA - const LINK_TYPE = FieldType.LINK - const STRING_TYPE = FieldType.STRING - const NUMBER_TYPE = FieldType.NUMBER - const JSON_TYPE = FieldType.JSON - const DATE_TYPE = FieldType.DATETIME + export let field const dispatch = createEventDispatcher() const { dispatch: gridDispatch, rows } = getContext("grid") - - export let field + const SafeID = `${makePropSafe("user")}.${makePropSafe("_id")}` + const SingleUserDefault = `{{ ${SafeID} }}` + const MultiUserDefault = `{{ js "${btoa(`return [$("${SafeID}")]`)}" }}` let mounted = false let originalName @@ -103,13 +101,14 @@ let optionsValid = true $: rowGoldenSample = RowUtils.generateGoldenSample($rows) + $: aiEnabled = $featureFlags.BUDIBASE_AI || $featureFlags.AI_CUSTOM_CONFIGS $: if (primaryDisplay) { editableColumn.constraints.presence = { allowEmpty: false } } $: { // this parses any changes the user has made when creating a new internal relationship // into what we expect the schema to look like - if (editableColumn.type === LINK_TYPE) { + if (editableColumn.type === FieldType.LINK) { relationshipTableIdPrimary = table._id if (relationshipPart1 === PrettyRelationshipDefinitions.ONE) { relationshipOpts2 = relationshipOpts2.filter( @@ -137,15 +136,16 @@ } $: initialiseField(field, savingColumn) $: checkConstraints(editableColumn) - $: required = hasDefault - ? false - : !!editableColumn?.constraints?.presence || primaryDisplay + $: required = + primaryDisplay || + editableColumn?.constraints?.presence === true || + editableColumn?.constraints?.presence?.allowEmpty === false $: uneditable = $tables.selected?._id === TableNames.USERS && UNEDITABLE_USER_FIELDS.includes(editableColumn.name) $: invalid = !editableColumn?.name || - (editableColumn?.type === LINK_TYPE && !editableColumn?.tableId) || + (editableColumn?.type === FieldType.LINK && !editableColumn?.tableId) || Object.keys(errors).length !== 0 || !optionsValid $: errors = checkErrors(editableColumn) @@ -168,12 +168,12 @@ // used to select what different options can be displayed for column type $: canBeDisplay = canBeDisplayColumn(editableColumn) && !editableColumn.autocolumn - $: canHaveDefault = - isEnabled("DEFAULT_VALUES") && canHaveDefaultColumn(editableColumn.type) + $: defaultValuesEnabled = isEnabled("DEFAULT_VALUES") + $: canHaveDefault = !required && canHaveDefaultColumn(editableColumn.type) $: canBeRequired = - editableColumn?.type !== LINK_TYPE && + editableColumn?.type !== FieldType.LINK && !uneditable && - editableColumn?.type !== AUTO_TYPE && + editableColumn?.type !== FieldType.AUTO && !editableColumn.autocolumn $: hasDefault = editableColumn?.default != null && editableColumn?.default !== "" @@ -188,7 +188,6 @@ (originalName && SWITCHABLE_TYPES[field.type] && !editableColumn?.autocolumn) - $: allowedTypes = getAllowedTypes(datasource).map(t => ({ fieldId: makeFieldId(t.type, t.subtype), ...t, @@ -206,6 +205,11 @@ }, ...getUserBindings(), ] + $: sanitiseDefaultValue( + editableColumn.type, + editableColumn.constraints?.inclusion || [], + editableColumn.default + ) const fieldDefinitions = Object.values(FIELDS).reduce( // Storing the fields by complex field id @@ -218,7 +222,7 @@ function makeFieldId(type, subtype, autocolumn) { // don't make field IDs for auto types - if (type === AUTO_TYPE || autocolumn) { + if (type === FieldType.AUTO || autocolumn) { return type.toUpperCase() } else if ( type === FieldType.BB_REFERENCE || @@ -243,7 +247,7 @@ // Here we are setting the relationship values based on the editableColumn // This part of the code is used when viewing an existing field hence the check // for the tableId - if (editableColumn.type === LINK_TYPE && editableColumn.tableId) { + if (editableColumn.type === FieldType.LINK && editableColumn.tableId) { relationshipTableIdPrimary = table._id relationshipTableIdSecondary = editableColumn.tableId if (editableColumn.relationshipType in relationshipMap) { @@ -284,17 +288,33 @@ delete saveColumn.fieldId - if (saveColumn.type === AUTO_TYPE) { + if (saveColumn.type === FieldType.AUTO) { saveColumn = buildAutoColumn( $tables.selected.name, saveColumn.name, saveColumn.subtype ) } - if (saveColumn.type !== LINK_TYPE) { + if (saveColumn.type !== FieldType.LINK) { delete saveColumn.fieldName } + // Ensure we don't have a default value if we can't have one + if (!canHaveDefault || !defaultValuesEnabled) { + delete saveColumn.default + } + + // Ensure primary display columns are always required and don't have default values + if (primaryDisplay) { + saveColumn.constraints.presence = { allowEmpty: false } + delete saveColumn.default + } + + // Ensure the field is not required if we have a default value + if (saveColumn.default) { + saveColumn.constraints.presence = false + } + try { await tables.saveField({ originalName, @@ -362,9 +382,9 @@ editableColumn.subtype = definition.subtype // Default relationships many to many - if (editableColumn.type === LINK_TYPE) { + if (editableColumn.type === FieldType.LINK) { editableColumn.relationshipType = RelationshipType.MANY_TO_MANY - } else if (editableColumn.type === FORMULA_TYPE) { + } else if (editableColumn.type === FieldType.FORMULA) { editableColumn.formulaType = "dynamic" } } @@ -430,6 +450,7 @@ FIELDS.BOOLEAN, FIELDS.DATETIME, FIELDS.LINK, + ...(aiEnabled ? [FIELDS.AI] : []), FIELDS.LONGFORM, FIELDS.USER, FIELDS.USERS, @@ -483,17 +504,23 @@ fieldToCheck.constraints = {} } // some string types may have been built by server, may not always have constraints - if (fieldToCheck.type === STRING_TYPE && !fieldToCheck.constraints.length) { + if ( + fieldToCheck.type === FieldType.STRING && + !fieldToCheck.constraints.length + ) { fieldToCheck.constraints.length = {} } // some number types made server-side will be missing constraints if ( - fieldToCheck.type === NUMBER_TYPE && + fieldToCheck.type === FieldType.NUMBER && !fieldToCheck.constraints.numericality ) { fieldToCheck.constraints.numericality = {} } - if (fieldToCheck.type === DATE_TYPE && !fieldToCheck.constraints.datetime) { + if ( + fieldToCheck.type === FieldType.DATETIME && + !fieldToCheck.constraints.datetime + ) { fieldToCheck.constraints.datetime = {} } } @@ -541,6 +568,20 @@ return newError } + const sanitiseDefaultValue = (type, options, defaultValue) => { + if (!defaultValue?.length) { + return + } + // Delete default value for options fields if the option is no longer available + if (type === FieldType.OPTIONS && !options.includes(defaultValue)) { + delete editableColumn.default + } + // Filter array default values to only valid options + if (type === FieldType.ARRAY) { + editableColumn.default = defaultValue.filter(x => options.includes(x)) + } + } + onMount(() => { mounted = true }) @@ -554,13 +595,13 @@ on:input={e => { if ( !uneditable && - !(linkEditDisabled && editableColumn.type === LINK_TYPE) + !(linkEditDisabled && editableColumn.type === FieldType.LINK) ) { editableColumn.name = e.target.value } }} disabled={uneditable || - (linkEditDisabled && editableColumn.type === LINK_TYPE)} + (linkEditDisabled && editableColumn.type === FieldType.LINK)} error={errors?.name} /> {/if} @@ -574,7 +615,7 @@ getOptionValue={field => field.fieldId} getOptionIcon={field => field.icon} isOptionEnabled={option => { - if (option.type === AUTO_TYPE) { + if (option.type === FieldType.AUTO) { return availableAutoColumnKeys?.length > 0 } return true @@ -617,7 +658,7 @@ bind:optionColors={editableColumn.optionColors} bind:valid={optionsValid} /> - {:else if editableColumn.type === DATE_TYPE && !editableColumn.autocolumn} + {:else if editableColumn.type === FieldType.DATETIME && !editableColumn.autocolumn}
@@ -704,7 +745,7 @@ {tableOptions} {errors} /> - {:else if editableColumn.type === FORMULA_TYPE} + {:else if editableColumn.type === FieldType.FORMULA} {#if !externalTable}
@@ -747,12 +788,19 @@ />
- {:else if editableColumn.type === JSON_TYPE} - + {:else if editableColumn.type === FieldType.AI} + + {:else if editableColumn.type === FieldType.JSON} + {/if} - {#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn} + {#if editableColumn.type === FieldType.AUTO || editableColumn.autocolumn} role._id} - getOptionLabel={role => role.name} - /> - {#if selectedRole} - - x._id} - getOptionLabel={x => x.name} - disabled={shouldDisableRoleInput} - /> - {/if} -
- {#if !isCreating && !builtInRoles.includes(selectedRole.name)} - - {/if} -
- diff --git a/packages/builder/src/components/backend/DataTable/modals/ExportModal.svelte b/packages/builder/src/components/backend/DataTable/modals/ExportModal.svelte deleted file mode 100644 index 2a31f5c452..0000000000 --- a/packages/builder/src/components/backend/DataTable/modals/ExportModal.svelte +++ /dev/null @@ -1,224 +0,0 @@ - - - - {#if selectedRows?.length} - - - {selectedRows?.length} - {`row${selectedRows?.length > 1 ? "s" : ""} will be exported`} - - - {:else if appliedFilters?.length || (sorting?.sortOrder && sorting?.sortColumn)} - - {#if !appliedFilters} - - Exporting all rows - - {:else} - Filters applied - {/if} - - -
- - - {:else} - - - Exporting all rows - - - {/if} - - - - diff --git a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte index e0745c15a1..d7a98c564a 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte @@ -39,9 +39,7 @@ const selectTable = tableId => { tables.select(tableId) - if (!$isActive("./table/:tableId")) { - $goto(`./table/${tableId}`) - } + $goto(`./table/${tableId}`) } function openNode(datasource) { @@ -78,6 +76,13 @@ selectedBy={$userSelectedResourceMap[TableNames.USERS]} /> {/if} + $goto("./roles")} + selectedBy={$userSelectedResourceMap.roles} + /> {#each enrichedDataSources.filter(ds => ds.show) as datasource} + import { BaseEdge } from "@xyflow/svelte" + import { NodeWidth, GridResolution } from "./constants" + import { getContext } from "svelte" + + export let sourceX + export let sourceY + + const { bounds } = getContext("flow") + + $: bracketWidth = GridResolution * 3 + $: bracketHeight = $bounds.height / 2 + GridResolution * 2 + $: path = getCurlyBracePath( + sourceX + bracketWidth, + sourceY - bracketHeight, + sourceX + bracketWidth, + sourceY + bracketHeight + ) + + const getCurlyBracePath = (x1, y1, x2, y2) => { + const w = 2 // Thickness + const q = 1 // Intensity + const i = 28 // Inner radius strenth (lower is stronger) + const j = 32 // Outer radius strength (higher is stronger) + + // Calculate unit vector + var dx = x1 - x2 + var dy = y1 - y2 + var len = Math.sqrt(dx * dx + dy * dy) + dx = dx / len + dy = dy / len + + // Path control points + const qx1 = x1 + q * w * dy - j + const qy1 = y1 - q * w * dx + const qx2 = x1 - 0.25 * len * dx + (1 - q) * w * dy - i + const qy2 = y1 - 0.25 * len * dy - (1 - q) * w * dx + const tx1 = x1 - 0.5 * len * dx + w * dy - bracketWidth + const ty1 = y1 - 0.5 * len * dy - w * dx + const qx3 = x2 + q * w * dy - j + const qy3 = y2 - q * w * dx + const qx4 = x1 - 0.75 * len * dx + (1 - q) * w * dy - i + const qy4 = y1 - 0.75 * len * dy - (1 - q) * w * dx + + return `M ${x1} ${y1} Q ${qx1} ${qy1} ${qx2} ${qy2} T ${tx1} ${ty1} M ${x2} ${y2} Q ${qx3} ${qy3} ${qx4} ${qy4} T ${tx1} ${ty1}` + } + + + + + diff --git a/packages/builder/src/components/backend/RoleEditor/Controls.svelte b/packages/builder/src/components/backend/RoleEditor/Controls.svelte new file mode 100644 index 0000000000..b695a71a43 --- /dev/null +++ b/packages/builder/src/components/backend/RoleEditor/Controls.svelte @@ -0,0 +1,74 @@ + + +
+
+ flow.zoomIn({ duration: ZoomDuration })} + /> + flow.zoomOut({ duration: ZoomDuration })} + /> +
+ +
+
+ +
+ + diff --git a/packages/builder/src/components/backend/RoleEditor/EmptyStateNode.svelte b/packages/builder/src/components/backend/RoleEditor/EmptyStateNode.svelte new file mode 100644 index 0000000000..2c949ed0f9 --- /dev/null +++ b/packages/builder/src/components/backend/RoleEditor/EmptyStateNode.svelte @@ -0,0 +1,24 @@ + + +
+ Add custom roles for more granular control over permissions +
+ + diff --git a/packages/builder/src/components/backend/RoleEditor/RoleEdge.svelte b/packages/builder/src/components/backend/RoleEditor/RoleEdge.svelte new file mode 100644 index 0000000000..84badeb6b2 --- /dev/null +++ b/packages/builder/src/components/backend/RoleEditor/RoleEdge.svelte @@ -0,0 +1,123 @@ + + + + + + + +
deleteEdge(id)} + on:mouseover={() => (iconHovered = true)} + on:mouseout={() => (iconHovered = false)} + > + +
+
+ + diff --git a/packages/builder/src/components/backend/RoleEditor/RoleEditor.svelte b/packages/builder/src/components/backend/RoleEditor/RoleEditor.svelte new file mode 100644 index 0000000000..6169013d12 --- /dev/null +++ b/packages/builder/src/components/backend/RoleEditor/RoleEditor.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/packages/builder/src/components/backend/RoleEditor/RoleFlow.svelte b/packages/builder/src/components/backend/RoleEditor/RoleFlow.svelte new file mode 100644 index 0000000000..90c0a9fca8 --- /dev/null +++ b/packages/builder/src/components/backend/RoleEditor/RoleFlow.svelte @@ -0,0 +1,234 @@ + + +
+
+
+
+ dragging.set(true)} + onconnectend={() => dragging.set(false)} + onconnect={onConnect} + deleteKey={null} + > + + +
+ Manage roles +
+ +
+
+ + diff --git a/packages/builder/src/components/backend/RoleEditor/RoleNode.svelte b/packages/builder/src/components/backend/RoleEditor/RoleNode.svelte new file mode 100644 index 0000000000..32a95f4278 --- /dev/null +++ b/packages/builder/src/components/backend/RoleEditor/RoleNode.svelte @@ -0,0 +1,231 @@ + + +
+
+
+
+
+ {data.displayName} +
+ {#if data.description} +
+ {data.description} +
+ {/if} +
+ {#if data.custom} +
+ + +
+ {/if} +
+ + +
+ + await deleteRole(id)} +/> + + + + (tempDisplayName = e.detail)} + /> + (tempDescription = e.detail)} + /> +
+ + (tempColor = e.detail)} /> +
+
+
+ + diff --git a/packages/builder/src/components/backend/RoleEditor/constants.js b/packages/builder/src/components/backend/RoleEditor/constants.js new file mode 100644 index 0000000000..6f188e2141 --- /dev/null +++ b/packages/builder/src/components/backend/RoleEditor/constants.js @@ -0,0 +1,9 @@ +export const ZoomDuration = 300 +export const MaxAutoZoom = 1.2 +export const GridResolution = 20 +export const NodeHeight = GridResolution * 3 +export const NodeWidth = GridResolution * 12 +export const NodeHSpacing = GridResolution * 6 +export const NodeVSpacing = GridResolution * 2 +export const MinHeight = GridResolution * 10 +export const EmptyStateID = "empty" diff --git a/packages/builder/src/components/backend/RoleEditor/utils.js b/packages/builder/src/components/backend/RoleEditor/utils.js new file mode 100644 index 0000000000..a958fc6401 --- /dev/null +++ b/packages/builder/src/components/backend/RoleEditor/utils.js @@ -0,0 +1,245 @@ +import dagre from "@dagrejs/dagre" +import { + NodeWidth, + NodeHeight, + GridResolution, + NodeHSpacing, + NodeVSpacing, + MinHeight, + EmptyStateID, +} from "./constants" +import { getNodesBounds, Position } from "@xyflow/svelte" +import { Roles } from "constants/backend" +import { roles } from "stores/builder" +import { get } from "svelte/store" + +// Calculates the bounds of all custom nodes +export const getBounds = nodes => { + const interactiveNodes = nodes.filter(node => node.data.interactive) + + // Empty state bounds which line up with bounds after adding first node + if (!interactiveNodes.length) { + return { + x: 0, + y: -3.5 * GridResolution, + width: 12 * GridResolution, + height: 10 * GridResolution, + } + } + let bounds = getNodesBounds(interactiveNodes) + + // Enforce a min size + if (bounds.height < MinHeight) { + const diff = MinHeight - bounds.height + bounds.height = MinHeight + bounds.y -= diff / 2 + } + return bounds +} + +// Gets the position of the basic role +export const getBasicPosition = bounds => ({ + x: bounds.x - NodeHSpacing - NodeWidth, + y: bounds.y + bounds.height / 2 - NodeHeight / 2, +}) + +// Gets the position of the admin role +export const getAdminPosition = bounds => ({ + x: bounds.x + bounds.width + NodeHSpacing, + y: bounds.y + bounds.height / 2 - NodeHeight / 2, +}) + +// Filters out invalid nodes and edges +const preProcessLayout = ({ nodes, edges }) => { + const ignoredIds = [Roles.PUBLIC, Roles.BASIC, Roles.ADMIN, EmptyStateID] + const targetlessIds = [Roles.POWER] + return { + nodes: nodes.filter(node => { + // Filter out ignored IDs + if (ignoredIds.includes(node.id)) { + return false + } + return true + }), + edges: edges.filter(edge => { + // Filter out edges from ignored IDs + if ( + ignoredIds.includes(edge.source) || + ignoredIds.includes(edge.target) + ) { + return false + } + // Filter out edges which have the same source and target + if (edge.source === edge.target) { + return false + } + // Filter out edges which target targetless roles + if (targetlessIds.includes(edge.target)) { + return false + } + return true + }), + } +} + +// Updates positions of nodes and edges into a nice graph structure +export const dagreLayout = ({ nodes, edges }) => { + const dagreGraph = new dagre.graphlib.Graph() + dagreGraph.setDefaultEdgeLabel(() => ({})) + dagreGraph.setGraph({ + rankdir: "LR", + ranksep: NodeHSpacing, + nodesep: NodeVSpacing, + }) + nodes.forEach(node => { + dagreGraph.setNode(node.id, { width: NodeWidth, height: NodeHeight }) + }) + edges.forEach(edge => { + dagreGraph.setEdge(edge.source, edge.target) + }) + dagre.layout(dagreGraph) + nodes.forEach(node => { + const pos = dagreGraph.node(node.id) + node.targetPosition = Position.Left + node.sourcePosition = Position.Right + node.position = { + x: Math.round((pos.x - NodeWidth / 2) / GridResolution) * GridResolution, + y: Math.round((pos.y - NodeHeight / 2) / GridResolution) * GridResolution, + } + }) + return { nodes, edges } +} + +const postProcessLayout = ({ nodes, edges }) => { + // Add basic and admin nodes at each edge + const bounds = getBounds(nodes) + const $roles = get(roles) + nodes.push({ + ...roleToNode($roles.find(role => role._id === Roles.BASIC)), + position: getBasicPosition(bounds), + }) + nodes.push({ + ...roleToNode($roles.find(role => role._id === Roles.ADMIN)), + position: getAdminPosition(bounds), + }) + + // Add custom edges for basic and admin brackets + edges.push({ + id: "basic-bracket", + source: Roles.BASIC, + target: Roles.ADMIN, + type: "bracket", + }) + edges.push({ + id: "admin-bracket", + source: Roles.ADMIN, + target: Roles.BASIC, + type: "bracket", + }) + + // Add empty state node if required + if (!nodes.some(node => node.data.interactive)) { + nodes.push({ + id: EmptyStateID, + type: "empty", + position: { + x: bounds.x + bounds.width / 2 - NodeWidth / 2, + y: bounds.y + bounds.height / 2 - NodeHeight / 2, + }, + data: {}, + measured: { + width: NodeWidth, + height: NodeHeight, + }, + deletable: false, + draggable: false, + connectable: false, + selectable: false, + }) + } + + return { nodes, edges } +} + +// Automatically lays out the graph, sanitising and enriching the structure +export const autoLayout = ({ nodes, edges }) => { + return postProcessLayout(dagreLayout(preProcessLayout({ nodes, edges }))) +} + +// Converts a role doc into a node structure +export const roleToNode = role => { + const custom = ![ + Roles.PUBLIC, + Roles.BASIC, + Roles.POWER, + Roles.ADMIN, + Roles.BUILDER, + ].includes(role._id) + const interactive = custom || role._id === Roles.POWER + return { + id: role._id, + sourcePosition: Position.Right, + targetPosition: Position.Left, + type: "role", + position: { x: 0, y: 0 }, + data: { + ...role.uiMetadata, + custom, + interactive, + }, + measured: { + width: NodeWidth, + height: NodeHeight, + }, + deletable: custom, + draggable: interactive, + connectable: interactive, + selectable: interactive, + } +} + +// Converts a node structure back into a role doc +export const nodeToRole = ({ node, edges }) => ({ + ...get(roles).find(role => role._id === node.id), + inherits: edges + .filter(x => x.target === node.id) + .map(x => x.source) + .concat(Roles.BASIC), + uiMetadata: { + displayName: node.data.displayName, + color: node.data.color, + description: node.data.description, + }, +}) + +// Builds a default layout from an array of roles +export const rolesToLayout = roles => { + let nodes = [] + let edges = [] + + // Add all nodes and edges + for (let role of roles) { + // Add node for this role + nodes.push(roleToNode(role)) + + // Add edges for this role + let inherits = [] + if (role.inherits) { + inherits = Array.isArray(role.inherits) ? role.inherits : [role.inherits] + } + for (let sourceRole of inherits) { + if (!roles.some(x => x._id === sourceRole)) { + continue + } + edges.push({ + id: `${sourceRole}-${role._id}`, + source: sourceRole, + target: role._id, + }) + } + } + return { + nodes, + edges, + } +} diff --git a/packages/builder/src/components/backend/TableNavigator/ExistingTableDataImport.svelte b/packages/builder/src/components/backend/TableNavigator/ExistingTableDataImport.svelte index f1d85a6a30..ce966d0daa 100644 --- a/packages/builder/src/components/backend/TableNavigator/ExistingTableDataImport.svelte +++ b/packages/builder/src/components/backend/TableNavigator/ExistingTableDataImport.svelte @@ -4,7 +4,7 @@ BBReferenceFieldSubType, SourceName, } from "@budibase/types" - import { Select, Toggle, Multiselect } from "@budibase/bbui" + import { Select, Toggle, Multiselect, Label, Layout } from "@budibase/bbui" import { DB_TYPE_INTERNAL } from "constants/backend" import { API } from "api" import { parseFile } from "./utils" @@ -140,84 +140,91 @@ } -
- - -
-{#if fileName && Object.keys(validation).length === 0} -

No valid fields, try another file

-{:else if rows.length > 0 && !error} -
- {#each Object.keys(validation) as name} -
- {name} - - {:else} -

Rows will be updated based on the table's primary key.

+ +
+ + + {#if fileName && Object.keys(validation).length === 0} +
No valid fields - please try another file.
+ {:else if fileName && rows.length > 0 && !error} +
+ {#each Object.keys(validation) as name} +
+ {name} + - -
-{#if rawRows.length > 0 && !error} -
- {#each Object.entries(schema) as [name, column]} -
- {column.name} - + +
+ + + {#if rawRows.length > 0 && !error} +
+ {#each Object.entries(schema) as [name, column]} +
+ {column.name} + -
-{/if} + {/if} + diff --git a/packages/builder/src/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte b/packages/builder/src/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte index 03da9f3fd3..c0e030845f 100644 --- a/packages/builder/src/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte +++ b/packages/builder/src/components/backend/TableNavigator/TableNavItem/DeleteConfirmationModal.svelte @@ -104,7 +104,7 @@
{/if} -

Please enter the app name below to confirm.

+

Please enter the table name below to confirm.

diff --git a/packages/builder/src/components/backend/TableNavigator/TableNavItem/TableNavItem.svelte b/packages/builder/src/components/backend/TableNavigator/TableNavItem/TableNavItem.svelte index ab79a8fff0..6b64096e2e 100644 --- a/packages/builder/src/components/backend/TableNavigator/TableNavItem/TableNavItem.svelte +++ b/packages/builder/src/components/backend/TableNavigator/TableNavItem/TableNavItem.svelte @@ -20,14 +20,6 @@ const getContextMenuItems = () => { return [ - { - icon: "Delete", - name: "Delete", - keyBind: null, - visible: true, - disabled: false, - callback: deleteConfirmationModal.show, - }, { icon: "Edit", name: "Edit", @@ -36,6 +28,14 @@ disabled: false, callback: editModal.show, }, + { + icon: "Delete", + name: "Delete", + keyBind: null, + visible: true, + disabled: false, + callback: deleteConfirmationModal.show, + }, ] } diff --git a/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte b/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte index f21230d7a6..f97bd2487b 100644 --- a/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte +++ b/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte @@ -1,33 +1,15 @@
{#each sortedTables as table, idx} selectTable(table._id)} /> - {#each [...Object.entries(table.views || {})].sort() as [name, view], idx (idx)} - { - if (view.version === 2) { - $goto(`./view/v2/${encodeURIComponent(view.id)}`) - } else { - $goto(`./view/v1/${encodeURIComponent(name)}`) - } - }} - /> - {/each} {/each}
diff --git a/packages/builder/src/components/backend/TableNavigator/ViewNavItem/ViewNavItem.svelte b/packages/builder/src/components/backend/TableNavigator/ViewNavItem/ViewNavItem.svelte deleted file mode 100644 index ba30cea165..0000000000 --- a/packages/builder/src/components/backend/TableNavigator/ViewNavItem/ViewNavItem.svelte +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - diff --git a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte index b62c8af03d..5596ade573 100644 --- a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte +++ b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte @@ -1,13 +1,7 @@ + + + {/if} + {/each} +{/if} diff --git a/packages/builder/src/components/common/DetailPopover.svelte b/packages/builder/src/components/common/DetailPopover.svelte new file mode 100644 index 0000000000..f1e81a6340 --- /dev/null +++ b/packages/builder/src/components/common/DetailPopover.svelte @@ -0,0 +1,77 @@ + + + + +
+ +
+ + +
+
+
+ {title} +
+ +
+
+ +
+
+
+ + diff --git a/packages/builder/src/components/common/RoleIcon.svelte b/packages/builder/src/components/common/RoleIcon.svelte index 1bd6ba49bc..3b48935e0c 100644 --- a/packages/builder/src/components/common/RoleIcon.svelte +++ b/packages/builder/src/components/common/RoleIcon.svelte @@ -1,12 +1,14 @@ diff --git a/packages/builder/src/components/common/RoleSelect.svelte b/packages/builder/src/components/common/RoleSelect.svelte index 6006b8ab8d..38b84e964d 100644 --- a/packages/builder/src/components/common/RoleSelect.svelte +++ b/packages/builder/src/components/common/RoleSelect.svelte @@ -3,7 +3,7 @@ import { roles } from "stores/builder" import { licensing } from "stores/portal" - import { Constants, RoleUtils } from "@budibase/frontend-core" + import { Constants } from "@budibase/frontend-core" import { createEventDispatcher } from "svelte" import { capitalise } from "helpers" @@ -49,7 +49,8 @@ let options = roles .filter(role => allowedRoles.includes(role._id)) .map(role => ({ - name: enrichLabel(role.name), + color: role.uiMetadata.color, + name: enrichLabel(role.uiMetadata.displayName), _id: role._id, })) if (allowedRoles.includes(Constants.Roles.CREATOR)) { @@ -64,7 +65,8 @@ // Allow all core roles let options = roles.map(role => ({ - name: enrichLabel(role.name), + color: role.uiMetadata.color, + name: enrichLabel(role.uiMetadata.displayName), _id: role._id, })) @@ -100,7 +102,7 @@ if (role._id === Constants.Roles.CREATOR || role._id === RemoveID) { return null } - return RoleUtils.getRoleColour(role._id) + return role.color || "var(--spectrum-global-color-static-magenta-400)" } const getIcon = role => { diff --git a/packages/frontend-core/src/components/grid/controls/ToggleActionButtonGroup.svelte b/packages/builder/src/components/common/ToggleActionButtonGroup.svelte similarity index 82% rename from packages/frontend-core/src/components/grid/controls/ToggleActionButtonGroup.svelte rename to packages/builder/src/components/common/ToggleActionButtonGroup.svelte index 497e77c2c9..8a5778534f 100644 --- a/packages/frontend-core/src/components/grid/controls/ToggleActionButtonGroup.svelte +++ b/packages/builder/src/components/common/ToggleActionButtonGroup.svelte @@ -9,7 +9,7 @@ export let options -
+
{#each options as option} diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte index c23edbeb58..819ed10880 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte @@ -33,7 +33,7 @@ const getSchemaFields = resourceId => { const { schema } = getSchemaForDatasourcePlus(resourceId) - return Object.values(schema || {}) + return Object.values(schema || {}).filter(field => !field.readonly) } const onFieldsChanged = e => { diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js index 606ee41d02..b171b34111 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/index.js @@ -25,3 +25,4 @@ export { default as OpenModal } from "./OpenModal.svelte" export { default as CloseModal } from "./CloseModal.svelte" export { default as ClearRowSelection } from "./ClearRowSelection.svelte" export { default as DownloadFile } from "./DownloadFile.svelte" +export { default as RowAction } from "./RowAction.svelte" diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json index 4022926e7f..631e3119e8 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json @@ -178,6 +178,11 @@ "name": "Download File", "type": "data", "component": "DownloadFile" + }, + { + "name": "Row Action", + "type": "data", + "component": "RowAction" } ] } diff --git a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte index db2289345f..6f3a13a745 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte @@ -2,10 +2,11 @@ import DraggableList from "../DraggableList/DraggableList.svelte" import ButtonSetting from "./ButtonSetting.svelte" import { createEventDispatcher } from "svelte" - import { Helpers } from "@budibase/bbui" + import { Helpers, Menu, MenuItem, Popover } from "@budibase/bbui" import { componentStore } from "stores/builder" import { getEventContextBindings } from "dataBinding" import { cloneDeep, isEqual } from "lodash/fp" + import { getRowActionButtonTemplates } from "templates/rowActions" export let componentInstance export let componentBindings @@ -17,13 +18,14 @@ const dispatch = createEventDispatcher() - let focusItem let cachedValue + let rowActionTemplates = [] + let anchor + let popover $: if (!isEqual(value, cachedValue)) { cachedValue = cloneDeep(value) } - $: buttonList = sanitizeValue(cachedValue) || [] $: buttonCount = buttonList.length $: eventContextBindings = getEventContextBindings({ @@ -73,17 +75,32 @@ _instanceName: Helpers.uuid(), text: cfg.text, type: cfg.type || "primary", - }, - {} + } ) } - const addButton = () => { + const addCustomButton = () => { const newButton = buildPseudoInstance({ text: `Button ${buttonCount + 1}`, }) dispatch("change", [...buttonList, newButton]) - focusItem = newButton._id + popover.hide() + } + + const addRowActionTemplate = template => { + dispatch("change", [...buttonList, template]) + popover.hide() + } + + const addButton = async () => { + rowActionTemplates = await getRowActionButtonTemplates({ + component: componentInstance, + }) + if (rowActionTemplates.length) { + popover.show() + } else { + addCustomButton() + } } const removeButton = id => { @@ -105,12 +122,11 @@ listItemKey={"_id"} listType={ButtonSetting} listTypeProps={itemProps} - focus={focusItem} draggable={buttonCount > 1} /> {/if} - + + + Custom button + {#each rowActionTemplates as template} + addRowActionTemplate(template)}> + {template.text} + + {/each} + + + diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index be8337b376..8e109d3145 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -228,7 +228,7 @@ .top-nav { flex: 0 0 60px; background: var(--background); - padding-left: var(--spacing-xl); + padding: 0 var(--spacing-xl); display: grid; grid-template-columns: 1fr auto 1fr; flex-direction: row; @@ -269,6 +269,7 @@ flex-direction: row; justify-content: flex-end; align-items: center; + margin-right: calc(-1 * var(--spacing-xl)); } .toprightnav :global(.avatars) { diff --git a/packages/builder/src/pages/builder/app/[application]/data/roles.svelte b/packages/builder/src/pages/builder/app/[application]/data/roles.svelte new file mode 100644 index 0000000000..0177577730 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/data/roles.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/[viewId]/_layout.svelte similarity index 95% rename from packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/_layout.svelte rename to packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/[viewId]/_layout.svelte index 3d0cffc387..9c1baa723e 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/[viewId]/_layout.svelte @@ -12,7 +12,7 @@ stateKey: "selectedViewId", validate: id => $viewsV2.list?.some(view => view.id === id), update: viewsV2.select, - fallbackUrl: "../../", + fallbackUrl: "../", store: viewsV2, routify, decode: decodeURIComponent, diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/[viewId]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/[viewId]/index.svelte new file mode 100644 index 0000000000..39b1dc4768 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/[viewId]/index.svelte @@ -0,0 +1,78 @@ + + + + + + {#if calculation} + + {/if} + + + + {#if !calculation} + + + generateButton?.show()} /> + {/if} + + + + diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/CreateViewButton.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/CreateViewButton.svelte new file mode 100644 index 0000000000..ecc85e7622 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/CreateViewButton.svelte @@ -0,0 +1,133 @@ + + + (name = null)} + width={540} +> + + {#if firstView} + + {:else} +
+ +
+ {/if} +
+
+
+ (calculation = false)} + selected={!calculation} + icon="Rail" + /> +
+
+ (calculation = true)} + selected={calculation} + icon="123" + /> +
+
+ +
+ +
+
+ + diff --git a/packages/builder/src/components/backend/TableNavigator/ViewNavItem/DeleteConfirmationModal.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/DeleteViewModal.svelte similarity index 96% rename from packages/builder/src/components/backend/TableNavigator/ViewNavItem/DeleteConfirmationModal.svelte rename to packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/DeleteViewModal.svelte index 774a2f987a..0b5f18b70b 100644 --- a/packages/builder/src/components/backend/TableNavigator/ViewNavItem/DeleteConfirmationModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/DeleteViewModal.svelte @@ -20,6 +20,7 @@ } notifications.success("View deleted") } catch (error) { + console.error(error) notifications.error("Error deleting view") } } diff --git a/packages/builder/src/components/backend/TableNavigator/ViewNavItem/EditViewModal.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/EditViewModal.svelte similarity index 87% rename from packages/builder/src/components/backend/TableNavigator/ViewNavItem/EditViewModal.svelte rename to packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/EditViewModal.svelte index 0809d55884..0f39fa063d 100644 --- a/packages/builder/src/components/backend/TableNavigator/ViewNavItem/EditViewModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/EditViewModal.svelte @@ -39,7 +39,7 @@ - - + + diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/ViewNavBar.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/ViewNavBar.svelte new file mode 100644 index 0000000000..bf3d073f60 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_components/ViewNavBar.svelte @@ -0,0 +1,384 @@ + + + + +{#if table && tableEditable} + + +{/if} + +{#if editableView} + + +{/if} + + diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_layout.svelte index 8c60dbdd69..da05196c04 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/_layout.svelte @@ -3,6 +3,7 @@ import { tables, builderStore } from "stores/builder" import * as routify from "@roxi/routify" import { onDestroy } from "svelte" + import ViewNavBar from "./_components/ViewNavBar.svelte" $: tableId = $tables.selectedTableId $: builderStore.selectResource(tableId) @@ -20,4 +21,17 @@ onDestroy(stopSyncing) - +
+ + +
+ + diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/index.svelte index d79a0bc0ad..5684e77e31 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/index.svelte @@ -1,7 +1,97 @@ {#if $tables?.selected?.name} @@ -40,7 +119,56 @@
{/if} - + + + + {#if isUsersTable && $appStore.features.disableUserMetadata} + + {/if} + + {#if relationshipsEnabled} + + {/if} + {#if !isUsersTable} + + + + generateButton?.show()} /> + generateButton?.show()} /> + + {/if} + + + + + + + + + + + + {#if isUsersTable} + + {:else} + + {/if} + {:else} Create your first table to start building {/if} diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/[rowId]/[field]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/[rowId]/[field]/index.svelte deleted file mode 100644 index 39dbcb9d11..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/[rowId]/[field]/index.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/[rowId]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/[rowId]/index.svelte deleted file mode 100644 index 348ed0b5bf..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/[rowId]/index.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/index.svelte deleted file mode 100644 index cecec0ab53..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/relationship/index.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/[viewName]/_layout.svelte similarity index 100% rename from packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/_layout.svelte rename to packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/[viewName]/_layout.svelte diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/[viewName]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/[viewName]/index.svelte new file mode 100644 index 0000000000..2c822569b7 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/[viewName]/index.svelte @@ -0,0 +1,91 @@ + + +
+ {#if view} +
+ + + {#if view.calculation} + + {/if} + + + +
+ {:else}Create your first table to start building{/if} +
+ + diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v1/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/index.svelte similarity index 100% rename from packages/builder/src/pages/builder/app/[application]/data/view/v1/index.svelte rename to packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/v1/index.svelte diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/index.svelte deleted file mode 100644 index 623cd224db..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/data/view/index.svelte +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/index.svelte deleted file mode 100644 index 51149b602d..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/index.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - -{#if selectedView} - -{:else}Create your first table to start building{/if} - - diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/index.svelte deleted file mode 100644 index c2281710ba..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/index.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v2/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/v2/index.svelte deleted file mode 100644 index c11ca87023..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/data/view/v2/index.svelte +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte index d5a696c6bf..d40f28e65a 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte @@ -8,6 +8,7 @@ import InfoDisplay from "./InfoDisplay.svelte" import analytics, { Events } from "analytics" import { shouldDisplaySetting } from "@budibase/frontend-core" + import { getContext, setContext } from "svelte" export let componentDefinition export let componentInstance @@ -19,6 +20,16 @@ export let includeHidden = false export let tag + // Sometimes we render component settings using a complicated nested + // component instance technique. This results in instances with IDs that + // don't exist anywhere in the tree. Therefore we need to keep track of + // what the real component tree ID is so we can always find it. + const rootId = getContext("rootId") + if (!rootId) { + setContext("rootId", componentInstance._id) + } + $: componentInstance._rootId = rootId || componentInstance._id + $: sections = getSections( componentInstance, componentDefinition, diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte index 03bf771beb..93044cdb9a 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte @@ -4,9 +4,12 @@ export let title export let body export let icon = "HelpOutline" + export let quiet = false + export let warning = false + export let error = false -
+
{#if title}
@@ -16,7 +19,7 @@ {@html body} {:else} - + {@html body} @@ -24,6 +27,23 @@
diff --git a/packages/builder/src/pages/builder/portal/users/users/_components/UpdateRolesModal.svelte b/packages/builder/src/pages/builder/portal/users/users/_components/UpdateRolesModal.svelte deleted file mode 100644 index 9ad41ad652..0000000000 --- a/packages/builder/src/pages/builder/portal/users/users/_components/UpdateRolesModal.svelte +++ /dev/null @@ -1,74 +0,0 @@ - - - - - Update {user.email}'s role for {app.name}. - - opt.label} + getOptionValue={opt => opt.value} + on:change={e => { + handleFilterChange({ + logicalOperator: e.detail, + }) + }} + placeholder={false} + /> + + of the following filter groups: +
+ {/if} + {#if editableFilters?.groups?.length} +
+ {#each editableFilters?.groups as group, groupIdx} +
+
+
+ + {getGroupPrefix(groupIdx, editableFilters.logicalOperator)} + + + { + const updated = { ...filter, field: e.detail } + onFieldChange(updated) + onFilterFieldUpdate(updated, groupIdx, filterIdx) + }} + placeholder="Column" + /> + + opt.label} + getOptionValue={opt => opt.value} + on:change={e => { + handleFilterChange({ + onEmptyFilter: e.detail, + }) + }} + placeholder={false} + /> + + when all filters are empty +
+ {/if} +
+ + + + +
+ +
+ {:else} + None of the table column can be used for filtering. + {/if} + +
+ + diff --git a/packages/frontend-core/src/components/FilterBuilder.svelte b/packages/frontend-core/src/components/FilterBuilder.svelte deleted file mode 100644 index 3a0c789b9e..0000000000 --- a/packages/frontend-core/src/components/FilterBuilder.svelte +++ /dev/null @@ -1,379 +0,0 @@ - - -
- - {#if fieldOptions?.length} - - {#if !fieldFilters?.length} - Add your first filter expression. - {:else} - - {#if behaviourFilters} -
- opt.label} - getOptionValue={opt => opt.value} - on:change={e => handleOnEmptyFilter(e.detail)} - placeholder={null} - /> - {/if} -
- {/if} - {/if} - - {#if fieldFilters?.length} -
- {#if filtersLabel} -
- -
- {/if} -
- {#each fieldFilters as filter} - onOperatorChange(filter)} - placeholder={null} - /> - {#if allowBindings} - - {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === ArrayOperator.ONE_OF)} - - {:else if filter.type === FieldType.OPTIONS} - - {:else if filter.type === FieldType.BOOLEAN} - - {:else if filter.type === FieldType.DATETIME} - - {:else if [FieldType.BB_REFERENCE, FieldType.BB_REFERENCE_SINGLE].includes(filter.type)} - - {:else} - - {/if} -
- duplicateFilter(filter.id)} - /> - removeFilter(filter.id)} - /> -
- {/each} -
-
- {/if} -
- -
- {:else} - None of the table column can be used for filtering. - {/if} -
-
- - diff --git a/packages/frontend-core/src/components/FilterField.svelte b/packages/frontend-core/src/components/FilterField.svelte new file mode 100644 index 0000000000..2f034ddf3e --- /dev/null +++ b/packages/frontend-core/src/components/FilterField.svelte @@ -0,0 +1,319 @@ + + +
+ + + + + + +
+
+ {#if filter.valueType === FilterValueType.BINDING} + + {:else} +
+ {#if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA, FieldType.AI].includes(filter.type)} + + {:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === ArrayOperator.ONE_OF)} + + {:else if filter.type === FieldType.OPTIONS} + + {:else if filter.type === FieldType.BOOLEAN} + + {:else if filter.type === FieldType.DATETIME} + + {:else if [FieldType.BB_REFERENCE, FieldType.BB_REFERENCE_SINGLE].includes(filter.type)} + + {:else} + + {/if} +
+ {/if} +
+ +
+ + {#if !disabled && allowBindings && filter.field && !filter.noValue} + + +
{ + bindingDrawer.show() + }} + > + +
+ {/if} +
+
+
+ + diff --git a/packages/frontend-core/src/components/FilterUsers.svelte b/packages/frontend-core/src/components/FilterUsers.svelte index 489426df1e..4640561afd 100644 --- a/packages/frontend-core/src/components/FilterUsers.svelte +++ b/packages/frontend-core/src/components/FilterUsers.svelte @@ -27,7 +27,8 @@
option.email} diff --git a/packages/frontend-core/src/components/grid/cells/AICell.svelte b/packages/frontend-core/src/components/grid/cells/AICell.svelte new file mode 100644 index 0000000000..38e81cefd3 --- /dev/null +++ b/packages/frontend-core/src/components/grid/cells/AICell.svelte @@ -0,0 +1,99 @@ + + + + +
+
+ {value || ""} +
+
+ +{#if isOpen} + +