Command palette (#9942)
* command palette E2E * tidy up * Improve theming with spectrum badges and dedupe spectrum label usage * Update data section nav to match designs and use panel component * Fix main content layout in data section * Update data section routing for tables * Improve data section routing for tables to account for edge cases * Update internal and sample datasource routing * Update external datasource routing * Update routing for queries and make a top level concept like everything else * Update routing for views * Fix undefined reference when deleting datasource * Reduce network calls and fix issues with stale datasourcenavigator state * Update routing for REST queries and unify routes for normal queries and REST queries * Lint * Fix links for queries from datasource details page * Remove redundant API calls and improve table deletion logic * Improve data entity deletion logic and redirection and fix query details keying * Improve determination of selected item in datasource tree * Update command palette to support new data routes * Update command palette, fix keybind issues and updating loading state * Lint * Fix publish command and fix preview published app URL * Fix BBUI import * Lint * Fix datasource navigator selected state not working for internal DB or sample data * Update command palette to use ctr+k/cmd+k * Update command palette to match new designs and add visible categories * Restore missing styles£ * Use proper theme constants for changing theme in command palette * Add command palette action for inviting users --------- Co-authored-by: Martin McKeaveney <martinmckeaveney@gmail.com>
This commit is contained in:
parent
93e9eaec8a
commit
e5271bdef1
4 changed files with 360 additions and 8 deletions
|
@ -29,6 +29,14 @@
|
||||||
visible = false
|
visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toggle() {
|
||||||
|
if (visible) {
|
||||||
|
hide()
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function cancel() {
|
export function cancel() {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
return
|
return
|
||||||
|
@ -61,7 +69,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext(Context.Modal, { show, hide, cancel })
|
setContext(Context.Modal, { show, hide, toggle, cancel })
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
document.addEventListener("keydown", handleKey)
|
document.addEventListener("keydown", handleKey)
|
||||||
|
|
|
@ -0,0 +1,333 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Context,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
ModalContent,
|
||||||
|
Detail,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { API } from "api"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import {
|
||||||
|
store,
|
||||||
|
sortedScreens,
|
||||||
|
automationStore,
|
||||||
|
themeStore,
|
||||||
|
} from "builderStore"
|
||||||
|
import { datasources, queries, tables, views } from "stores/backend"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
const modalContext = getContext(Context.Modal)
|
||||||
|
const commands = [
|
||||||
|
{
|
||||||
|
type: "Access",
|
||||||
|
name: "Invite users and manage app access",
|
||||||
|
description: "",
|
||||||
|
icon: "User",
|
||||||
|
action: () =>
|
||||||
|
store.update(state => ({ ...state, builderSidePanel: 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("./automate"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "Publish",
|
||||||
|
name: "App",
|
||||||
|
description: "Deploy your application",
|
||||||
|
icon: "Box",
|
||||||
|
action: deployApp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "Preview",
|
||||||
|
name: "App",
|
||||||
|
description: "",
|
||||||
|
icon: "Play",
|
||||||
|
action: () => window.open(`/${$store.appId}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "Preview",
|
||||||
|
name: "Published App",
|
||||||
|
icon: "Play",
|
||||||
|
action: () => window.open(`/app${$store.url}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "Support",
|
||||||
|
name: "Raise Github Discussion",
|
||||||
|
icon: "Help",
|
||||||
|
action: () =>
|
||||||
|
window.open(`https://github.com/Budibase/budibase/discussions/new`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "Support",
|
||||||
|
name: "Raise A Bug",
|
||||||
|
icon: "Bug",
|
||||||
|
action: () =>
|
||||||
|
window.open(
|
||||||
|
`https://github.com/Budibase/budibase/issues/new?assignees=&labels=bug&template=bug_report.md&title=`
|
||||||
|
),
|
||||||
|
},
|
||||||
|
...$datasources?.list.map(datasource => ({
|
||||||
|
type: "Datasource",
|
||||||
|
name: `${datasource.name}`,
|
||||||
|
icon: "Data",
|
||||||
|
action: () => $goto(`./data/datasource/${datasource._id}`),
|
||||||
|
})),
|
||||||
|
...$tables?.list.map(table => ({
|
||||||
|
type: "Table",
|
||||||
|
name: table.name,
|
||||||
|
icon: "Table",
|
||||||
|
action: () => $goto(`./data/table/${table._id}`),
|
||||||
|
})),
|
||||||
|
...$views?.list.map(view => ({
|
||||||
|
type: "View",
|
||||||
|
name: view.name,
|
||||||
|
icon: "Remove",
|
||||||
|
action: () => $goto(`./data/view/${view.name}`),
|
||||||
|
})),
|
||||||
|
...$queries?.list.map(query => ({
|
||||||
|
type: "Query",
|
||||||
|
name: query.name,
|
||||||
|
icon: "SQLQuery",
|
||||||
|
action: () => $goto(`./data/query/${query._id}`),
|
||||||
|
})),
|
||||||
|
...$sortedScreens.map(screen => ({
|
||||||
|
type: "Screen",
|
||||||
|
name: screen.routing.route,
|
||||||
|
icon: "WebPage",
|
||||||
|
action: () => $goto(`./design/${screen._id}/components`),
|
||||||
|
})),
|
||||||
|
...$automationStore?.automations.map(automation => ({
|
||||||
|
type: "Automation",
|
||||||
|
name: automation.name,
|
||||||
|
icon: "ShareAndroid",
|
||||||
|
action: () => $goto(`./automate/${automation._id}`),
|
||||||
|
})),
|
||||||
|
...Constants.Themes.map(theme => ({
|
||||||
|
type: "Change Builder Theme",
|
||||||
|
name: theme.name,
|
||||||
|
icon: "ColorPalette",
|
||||||
|
action: () =>
|
||||||
|
themeStore.update(state => {
|
||||||
|
state.theme = theme.class
|
||||||
|
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
|
||||||
|
}
|
||||||
|
selected = 0
|
||||||
|
search = search.toLowerCase()
|
||||||
|
return commands
|
||||||
|
.filter(cmd => cmd.searchValue.includes(search))
|
||||||
|
.map((cmd, idx) => ({
|
||||||
|
...cmd,
|
||||||
|
idx,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupResults = results => {
|
||||||
|
let categories = {}
|
||||||
|
results?.forEach(result => {
|
||||||
|
if (!categories[result.type]) {
|
||||||
|
categories[result.type] = []
|
||||||
|
}
|
||||||
|
categories[result.type].push(result)
|
||||||
|
})
|
||||||
|
return Object.entries(categories)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = e => {
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault()
|
||||||
|
if (selected === null) {
|
||||||
|
selected = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (selected < results.length - 1) {
|
||||||
|
selected += 1
|
||||||
|
}
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault()
|
||||||
|
if (selected === null) {
|
||||||
|
selected = results.length - 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (selected > 0) {
|
||||||
|
selected -= 1
|
||||||
|
}
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
if (selected == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runAction(results[selected])
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
modalContext.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deployApp() {
|
||||||
|
try {
|
||||||
|
await API.deployAppChanges()
|
||||||
|
notifications.success("Application published successfully")
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error publishing app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runAction = command => {
|
||||||
|
if (!command) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
command.action()
|
||||||
|
modalContext.hide()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={onKeyDown} />
|
||||||
|
<ModalContent
|
||||||
|
size="L"
|
||||||
|
showCancelButton={false}
|
||||||
|
showConfirmButton={false}
|
||||||
|
showCloseIcon={false}
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
<div class="title">
|
||||||
|
<Icon size="XL" name="Search" />
|
||||||
|
<Input bind:value={search} quiet placeholder="Search for command" />
|
||||||
|
</div>
|
||||||
|
<div class="commands">
|
||||||
|
{#each categories as [name, results], catIdx}
|
||||||
|
<div class="category">
|
||||||
|
<Detail>{name}</Detail>
|
||||||
|
<div class="options">
|
||||||
|
{#each results as command, cmdIdx}
|
||||||
|
<div
|
||||||
|
class="command"
|
||||||
|
on:click={() => runAction(command)}
|
||||||
|
class:selected={command.idx === selected}
|
||||||
|
>
|
||||||
|
<Icon size="M" name={command.icon} />
|
||||||
|
<strong>{command.type}: </strong>
|
||||||
|
<div class="name">
|
||||||
|
{command.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content {
|
||||||
|
margin: -40px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-l)
|
||||||
|
var(--spacing-xl);
|
||||||
|
border-bottom: var(--border-dark);
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
}
|
||||||
|
.title :global(.spectrum-Textfield-input) {
|
||||||
|
border-bottom: none;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commands {
|
||||||
|
height: 378px;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
padding: var(--spacing-m) var(--spacing-xl);
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
}
|
||||||
|
.category:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.category :global(.spectrum-Detail) {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
.options {
|
||||||
|
padding-top: var(--spacing-m);
|
||||||
|
margin: 0 calc(-1 * var(--spacing-xl));
|
||||||
|
}
|
||||||
|
|
||||||
|
.command {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-s) var(--spacing-xl);
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: color 130ms ease-out, background-color 130ms ease-out;
|
||||||
|
}
|
||||||
|
.command:hover,
|
||||||
|
.selected {
|
||||||
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
background-color: var(--spectrum-global-color-gray-300);
|
||||||
|
}
|
||||||
|
.command strong {
|
||||||
|
margin-left: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -10,6 +10,7 @@
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
Heading,
|
Heading,
|
||||||
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
import CommandPalette from "components/commandPalette/CommandPalette.svelte"
|
||||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||||
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
|
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
|
||||||
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
||||||
|
@ -25,12 +27,9 @@
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
|
||||||
// Get Package and set store
|
|
||||||
let promise = getPackage()
|
let promise = getPackage()
|
||||||
// let betaAccess = false
|
|
||||||
|
|
||||||
// Sync once when you load the app
|
|
||||||
let hasSynced = false
|
let hasSynced = false
|
||||||
|
let commandPaletteModal
|
||||||
|
|
||||||
$: selected = capitalise(
|
$: selected = capitalise(
|
||||||
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
|
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
|
||||||
|
@ -50,7 +49,6 @@
|
||||||
$redirect("../../")
|
$redirect("../../")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles navigation between frontend, backend, automation.
|
// Handles navigation between frontend, backend, automation.
|
||||||
// This remembers your last place on each of the sections
|
// This remembers your last place on each of the sections
|
||||||
// e.g. if one of your screens is selected on front end, then
|
// e.g. if one of your screens is selected on front end, then
|
||||||
|
@ -67,6 +65,14 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Event handler for the command palette
|
||||||
|
const handleKeyDown = e => {
|
||||||
|
if (e.key === "k" && (e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault()
|
||||||
|
commandPaletteModal.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const initTour = async () => {
|
const initTour = async () => {
|
||||||
// Check if onboarding is enabled.
|
// Check if onboarding is enabled.
|
||||||
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
||||||
|
@ -201,6 +207,11 @@
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeyDown} />
|
||||||
|
<Modal bind:this={commandPaletteModal}>
|
||||||
|
<CommandPalette />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.loading {
|
.loading {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
|
|
@ -34,8 +34,8 @@
|
||||||
{#if duplicates?.length}
|
{#if duplicates?.length}
|
||||||
<div class="alert-wrap">
|
<div class="alert-wrap">
|
||||||
<Banner type="warning" showCloseButton={false}>
|
<Banner type="warning" showCloseButton={false}>
|
||||||
{`Schema Invalid - There are duplicate auto column types defined in this schema.
|
{`Schema Invalid - There are duplicate auto column types defined in this schema.
|
||||||
Please delete the duplicate entries where appropriate: -
|
Please delete the duplicate entries where appropriate: -
|
||||||
${invalidColumnText.join(", ")}`}
|
${invalidColumnText.join(", ")}`}
|
||||||
</Banner>
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue