diff --git a/packages/backend-core/src/constants/misc.ts b/packages/backend-core/src/constants/misc.ts index e2fd975e40..1fa8d6e926 100644 --- a/packages/backend-core/src/constants/misc.ts +++ b/packages/backend-core/src/constants/misc.ts @@ -9,6 +9,7 @@ export enum Cookie { ACCOUNT_RETURN_URL = "budibase:account:returnurl", DatasourceAuth = "budibase:datasourceauth", OIDC_CONFIG = "budibase:oidc:config", + FeatureFlags = "budibase:featureflags", } export { Header } from "@budibase/shared-core" diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index 22b937db88..9bea94de29 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -460,6 +460,17 @@ export function setFeatureFlags(key: string, value: Record) { context.featureFlagCache[key] = value } +export function getFeatureFlagOverrides(): Record { + return getCurrentContext()?.featureFlagOverrides || {} +} + +export async function doInFeatureFlagOverrideContext( + value: Record, + callback: () => Promise +) { + return await newContext({ featureFlagOverrides: value }, callback) +} + export function getTableForView(viewId: string): Table | undefined { const context = getCurrentContext() if (!context) { diff --git a/packages/backend-core/src/context/types.ts b/packages/backend-core/src/context/types.ts index c2cb966731..b34ef8c1b7 100644 --- a/packages/backend-core/src/context/types.ts +++ b/packages/backend-core/src/context/types.ts @@ -24,5 +24,6 @@ export type ContextMap = { featureFlagCache?: { [key: string]: Record } + featureFlagOverrides?: Record viewToTableCache?: Record } diff --git a/packages/backend-core/src/errors/errors.ts b/packages/backend-core/src/errors/errors.ts index ef6a3038ba..ceff6945af 100644 --- a/packages/backend-core/src/errors/errors.ts +++ b/packages/backend-core/src/errors/errors.ts @@ -1,5 +1,7 @@ // BASE +import { ErrorCode } from "@budibase/types" + export abstract class BudibaseError extends Error { code: string @@ -13,13 +15,6 @@ export abstract class BudibaseError extends Error { // ERROR HANDLING -export enum ErrorCode { - USAGE_LIMIT_EXCEEDED = "usage_limit_exceeded", - FEATURE_DISABLED = "feature_disabled", - INVALID_API_KEY = "invalid_api_key", - HTTP = "http", -} - /** * For the given error, build the public representation that is safe * to be exposed over an api. diff --git a/packages/backend-core/src/features/features.ts b/packages/backend-core/src/features/features.ts index 7106777084..a76ffe8a7b 100644 --- a/packages/backend-core/src/features/features.ts +++ b/packages/backend-core/src/features/features.ts @@ -175,6 +175,21 @@ export class FlagSet { } } + const overrides = context.getFeatureFlagOverrides() + for (const [key, value] of Object.entries(overrides)) { + if (!this.isFlagName(key)) { + continue + } + + if (typeof value !== "boolean") { + continue + } + + // @ts-expect-error - TS does not like you writing into a generic type. + flagValues[key] = value + tags[`flags.${key}.source`] = "override" + } + context.setFeatureFlags(this.setId, flagValues) for (const [key, value] of Object.entries(flagValues)) { tags[`flags.${key}.value`] = value diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index 54d808de86..86c0c687dc 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -16,11 +16,12 @@ import env from "../environment" import { Ctx, EndpointMatcher, + ErrorCode, LoginMethod, SessionCookie, User, } from "@budibase/types" -import { ErrorCode, InvalidAPIKeyError } from "../errors" +import { InvalidAPIKeyError } from "../errors" import tracer from "dd-trace" import type { Middleware, Next } from "koa" diff --git a/packages/backend-core/src/middleware/featureFlagCookie.ts b/packages/backend-core/src/middleware/featureFlagCookie.ts new file mode 100644 index 0000000000..070e2161af --- /dev/null +++ b/packages/backend-core/src/middleware/featureFlagCookie.ts @@ -0,0 +1,13 @@ +import { Ctx, FeatureFlagCookie } from "@budibase/types" +import { Middleware, Next } from "koa" +import { getCookie } from "../utils" +import { Cookie } from "../constants" +import { doInFeatureFlagOverrideContext } from "../context" + +export default (async (ctx: Ctx, next: Next) => { + const cookie = getCookie(ctx, Cookie.FeatureFlags) + const flags = cookie?.flags || {} + await doInFeatureFlagOverrideContext(flags, async () => { + await next() + }) +}) as Middleware diff --git a/packages/backend-core/src/middleware/index.ts b/packages/backend-core/src/middleware/index.ts index 9ee51db45b..0d16ae52e3 100644 --- a/packages/backend-core/src/middleware/index.ts +++ b/packages/backend-core/src/middleware/index.ts @@ -20,5 +20,6 @@ export { default as correlation } from "../logging/correlation/middleware" export { default as errorHandling } from "./errorHandling" export { default as querystringToBody } from "./querystringToBody" export { default as csp } from "./contentSecurityPolicy" +export { default as featureFlagCookie } from "./featureFlagCookie" export * as joiValidator from "./joi-validator" export { default as ip } from "./ip" diff --git a/packages/builder/src/components/commandPalette/CommandPalette.svelte b/packages/builder/src/components/commandPalette/CommandPalette.svelte index 4598b44873..427b1e960f 100644 --- a/packages/builder/src/components/commandPalette/CommandPalette.svelte +++ b/packages/builder/src/components/commandPalette/CommandPalette.svelte @@ -8,7 +8,7 @@ notifications, } from "@budibase/bbui" import { API } from "@/api" - import { goto } from "@roxi/routify" + import { goto, params, isActive } from "@roxi/routify" import { automationStore, previewStore, @@ -19,54 +19,36 @@ queries, tables, views, + viewsV2, } from "@/stores/builder" - import { themeStore } from "@/stores/portal" + import { themeStore, featureFlags } from "@/stores/portal" import { getContext } from "svelte" import { ThemeOptions } from "@budibase/shared-core" + import { FeatureFlag } from "@budibase/types" const modalContext = getContext(Context.Modal) - const commands = [ + + let search + let selected = null + + $: inApp = $isActive("/builder/app/:application") + $: commands = [ { type: "Access", name: "Invite users and manage app access", description: "", icon: "User", action: () => builderStore.showBuilderSidePanel(), + requiresApp: true, }, - { - type: "Navigate", - name: "Portal", - description: "", - icon: "Compass", - action: () => $goto("../../portal"), - }, - { - type: "Navigate", - name: "Data", - description: "", - icon: "Compass", - action: () => $goto("./data"), - }, - { - type: "Navigate", - name: "Design", - description: "", - icon: "Compass", - action: () => $goto("./design"), - }, - { - type: "Navigate", - name: "Automations", - description: "", - icon: "Compass", - action: () => $goto("./automation"), - }, + ...navigationCommands(), { type: "Publish", name: "App", description: "Deploy your application", icon: "Box", action: deployApp, + requiresApp: true, }, { type: "Preview", @@ -74,12 +56,14 @@ description: "", icon: "Play", action: () => previewStore.showPreview(true), + requiresApp: true, }, { type: "Preview", name: "Published App", icon: "Play", action: () => window.open(`/app${$appStore.url}`), + requiresApp: true, }, { type: "Support", @@ -87,6 +71,7 @@ icon: "Help", action: () => window.open(`https://github.com/Budibase/budibase/discussions/new`), + requiresApp: true, }, { type: "Support", @@ -96,52 +81,166 @@ window.open( `https://github.com/Budibase/budibase/issues/new?assignees=&labels=bug&template=bug_report.md&title=` ), + requiresApp: true, }, - ...($datasources?.list?.map(datasource => ({ + ...datasourceCommands($datasources?.list || []), + ...tableCommands($tables?.list || []), + ...viewCommands($views?.list || []), + ...viewV2Commands($viewsV2?.list || []), + ...queryCommands($queries?.list || []), + ...screenCommands($sortedScreens), + ...automationCommands($automationStore?.automations || []), + ...themeCommands(), + ...featureFlagCommands($featureFlags), + ] + $: enrichedCommands = commands.map(cmd => ({ + ...cmd, + searchValue: `${cmd.type} ${cmd.name}`.toLowerCase().replace(/_/g, " "), + })) + $: results = filterResults(enrichedCommands, search, inApp) + $: categories = groupResults(results) + + const navigationCommands = () => { + const routes = [ + { + name: "Portal", + url: "/builder/portal", + }, + { + name: "Data", + url: "/builder/app/:application/data", + }, + { + name: "Design", + url: "/builder/app/:application/design", + }, + { + name: "Automations", + url: "/builder/app/:application/automation", + }, + { + name: "Settings", + url: "/builder/app/:application/settings", + }, + ] + return routes.map(route => ({ + type: "Navigate", + name: route.name, + icon: "Compass", + action: () => { + const gotoParams = route.url.includes(":application") + ? { application: $params.application } + : {} + $goto(route.url, gotoParams) + }, + requiresApp: true, + })) + } + + const datasourceCommands = datasources => { + return datasources.map(datasource => ({ type: "Datasource", - name: `${datasource.name}`, + name: datasource.name, icon: "Data", - action: () => $goto(`./data/datasource/${datasource._id}`), - })) ?? []), - ...($tables?.list?.map(table => ({ + action: () => + $goto(`/builder/app/:application/data/datasource/:id`, { + application: $params.application, + id: datasource._id, + }), + requiresApp: true, + })) + } + + const tableCommands = tables => { + return tables.map(table => ({ type: "Table", name: table.name, icon: "Table", - action: () => $goto(`./data/table/${table._id}`), - })) ?? []), - ...($views?.list?.map(view => ({ + action: () => + $goto(`/builder/app/:application/data/table/:id`, { + application: $params.application, + id: table._id, + }), + requiresApp: true, + })) + } + + const viewCommands = views => { + return views.map(view => ({ type: "View", name: view.name, icon: "Remove", action: () => { - if (view.version === 2) { - $goto(`./data/view/v2/${view.id}`) - } else { - $goto(`./data/view/${view.name}`) - } + $goto(`/builder/app/:application/data/view/:name`, { + application: $params.application, + name: view.name, + }) }, - })) ?? []), - ...($queries?.list?.map(query => ({ + requiresApp: true, + })) + } + + const viewV2Commands = views => { + return views.map(view => ({ + type: "View", + name: view.name, + icon: "Remove", + action: () => { + $goto(`/builder/app/:application/data/table/:tableId/:viewId`, { + application: $params.application, + x: view.tableId, + viewId: view.id, + }) + }, + requiresApp: true, + })) + } + + const queryCommands = queries => { + return queries.map(query => ({ type: "Query", name: query.name, icon: "SQLQuery", - action: () => $goto(`./data/query/${query._id}`), - })) ?? []), - ...$sortedScreens.map(screen => ({ + action: () => + $goto(`/builder/app/:application/data/query/:id`, { + application: $params.application, + id: query._id, + }), + requiresApp: true, + })) + } + + const screenCommands = screens => { + return screens.map(screen => ({ type: "Screen", name: screen.routing.route, icon: "WebPage", - action: () => { - $goto(`./design/${screen._id}/${screen._id}-screen`) - }, - })), - ...($automationStore?.automations?.map(automation => ({ + action: () => + $goto(`/builder/app/:application/design/:screenId/:componentId`, { + application: $params.application, + screenId: screen._id, + componentId: `${screen._id}-screen`, + }), + requiresApp: true, + })) + } + + const automationCommands = automations => { + return automations.map(automation => ({ type: "Automation", name: automation.name, icon: "ShareAndroid", - action: () => $goto(`./automation/${automation._id}`), - })) ?? []), - ...ThemeOptions.map(themeMeta => ({ + action: () => + $goto(`/builder/app/:application/automation/:id`, { + application: $params.application, + id: automation._id, + }), + requiresApp: true, + })) + } + + const themeCommands = () => { + return ThemeOptions.map(themeMeta => ({ type: "Change Builder Theme", name: themeMeta.name, icon: "ColorPalette", @@ -150,28 +249,41 @@ state.theme = themeMeta.id return state }), - })), - ] + })) + } - let search - let selected = null - - $: enrichedCommands = commands.map(cmd => ({ - ...cmd, - searchValue: `${cmd.type} ${cmd.name}`.toLowerCase(), - })) - $: results = filterResults(enrichedCommands, search) - $: categories = groupResults(results) - - const filterResults = (commands, search) => { - if (!search) { - selected = null - return commands + const featureFlagCommands = flags => { + if (!flags.DEBUG_UI) { + return [] + } + return Object.entries(flags) + .filter(([flag]) => flag !== FeatureFlag.DEBUG_UI) + .map(([flag, value]) => ({ + type: "Feature Flag", + name: `${value ? "Disable" : "Enable"} ${flag}`, + icon: "Flag", + action: () => { + featureFlags.setFlag(flag, !value) + }, + })) + } + + const filterResults = (commands, search, inApp) => { + if (search) { + selected = 0 + search = search.toLowerCase().replace(/_/g, " ") + } else { + selected = null } - selected = 0 - search = search.toLowerCase() return commands - .filter(cmd => cmd.searchValue.includes(search)) + .filter(cmd => { + // Handle searching + if (search && !cmd.searchValue.includes(search)) { + return false + } + // Handle commands that require an app + return inApp || !cmd.requiresApp + }) .map((cmd, idx) => ({ ...cmd, idx, @@ -264,7 +376,8 @@ {command.type}: 
- {command.name} + + {@html command.name}
{/each} @@ -339,4 +452,10 @@ text-overflow: ellipsis; white-space: nowrap; } + .name :global(code) { + font-size: 12px; + background: var(--background-alt); + padding: 4px; + border-radius: 4px; + } diff --git a/packages/builder/src/components/common/CodeEditor/AIGen.svelte b/packages/builder/src/components/common/CodeEditor/AIGen.svelte index c7e3a0f1ce..189ec3594d 100644 --- a/packages/builder/src/components/common/CodeEditor/AIGen.svelte +++ b/packages/builder/src/components/common/CodeEditor/AIGen.svelte @@ -3,7 +3,7 @@ import { createEventDispatcher } from "svelte" import { API } from "@/api" - import type { EnrichedBinding } from "@budibase/types" + import { ErrorCode, type EnrichedBinding } from "@budibase/types" import analytics, { Events } from "@/analytics" import AiInput from "../ai/AIInput.svelte" @@ -43,17 +43,26 @@ const resp = await API.generateJs({ prompt, bindings }) const code = resp.code if (code === "") { - throw new Error("We didn't understand your prompt. Please rephrase it.") + throw new Error( + "We didn't understand your prompt. This can happen if the prompt isn't specific, or if it's a request for something other than code. Try expressing your request in a different way." + ) } suggestedCode = code dispatch("update", { code }) } catch (e) { console.error(e) - notifications.error( - e instanceof Error - ? `Unable to generate code: ${e.message}` - : "Unable to generate code. Please try again later." - ) + if (!(e instanceof Error)) { + notifications.error("Unable to generate code. Please try again later.") + return + } + + if ("code" in e && e.code === ErrorCode.USAGE_LIMIT_EXCEEDED) { + notifications.error( + "Monthly usage limit reached. We're exploring options to expand this soon. Questions? Contact support@budibase.com" + ) + } else { + notifications.error(`Unable to generate code: ${e.message}`) + } } } @@ -78,6 +87,8 @@ function reset() { suggestedCode = null previousContents = null + promptText = "" + expanded = false } function calculateExpandedWidth() { @@ -103,7 +114,6 @@ placeholder="Generate with AI" onSubmit={generateJs} bind:expanded - on:collapse={rejectSuggestion} readonly={!!suggestedCode} {expandedOnly} /> @@ -121,21 +131,11 @@ overflow: visible; } - @keyframes border-fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } - } - .floating-actions { position: absolute; display: flex; gap: var(--spacing-s); bottom: calc(100% + 5px); - left: 5px; z-index: 2; animation: fade-in 0.2s ease-out forwards; } diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index c159804e47..bcf377c069 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -9,7 +9,6 @@ import { Label } from "@budibase/bbui" import { onMount, createEventDispatcher, onDestroy } from "svelte" import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates" - import { autocompletion, closeBrackets, @@ -52,17 +51,12 @@ import type { Extension } from "@codemirror/state" import { javascript } from "@codemirror/lang-javascript" import { EditorModes } from "./" - import { themeStore } from "@/stores/portal" - import { - type EnrichedBinding, - FeatureFlag, - type EditorMode, - } from "@budibase/types" + import { featureFlags, themeStore } from "@/stores/portal" + import { type EnrichedBinding, type EditorMode } from "@budibase/types" import { tooltips } from "@codemirror/view" import type { BindingCompletion, CodeValidator } from "@/types" import { validateHbsTemplate } from "./validator/hbs" import { validateJsTemplate } from "./validator/js" - import { featureFlag } from "@/helpers" import AIGen from "./AIGen.svelte" export let label: string | undefined = undefined @@ -88,6 +82,7 @@ let isEditorInitialised = false let queuedRefresh = false let editorWidth: number | null = null + let isAIGeneratedContent = false // Theming! let currentTheme = $themeStore?.theme @@ -101,9 +96,7 @@ } $: aiGenEnabled = - featureFlag.isEnabled(FeatureFlag.AI_JS_GENERATION) && - mode.name === "javascript" && - !readonly + $featureFlags.AI_JS_GENERATION && mode.name === "javascript" && !readonly $: { if (autofocus && isEditorInitialised) { @@ -429,6 +422,7 @@ editor.dispatch({ changes: { from: 0, to: editor.state.doc.length, insert: code }, }) + isAIGeneratedContent = true } onMount(() => { @@ -462,7 +456,12 @@ {/if} -
+
@@ -475,6 +474,7 @@ on:accept={() => { dispatch("change", editor.state.doc.toString()) dispatch("blur", editor.state.doc.toString()) + isAIGeneratedContent = false }} on:reject={event => { const { code } = event.detail @@ -482,6 +482,7 @@ editor.dispatch({ changes: { from: 0, to: editor.state.doc.length, insert: code || "" }, }) + isAIGeneratedContent = false }} /> {/if} @@ -691,4 +692,19 @@ text-overflow: ellipsis !important; white-space: nowrap !important; } + + .code-editor.ai-generated :global(.cm-editor) { + background: var(--spectrum-global-color-blue-50) !important; + } + + .code-editor.ai-generated :global(.cm-content) { + background: transparent !important; + } + + .code-editor.ai-generated :global(.cm-line) { + background: #765ffe1a !important; + display: inline-block; + min-width: fit-content; + padding-right: 2px !important; + } diff --git a/packages/builder/src/components/common/ai/AIInput.svelte b/packages/builder/src/components/common/ai/AIInput.svelte index 8e64fea7f3..9f75e5d2ef 100644 --- a/packages/builder/src/components/common/ai/AIInput.svelte +++ b/packages/builder/src/components/common/ai/AIInput.svelte @@ -14,7 +14,6 @@ export const submit = onPromptSubmit $: expanded = expandedOnly || expanded - const dispatch = createEventDispatcher() let promptInput: HTMLInputElement @@ -28,7 +27,7 @@ $: aiEnabled = $auth?.user?.llm $: creditsExceeded = $licensing.aiCreditsExceeded - $: disabled = !aiEnabled || creditsExceeded || readonly || promptLoading + $: disabled = !aiEnabled || creditsExceeded || readonly $: animateBorder = !disabled && expanded $: canSubmit = !readonly && !!value @@ -91,6 +90,7 @@ src={BBAI} alt="AI" class="ai-icon" + class:loading={promptLoading} class:disabled={expanded && disabled} on:click={e => { e.stopPropagation() @@ -105,7 +105,7 @@ class="prompt-input" {placeholder} on:keydown={handleKeyPress} - {disabled} + disabled={disabled || promptLoading} /> {:else} @@ -153,11 +153,11 @@ {:else} .spectrum-ActionButton { - --offset: 1px; position: relative; display: flex; align-items: center; justify-content: space-between; box-sizing: border-box; padding: var(--spacing-s); - border: 1px solid var(--spectrum-alias-border-color); border-radius: 30px; transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); width: 100%; @@ -185,16 +183,30 @@ overflow: hidden; cursor: pointer; background-color: var(--spectrum-global-color-gray-75); + border: none; } .spectrum-ActionButton::before { content: ""; position: absolute; - top: -1px; - left: -1px; - width: calc(100% + 2px); - height: calc(100% + 2px); - border-radius: inherit; + inset: 0; + border-radius: 30px; + padding: 1px; + background: var(--spectrum-alias-border-color); + -webkit-mask: linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; + } + + .animate-border::after { + content: ""; + position: absolute; + inset: 0; + border-radius: 30px; + padding: 1px; background: linear-gradient( 125deg, transparent -10%, @@ -204,40 +216,42 @@ transparent 35%, transparent 110% ); + -webkit-mask: linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + mask-composite: exclude; pointer-events: none; - z-index: 0; + animation: border-flow 1.5s cubic-bezier(0.17, 0.67, 0.83, 0.67) forwards; } - .spectrum-ActionButton:not(.animate-border)::before { + @keyframes border-flow { + 0% { + clip-path: polygon(0% 0%, 10% 0%, 8% 100%, 0% 100%); + } + 30% { + clip-path: polygon(0% 0%, 35% 0%, 26% 100%, 0% 100%); + } + 50% { + clip-path: polygon(0% 0%, 55% 0%, 41% 100%, 0% 100%); + } + 70% { + clip-path: polygon(0% 0%, 70% 0%, 53% 100%, 0% 100%); + } + 85% { + clip-path: polygon(0% 0%, 80% 0%, 60% 100%, 0% 100%); + } + 95% { + clip-path: polygon(0% 0%, 86% 0%, 65% 100%, 0% 100%); + } + 100% { + clip-path: polygon(0% 0%, 90% 0%, 68% 100%, 0% 100%); + } + } + + .spectrum-ActionButton:not(.animate-border)::after { content: none; } - .animate-border::before { - animation: border-fade-in 1s cubic-bezier(0.17, 0.67, 0.83, 0.67); - animation-fill-mode: forwards; - } - - @keyframes border-fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } - } - - .spectrum-ActionButton::after { - content: ""; - background: inherit; - position: absolute; - top: 50%; - left: 50%; - inset: var(--offset); - height: calc(100% - 2 * var(--offset)); - width: calc(100% - 2 * var(--offset)); - border-radius: inherit; - } - @keyframes fade-in { from { opacity: 0; @@ -268,10 +282,12 @@ .ai-icon { width: 18px; height: 18px; - margin-left: 4px; - margin-right: 8px; + margin-left: var(--spacing-xs); + margin-right: var(--spacing-s); flex-shrink: 0; cursor: var(--ai-icon-cursor, pointer); + position: relative; + z-index: 2; } .ai-gen-text { @@ -280,10 +296,12 @@ text-overflow: ellipsis; transition: opacity 0.2s ease-out; margin-right: var(--spacing-xs); + position: relative; + z-index: 2; } .prompt-input { - font-size: 14px; + font-size: inherit; flex: 1; border: none; background: transparent; @@ -293,6 +311,8 @@ min-width: 0; resize: none; overflow: hidden; + position: relative; + z-index: 2; } .prompt-input::placeholder { @@ -303,14 +323,15 @@ .action-buttons { display: flex; gap: var(--spacing-s); - z-index: 4; + z-index: 5; flex-shrink: 0; margin-right: var(--spacing-s); + position: relative; } .button-content-wrapper { position: relative; - z-index: 1; + z-index: 2; display: flex; align-items: center; overflow: hidden; @@ -328,4 +349,17 @@ filter: grayscale(1) brightness(1.5); opacity: 0.5; } + + .ai-icon.loading { + animation: spin 1s ease-in-out infinite; + } + + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } diff --git a/packages/builder/src/pages/builder/_layout.svelte b/packages/builder/src/pages/builder/_layout.svelte index 7e765d7366..7bb97be71d 100644 --- a/packages/builder/src/pages/builder/_layout.svelte +++ b/packages/builder/src/pages/builder/_layout.svelte @@ -6,8 +6,11 @@ import { API } from "@/api" import Branding from "./Branding.svelte" import ContextMenu from "@/components/ContextMenu.svelte" + import CommandPalette from "@/components/commandPalette/CommandPalette.svelte" + import { Modal } from "@budibase/bbui" let loaded = false + let commandPaletteModal $: multiTenancyEnabled = $admin.multiTenancy $: hasAdminUser = $admin?.checklist?.adminUser?.checked @@ -157,12 +160,25 @@ } } } + + // Event handler for the command palette + const handleKeyDown = e => { + if (e.key === "k" && (e.ctrlKey || e.metaKey)) { + e.preventDefault() + commandPaletteModal.toggle() + } + } + + + + + {#if loaded} {/if} diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index a799553aca..668aca657e 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -14,7 +14,6 @@ Tabs, Tab, Heading, - Modal, notifications, TooltipPosition, } from "@budibase/bbui" @@ -24,7 +23,6 @@ import { capitalise } from "@/helpers" import { onMount, onDestroy } from "svelte" import VerificationPromptBanner from "@/components/common/VerificationPromptBanner.svelte" - import CommandPalette from "@/components/commandPalette/CommandPalette.svelte" import TourWrap from "@/components/portal/onboarding/TourWrap.svelte" import TourPopover from "@/components/portal/onboarding/TourPopover.svelte" import BuilderSidePanel from "./_components/BuilderSidePanel.svelte" @@ -38,7 +36,6 @@ let promise = getPackage() let hasSynced = false - let commandPaletteModal let loaded = false $: loaded && initTour() @@ -79,14 +76,6 @@ $goto($builderStore.previousTopNavPath[path] || path) } - // Event handler for the command palette - const handleKeyDown = e => { - if (e.key === "k" && (e.ctrlKey || e.metaKey)) { - e.preventDefault() - commandPaletteModal.toggle() - } - } - const initTour = async () => { // Check if onboarding is enabled. if (!$auth.user?.onboardedAt) { @@ -184,11 +173,6 @@ {/if} - - - - -