Merge branch 'master' of github.com:Budibase/budibase into conditional-table-colours

This commit is contained in:
Andrew Kingston 2024-07-22 11:02:02 +01:00
commit a14a737921
No known key found for this signature in database
45 changed files with 1315 additions and 962 deletions

View File

@ -29,6 +29,7 @@
>
<div class="icon" class:newStyles>
<svg
on:contextmenu
on:click
class:hoverable
class:disabled

View File

@ -0,0 +1,62 @@
<script>
import { contextMenuStore } from "stores/builder"
import { Popover, Menu, MenuItem } from "@budibase/bbui"
let dropdown
let anchor
const handleKeyDown = () => {
if ($contextMenuStore.visible) {
contextMenuStore.close()
}
}
const handleItemClick = async itemCallback => {
await itemCallback()
contextMenuStore.close()
}
</script>
<svelte:window on:keydown={handleKeyDown} />
{#key $contextMenuStore.position}
<div
bind:this={anchor}
class="anchor"
style:top={`${$contextMenuStore.position.y}px`}
style:left={`${$contextMenuStore.position.x}px`}
/>
{/key}
<Popover
open={$contextMenuStore.visible}
animate={false}
bind:this={dropdown}
{anchor}
resizable={false}
align="left"
on:close={contextMenuStore.close}
>
<Menu>
{#each $contextMenuStore.items as item}
{#if item.visible}
<MenuItem
icon={item.icon}
keyBind={item.keyBind}
on:click={() => handleItemClick(item.callback)}
disabled={item.disabled}
>
{item.name}
</MenuItem>
{/if}
{/each}
</Menu>
</Popover>
<style>
.anchor {
z-index: 100;
position: absolute;
width: 0;
height: 0;
}
</style>

View File

@ -1,48 +0,0 @@
<script>
import { onMount } from "svelte"
import {
automationStore,
selectedAutomation,
userSelectedResourceMap,
} from "stores/builder"
import NavItem from "components/common/NavItem.svelte"
import EditAutomationPopover from "./EditAutomationPopover.svelte"
import { notifications } from "@budibase/bbui"
$: selectedAutomationId = $selectedAutomation?._id
onMount(async () => {
try {
await automationStore.actions.fetch()
} catch (error) {
notifications.error("Error getting automations list")
}
})
function selectAutomation(id) {
automationStore.actions.select(id)
}
</script>
<div class="automations-list">
{#each $automationStore.automations.sort(aut => aut.name) as automation}
<NavItem
icon="ShareAndroid"
text={automation.name}
selected={automation._id === selectedAutomationId}
on:click={() => selectAutomation(automation._id)}
selectedBy={$userSelectedResourceMap[automation._id]}
>
<EditAutomationPopover {automation} />
</NavItem>
{/each}
</div>
<style>
.automations-list {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
</style>

View File

@ -0,0 +1,123 @@
<script>
import {
selectedAutomation,
userSelectedResourceMap,
automationStore,
contextMenuStore,
} from "stores/builder"
import { notifications, Icon } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import UpdateAutomationModal from "components/automation/AutomationPanel/UpdateAutomationModal.svelte"
import NavItem from "components/common/NavItem.svelte"
export let automation
export let icon
let confirmDeleteDialog
let updateAutomationDialog
async function deleteAutomation() {
try {
await automationStore.actions.delete(automation)
notifications.success("Automation deleted successfully")
} catch (error) {
notifications.error("Error deleting automation")
}
}
async function duplicateAutomation() {
try {
await automationStore.actions.duplicate(automation)
notifications.success("Automation has been duplicated successfully")
} catch (error) {
notifications.error("Error duplicating automation")
}
}
const getContextMenuItems = () => {
return [
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: confirmDeleteDialog.show,
},
{
icon: "Edit",
name: "Edit",
keyBind: null,
visible: true,
disabled: false,
callback: updateAutomationDialog.show,
},
{
icon: "Duplicate",
name: "Duplicate",
keyBind: null,
visible: true,
disabled: automation.definition.trigger.name === "Webhook",
callback: duplicateAutomation,
},
{
icon: automation.disabled ? "CheckmarkCircle" : "Cancel",
name: automation.disabled ? "Activate" : "Pause",
keyBind: null,
visible: true,
disabled: false,
callback: () => {
automationStore.actions.toggleDisabled(
automation._id,
automation.disabled
)
},
},
]
}
const openContextMenu = e => {
e.preventDefault()
e.stopPropagation()
const items = getContextMenuItems()
contextMenuStore.open(automation._id, items, { x: e.clientX, y: e.clientY })
}
</script>
<NavItem
on:contextmenu={openContextMenu}
{icon}
iconColor={"var(--spectrum-global-color-gray-900)"}
text={automation.name}
selected={automation._id === $selectedAutomation?._id}
hovering={automation._id === $contextMenuStore.id}
on:click={() => automationStore.actions.select(automation._id)}
selectedBy={$userSelectedResourceMap[automation._id]}
disabled={automation.disabled}
>
<div class="icon">
<Icon on:click={openContextMenu} size="S" hoverable name="MoreSmallList" />
</div>
</NavItem>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Automation"
onOk={deleteAutomation}
title="Confirm Deletion"
>
Are you sure you wish to delete the automation
<i>{automation.name}?</i>
This action cannot be undone.
</ConfirmDialog>
<UpdateAutomationModal {automation} bind:this={updateAutomationDialog} />
<style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
</style>

View File

@ -3,20 +3,13 @@
import { Modal, notifications, Layout } from "@budibase/bbui"
import NavHeader from "components/common/NavHeader.svelte"
import { onMount } from "svelte"
import {
automationStore,
selectedAutomation,
userSelectedResourceMap,
} from "stores/builder"
import NavItem from "components/common/NavItem.svelte"
import EditAutomationPopover from "./EditAutomationPopover.svelte"
import { automationStore } from "stores/builder"
import AutomationNavItem from "./AutomationNavItem.svelte"
export let modal
export let webhookModal
let searchString
$: selectedAutomationId = $selectedAutomation?._id
$: filteredAutomations = $automationStore.automations
.filter(automation => {
return (
@ -49,10 +42,6 @@
notifications.error("Error getting automations list")
}
})
function selectAutomation(id) {
automationStore.actions.select(id)
}
</script>
<div class="side-bar">
@ -71,17 +60,7 @@
{triggerGroup?.name}
</div>
{#each triggerGroup.entries as automation}
<NavItem
icon={triggerGroup.icon}
iconColor={"var(--spectrum-global-color-gray-900)"}
text={automation.name}
selected={automation._id === selectedAutomationId}
on:click={() => selectAutomation(automation._id)}
selectedBy={$userSelectedResourceMap[automation._id]}
disabled={automation.disabled}
>
<EditAutomationPopover {automation} />
</NavItem>
<AutomationNavItem {automation} icon={triggerGroup.icon} />
{/each}
</div>
{/each}

View File

@ -1,73 +0,0 @@
<script>
import { automationStore } from "stores/builder"
import { ActionMenu, MenuItem, notifications, Icon } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import UpdateAutomationModal from "components/automation/AutomationPanel/UpdateAutomationModal.svelte"
export let automation
let confirmDeleteDialog
let updateAutomationDialog
async function deleteAutomation() {
try {
await automationStore.actions.delete(automation)
notifications.success("Automation deleted successfully")
} catch (error) {
notifications.error("Error deleting automation")
}
}
async function duplicateAutomation() {
try {
await automationStore.actions.duplicate(automation)
notifications.success("Automation has been duplicated successfully")
} catch (error) {
notifications.error("Error duplicating automation")
}
}
</script>
<ActionMenu>
<div slot="control" class="icon">
<Icon s hoverable name="MoreSmallList" />
</div>
<MenuItem
icon="Duplicate"
on:click={duplicateAutomation}
disabled={automation.definition.trigger.name === "Webhook"}
>Duplicate</MenuItem
>
<MenuItem icon="Edit" on:click={updateAutomationDialog.show}>Edit</MenuItem>
<MenuItem
icon={automation.disabled ? "CheckmarkCircle" : "Cancel"}
on:click={automationStore.actions.toggleDisabled(
automation._id,
automation.disabled
)}
>
{automation.disabled ? "Activate" : "Pause"}
</MenuItem>
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
</ActionMenu>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Automation"
onOk={deleteAutomation}
title="Confirm Deletion"
>
Are you sure you wish to delete the automation
<i>{automation.name}?</i>
This action cannot be undone.
</ConfirmDialog>
<UpdateAutomationModal {automation} bind:this={updateAutomationDialog} />
<style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
</style>

View File

@ -79,18 +79,18 @@
table = $tables.list.find(table => table._id === tableId)
schemaFields = Object.entries(table?.schema ?? {})
.filter(entry => {
const [, field] = entry
return field.type !== "formula" && !field.autocolumn
})
.sort(([nameA], [nameB]) => {
return nameA < nameB ? -1 : 1
})
if (table) {
editableRow["tableId"] = tableId
schemaFields = Object.entries(table?.schema ?? {})
.filter(entry => {
const [, field] = entry
return field.type !== "formula" && !field.autocolumn
})
.sort(([nameA], [nameB]) => {
return nameA < nameB ? -1 : 1
})
// Parse out any data not in the schema.
for (const column in editableFields) {
if (!Object.hasOwn(table?.schema, column)) {

View File

@ -0,0 +1,82 @@
<script>
import { isActive } from "@roxi/routify"
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
import { contextMenuStore, userSelectedResourceMap } from "stores/builder"
import NavItem from "components/common/NavItem.svelte"
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
import { Icon } from "@budibase/bbui"
import UpdateDatasourceModal from "components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte"
import DeleteConfirmationModal from "./DeleteConfirmationModal.svelte"
export let datasource
let editModal
let deleteConfirmationModal
const getContextMenuItems = () => {
return [
{
icon: "Edit",
name: "Edit",
keyBind: null,
visible: true,
disabled: false,
callback: editModal.show,
},
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: deleteConfirmationModal.show,
},
]
}
const openContextMenu = e => {
if (datasource._id === BUDIBASE_INTERNAL_DB_ID) {
return
}
e.preventDefault()
e.stopPropagation()
const items = getContextMenuItems()
contextMenuStore.open(datasource._id, items, { x: e.clientX, y: e.clientY })
}
</script>
<NavItem
on:contextmenu={openContextMenu}
border
text={datasource.name}
opened={datasource.open}
selected={$isActive("./datasource") && datasource.selected}
hovering={datasource._id === $contextMenuStore.id}
withArrow={true}
on:click
on:iconClick
selectedBy={$userSelectedResourceMap[datasource._id]}
>
<div class="datasource-icon" slot="icon">
<IntegrationIcon
integrationType={datasource.source}
schema={datasource.schema}
size="18"
/>
</div>
{#if datasource._id !== BUDIBASE_INTERNAL_DB_ID}
<Icon on:click={openContextMenu} size="S" hoverable name="MoreSmallList" />
{/if}
</NavItem>
<UpdateDatasourceModal {datasource} bind:this={editModal} />
<DeleteConfirmationModal {datasource} bind:this={deleteConfirmationModal} />
<style>
.datasource-icon {
display: grid;
place-items: center;
flex: 0 0 24px;
}
</style>

View File

@ -1,15 +1,16 @@
<script>
import { goto } from "@roxi/routify"
import { datasources } from "stores/builder"
import { notifications, ActionMenu, MenuItem, Icon } from "@budibase/bbui"
import { notifications } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import UpdateDatasourceModal from "components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte"
import { BUDIBASE_DATASOURCE_TYPE } from "constants/backend"
export let datasource
let confirmDeleteDialog
let updateDatasourceDialog
export const show = () => {
confirmDeleteDialog.show()
}
async function deleteDatasource() {
try {
@ -25,16 +26,6 @@
}
</script>
<ActionMenu>
<div slot="control" class="icon">
<Icon size="S" hoverable name="MoreSmallList" />
</div>
{#if datasource.type !== BUDIBASE_DATASOURCE_TYPE}
<MenuItem icon="Edit" on:click={updateDatasourceDialog.show}>Edit</MenuItem>
{/if}
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
</ActionMenu>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Datasource"
@ -45,13 +36,3 @@
<i>{datasource.name}?</i>
This action cannot be undone.
</ConfirmDialog>
<UpdateDatasourceModal {datasource} bind:this={updateDatasourceDialog} />
<style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
</style>

View File

@ -1,7 +1,6 @@
<script>
import { goto, isActive, params } from "@roxi/routify"
import { Layout } from "@budibase/bbui"
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
import {
datasources,
queries,
@ -10,16 +9,10 @@
viewsV2,
userSelectedResourceMap,
} from "stores/builder"
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
import QueryNavItem from "./QueryNavItem.svelte"
import NavItem from "components/common/NavItem.svelte"
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
import {
customQueryIconText,
customQueryIconColor,
customQueryText,
} from "helpers/data/utils"
import IntegrationIcon from "./IntegrationIcon.svelte"
import DatasourceNavItem from "./DatasourceNavItem/DatasourceNavItem.svelte"
import { TableNames } from "constants"
import { enrichDatasources } from "./datasourceUtils"
import { onMount } from "svelte"
@ -86,44 +79,15 @@
/>
{/if}
{#each enrichedDataSources.filter(ds => ds.show) as datasource}
<NavItem
border
text={datasource.name}
opened={datasource.open}
selected={$isActive("./datasource") && datasource.selected}
withArrow={true}
<DatasourceNavItem
{datasource}
on:click={() => selectDatasource(datasource)}
on:iconClick={() => toggleNode(datasource)}
selectedBy={$userSelectedResourceMap[datasource._id]}
>
<div class="datasource-icon" slot="icon">
<IntegrationIcon
integrationType={datasource.source}
schema={datasource.schema}
size="18"
/>
</div>
{#if datasource._id !== BUDIBASE_INTERNAL_DB_ID}
<EditDatasourcePopover {datasource} />
{/if}
</NavItem>
/>
{#if datasource.open}
<TableNavigator tables={datasource.tables} {selectTable} />
{#each datasource.queries as query}
<NavItem
indentLevel={1}
icon="SQLQuery"
iconText={customQueryIconText(datasource, query)}
iconColor={customQueryIconColor(datasource, query)}
text={customQueryText(datasource, query)}
selected={$isActive("./query/:queryId") &&
$queries.selectedQueryId === query._id}
on:click={() => $goto(`./query/${query._id}`)}
selectedBy={$userSelectedResourceMap[query._id]}
>
<EditQueryPopover {query} />
</NavItem>
<QueryNavItem {datasource} {query} />
{/each}
{/if}
{/each}
@ -140,11 +104,6 @@
.hierarchy-items-container {
margin: 0 calc(-1 * var(--spacing-l));
}
.datasource-icon {
display: grid;
place-items: center;
flex: 0 0 24px;
}
.no-results {
color: var(--spectrum-global-color-gray-600);

View File

@ -0,0 +1,103 @@
<script>
import {
customQueryIconText,
customQueryIconColor,
customQueryText,
} from "helpers/data/utils"
import { goto as gotoStore, isActive } from "@roxi/routify"
import {
datasources,
queries,
userSelectedResourceMap,
contextMenuStore,
} from "stores/builder"
import NavItem from "components/common/NavItem.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { notifications, Icon } from "@budibase/bbui"
export let datasource
export let query
let confirmDeleteDialog
// goto won't work in the context menu callback if the store is called directly
$: goto = $gotoStore
const getContextMenuItems = () => {
return [
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: confirmDeleteDialog.show,
},
{
icon: "Duplicate",
name: "Duplicate",
keyBind: null,
visible: true,
disabled: false,
callback: async () => {
try {
const newQuery = await queries.duplicate(query)
goto(`./query/${newQuery._id}`)
} catch (error) {
notifications.error("Error duplicating query")
}
},
},
]
}
async function deleteQuery() {
try {
// Go back to the datasource if we are deleting the active query
if ($queries.selectedQueryId === query._id) {
goto(`./datasource/${query.datasourceId}`)
}
await queries.delete(query)
await datasources.fetch()
notifications.success("Query deleted")
} catch (error) {
notifications.error("Error deleting query")
}
}
const openContextMenu = e => {
e.preventDefault()
e.stopPropagation()
const items = getContextMenuItems()
contextMenuStore.open(query._id, items, { x: e.clientX, y: e.clientY })
}
</script>
<NavItem
on:contextmenu={openContextMenu}
indentLevel={1}
icon="SQLQuery"
iconText={customQueryIconText(datasource, query)}
iconColor={customQueryIconColor(datasource, query)}
text={customQueryText(datasource, query)}
selected={$isActive("./query/:queryId") &&
$queries.selectedQueryId === query._id}
hovering={query._id === $contextMenuStore.id}
on:click={() => goto(`./query/${query._id}`)}
selectedBy={$userSelectedResourceMap[query._id]}
>
<Icon size="S" hoverable name="MoreSmallList" on:click={openContextMenu} />
</NavItem>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Query"
onOk={deleteQuery}
title="Confirm Deletion"
>
Are you sure you wish to delete this query? This action cannot be undone.
</ConfirmDialog>
<style>
</style>

View File

@ -1,59 +0,0 @@
<script>
import { goto } from "@roxi/routify"
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { datasources, queries } from "stores/builder"
export let query
let confirmDeleteDialog
async function deleteQuery() {
try {
// Go back to the datasource if we are deleting the active query
if ($queries.selectedQueryId === query._id) {
$goto(`./datasource/${query.datasourceId}`)
}
await queries.delete(query)
await datasources.fetch()
notifications.success("Query deleted")
} catch (error) {
notifications.error("Error deleting query")
}
}
async function duplicateQuery() {
try {
const newQuery = await queries.duplicate(query)
$goto(`./query/${newQuery._id}`)
} catch (error) {
notifications.error("Error duplicating query")
}
}
</script>
<ActionMenu>
<div slot="control" class="icon">
<Icon size="S" hoverable name="MoreSmallList" />
</div>
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
<MenuItem icon="Duplicate" on:click={duplicateQuery}>Duplicate</MenuItem>
</ActionMenu>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Query"
onOk={deleteQuery}
title="Confirm Deletion"
>
Are you sure you wish to delete this query? This action cannot be undone.
</ConfirmDialog>
<style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
</style>

View File

@ -1,35 +1,15 @@
<script>
import { goto, params } from "@roxi/routify"
import { cloneDeep } from "lodash/fp"
import { tables, datasources, screenStore } from "stores/builder"
import {
ActionMenu,
Icon,
Input,
MenuItem,
Modal,
ModalContent,
notifications,
} from "@budibase/bbui"
import { Input, notifications } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { DB_TYPE_EXTERNAL } from "constants/backend"
export let table
let editorModal, editTableNameModal
let confirmDeleteDialog
let error = ""
let originalName
let updatedName
let templateScreens
let willBeDeleted
let deleteTableName
$: externalTable = table?.sourceType === DB_TYPE_EXTERNAL
function showDeleteModal() {
export const show = () => {
templateScreens = $screenStore.screens.filter(
screen => screen.autoTableId === table._id
)
@ -39,6 +19,10 @@
confirmDeleteDialog.show()
}
let templateScreens
let willBeDeleted
let deleteTableName
async function deleteTable() {
const isSelected = $params.tableId === table._id
try {
@ -62,58 +46,8 @@
function hideDeleteDialog() {
deleteTableName = ""
}
async function save() {
const updatedTable = cloneDeep(table)
updatedTable.name = updatedName
await tables.save(updatedTable)
await datasources.fetch()
notifications.success("Table renamed successfully")
}
function checkValid(evt) {
const tableName = evt.target.value
error =
originalName === tableName
? `Table with name ${tableName} already exists. Please choose another name.`
: ""
}
const initForm = () => {
originalName = table.name + ""
updatedName = table.name + ""
}
</script>
<ActionMenu>
<div slot="control" class="icon">
<Icon s hoverable name="MoreSmallList" />
</div>
{#if !externalTable}
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
{/if}
<MenuItem icon="Delete" on:click={showDeleteModal}>Delete</MenuItem>
</ActionMenu>
<Modal bind:this={editorModal} on:show={initForm}>
<ModalContent
bind:this={editTableNameModal}
title="Edit Table"
confirmText="Save"
onConfirm={save}
disabled={updatedName === originalName || error}
>
<form on:submit|preventDefault={() => editTableNameModal.confirm()}>
<Input
label="Table Name"
thin
bind:value={updatedName}
on:input={checkValid}
{error}
/>
</form>
</ModalContent>
</Modal>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Table"
@ -142,13 +76,6 @@
</ConfirmDialog>
<style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
div.delete-items {
margin-top: 10px;
margin-bottom: 10px;

View File

@ -0,0 +1,58 @@
<script>
import { cloneDeep } from "lodash/fp"
import { tables, datasources } from "stores/builder"
import { Input, Modal, ModalContent, notifications } from "@budibase/bbui"
export let table
export const show = () => {
editorModal.show()
}
let editorModal, editTableNameModal
let error = ""
let originalName
let updatedName
async function save() {
const updatedTable = cloneDeep(table)
updatedTable.name = updatedName
await tables.save(updatedTable)
await datasources.fetch()
notifications.success("Table renamed successfully")
}
function checkValid(evt) {
const tableName = evt.target.value
error =
originalName === tableName
? `Table with name ${tableName} already exists. Please choose another name.`
: ""
}
const initForm = () => {
originalName = table.name + ""
updatedName = table.name + ""
}
</script>
<Modal bind:this={editorModal} on:show={initForm}>
<ModalContent
bind:this={editTableNameModal}
title="Edit Table"
confirmText="Save"
onConfirm={save}
disabled={updatedName === originalName || error}
>
<form on:submit|preventDefault={() => editTableNameModal.confirm()}>
<Input
label="Table Name"
thin
bind:value={updatedName}
on:input={checkValid}
{error}
/>
</form>
</ModalContent>
</Modal>

View File

@ -0,0 +1,68 @@
<script>
import {
tables as tablesStore,
userSelectedResourceMap,
contextMenuStore,
} from "stores/builder"
import { TableNames } from "constants"
import NavItem from "components/common/NavItem.svelte"
import { isActive } from "@roxi/routify"
import EditModal from "./EditModal.svelte"
import DeleteConfirmationModal from "./DeleteConfirmationModal.svelte"
import { Icon } from "@budibase/bbui"
import { DB_TYPE_EXTERNAL } from "constants/backend"
export let table
export let idx
let editModal
let deleteConfirmationModal
const getContextMenuItems = () => {
return [
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: deleteConfirmationModal.show,
},
{
icon: "Edit",
name: "Edit",
keyBind: null,
visible: table?.sourceType !== DB_TYPE_EXTERNAL,
disabled: false,
callback: editModal.show,
},
]
}
const openContextMenu = e => {
e.preventDefault()
e.stopPropagation()
const items = getContextMenuItems()
contextMenuStore.open(table._id, items, { x: e.clientX, y: e.clientY })
}
</script>
<NavItem
on:contextmenu={openContextMenu}
indentLevel={1}
border={idx > 0}
icon={table._id === TableNames.USERS ? "UserGroup" : "Table"}
text={table.name}
hovering={table._id === $contextMenuStore.id}
selected={$isActive("./table/:tableId") &&
$tablesStore.selected?._id === table._id}
selectedBy={$userSelectedResourceMap[table._id]}
on:click
>
{#if table._id !== TableNames.USERS}
<Icon s on:click={openContextMenu} hoverable name="MoreSmallList" />
{/if}
</NavItem>
<EditModal {table} bind:this={editModal} />
<DeleteConfirmationModal {table} bind:this={deleteConfirmationModal} />

View File

@ -1,15 +1,7 @@
<script>
import {
tables as tablesStore,
views,
viewsV2,
userSelectedResourceMap,
} from "stores/builder"
import { TableNames } from "constants"
import EditTablePopover from "./popovers/EditTablePopover.svelte"
import EditViewPopover from "./popovers/EditViewPopover.svelte"
import NavItem from "components/common/NavItem.svelte"
import { goto, isActive } from "@roxi/routify"
import { goto } from "@roxi/routify"
import TableNavItem from "./TableNavItem/TableNavItem.svelte"
import ViewNavItem from "./ViewNavItem/ViewNavItem.svelte"
export let tables
export let selectTable
@ -19,37 +11,15 @@
const alphabetical = (a, b) => {
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
}
const isViewActive = (view, isActive, views, viewsV2) => {
return (
(isActive("./view/v1") && views.selected?.name === view.name) ||
(isActive("./view/v2") && viewsV2.selected?.id === view.id)
)
}
</script>
<div class="hierarchy-items-container">
{#each sortedTables as table, idx}
<NavItem
indentLevel={1}
border={idx > 0}
icon={table._id === TableNames.USERS ? "UserGroup" : "Table"}
text={table.name}
selected={$isActive("./table/:tableId") &&
$tablesStore.selected?._id === table._id}
on:click={() => selectTable(table._id)}
selectedBy={$userSelectedResourceMap[table._id]}
>
{#if table._id !== TableNames.USERS}
<EditTablePopover {table} />
{/if}
</NavItem>
<TableNavItem {table} {idx} on:click={() => selectTable(table._id)} />
{#each [...Object.entries(table.views || {})].sort() as [name, view], idx (idx)}
<NavItem
indentLevel={2}
icon="Remove"
text={name}
selected={isViewActive(view, $isActive, $views, $viewsV2)}
<ViewNavItem
{view}
{name}
on:click={() => {
if (view.version === 2) {
$goto(`./view/v2/${encodeURIComponent(view.id)}`)
@ -57,11 +27,7 @@
$goto(`./view/v1/${encodeURIComponent(name)}`)
}
}}
selectedBy={$userSelectedResourceMap[name] ||
$userSelectedResourceMap[view.id]}
>
<EditViewPopover {view} />
</NavItem>
/>
{/each}
{/each}
</div>

View File

@ -0,0 +1,34 @@
<script>
import { views, viewsV2 } from "stores/builder"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { notifications } from "@budibase/bbui"
export let view
let confirmDeleteDialog
export const show = () => {
confirmDeleteDialog.show()
}
async function deleteView() {
try {
if (view.version === 2) {
await viewsV2.delete(view)
} else {
await views.delete(view)
}
notifications.success("View deleted")
} catch (error) {
notifications.error("Error deleting view")
}
}
</script>
<ConfirmDialog
bind:this={confirmDeleteDialog}
body={`Are you sure you wish to delete the view '${view.name}'? Your data will be deleted and this action cannot be undone.`}
okText="Delete View"
onOk={deleteView}
title="Confirm Deletion"
/>

View File

@ -0,0 +1,45 @@
<script>
import { views, viewsV2 } from "stores/builder"
import { cloneDeep } from "lodash/fp"
import { notifications, Input, Modal, ModalContent } from "@budibase/bbui"
export let view
let editorModal
let originalName
let updatedName
export const show = () => {
editorModal.show()
}
async function save() {
const updatedView = cloneDeep(view)
updatedView.name = updatedName
if (view.version === 2) {
await viewsV2.save({
originalName,
...updatedView,
})
} else {
await views.save({
originalName,
...updatedView,
})
}
notifications.success("View renamed successfully")
}
const initForm = () => {
updatedName = view.name + ""
originalName = view.name + ""
}
</script>
<Modal bind:this={editorModal} on:show={initForm}>
<ModalContent title="Edit View" onConfirm={save} confirmText="Save">
<Input label="View Name" thin bind:value={updatedName} />
</ModalContent>
</Modal>

View File

@ -0,0 +1,71 @@
<script>
import {
contextMenuStore,
views,
viewsV2,
userSelectedResourceMap,
} from "stores/builder"
import NavItem from "components/common/NavItem.svelte"
import { isActive } from "@roxi/routify"
import { Icon } from "@budibase/bbui"
import EditViewModal from "./EditViewModal.svelte"
import DeleteConfirmationModal from "./DeleteConfirmationModal.svelte"
export let view
export let name
let editModal
let deleteConfirmationModal
const getContextMenuItems = () => {
return [
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: deleteConfirmationModal.show,
},
{
icon: "Edit",
name: "Edit",
keyBind: null,
visible: true,
disabled: false,
callback: editModal.show,
},
]
}
const openContextMenu = e => {
e.preventDefault()
e.stopPropagation()
const items = getContextMenuItems()
contextMenuStore.open(view.id, items, { x: e.clientX, y: e.clientY })
}
const isViewActive = (view, isActive, views, viewsV2) => {
return (
(isActive("./view/v1") && views.selected?.name === view.name) ||
(isActive("./view/v2") && viewsV2.selected?.id === view.id)
)
}
</script>
<NavItem
on:contextmenu={openContextMenu}
indentLevel={2}
icon="Remove"
text={name}
selected={isViewActive(view, $isActive, $views, $viewsV2)}
hovering={view.id === $contextMenuStore.id}
on:click
selectedBy={$userSelectedResourceMap[name] ||
$userSelectedResourceMap[view.id]}
>
<Icon on:click={openContextMenu} s hoverable name="MoreSmallList" />
</NavItem>
<EditViewModal {view} bind:this={editModal} />
<DeleteConfirmationModal {view} bind:this={deleteConfirmationModal} />

View File

@ -1,78 +0,0 @@
<script>
import { views, viewsV2 } from "stores/builder"
import { cloneDeep } from "lodash/fp"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import {
notifications,
Icon,
Input,
ActionMenu,
MenuItem,
Modal,
ModalContent,
} from "@budibase/bbui"
export let view
let editorModal
let originalName
let updatedName
let confirmDeleteDialog
async function save() {
const updatedView = cloneDeep(view)
updatedView.name = updatedName
if (view.version === 2) {
await viewsV2.save({
originalName,
...updatedView,
})
} else {
await views.save({
originalName,
...updatedView,
})
}
notifications.success("View renamed successfully")
}
async function deleteView() {
try {
if (view.version === 2) {
await viewsV2.delete(view)
} else {
await views.delete(view)
}
notifications.success("View deleted")
} catch (error) {
notifications.error("Error deleting view")
}
}
const initForm = () => {
updatedName = view.name + ""
originalName = view.name + ""
}
</script>
<ActionMenu>
<div slot="control" class="icon open-popover">
<Icon s hoverable name="MoreSmallList" />
</div>
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
</ActionMenu>
<Modal bind:this={editorModal} on:show={initForm}>
<ModalContent title="Edit View" onConfirm={save} confirmText="Save">
<Input label="View Name" thin bind:value={updatedName} />
</ModalContent>
</Modal>
<ConfirmDialog
bind:this={confirmDeleteDialog}
body={`Are you sure you wish to delete the view '${view.name}'? Your data will be deleted and this action cannot be undone.`}
okText="Delete View"
onOk={deleteView}
title="Confirm Deletion"
/>

View File

@ -83,6 +83,7 @@
on:mouseenter
on:mouseleave
on:click={onClick}
on:contextmenu
ondragover="return false"
ondragenter="return false"
{id}

View File

@ -1,5 +1,5 @@
<script>
import { AbsTooltip, Icon, Body } from "@budibase/bbui"
import { Icon, Body, TooltipPosition, TooltipType } from "@budibase/bbui"
export let title
export let icon
@ -18,6 +18,7 @@
export let closeButtonIcon = "Close"
export let noHeaderBorder = false
export let titleCSS = true
$: customHeaderContent = $$slots["panel-header-content"]
$: customTitleContent = $$slots["panel-title-content"]
</script>
@ -41,9 +42,12 @@
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
{/if}
{#if icon}
<AbsTooltip type="info" text={iconTooltip}>
<Icon name={icon} />
</AbsTooltip>
<Icon
name={icon}
tooltipType={TooltipType.Info}
tooltip={iconTooltip}
tooltipPosition={TooltipPosition.Top}
/>
{/if}
<div class:title={titleCSS}>
{#if customTitleContent}

View File

@ -0,0 +1,56 @@
<script>
import { Modal } from "@budibase/bbui"
import DeleteModal from "components/deploy/DeleteModal.svelte"
import ExportAppModal from "./ExportAppModal.svelte"
import DuplicateAppModal from "./DuplicateAppModal.svelte"
import { licensing } from "stores/portal"
export let app
let exportPublishedVersion = false
let deleteModal
let exportModal
let duplicateModal
export const showDuplicateModal = () => {
duplicateModal.show()
}
export const showExportDevModal = () => {
exportPublishedVersion = false
exportModal.show()
}
export const showExportProdModal = () => {
exportPublishedVersion = true
exportModal.show()
}
export const showDeleteModal = () => {
deleteModal.show()
}
</script>
<DeleteModal
bind:this={deleteModal}
appId={app?.devId}
appName={app?.name}
onDeleteSuccess={async () => {
await licensing.init()
}}
/>
<Modal bind:this={exportModal} padding={false}>
<ExportAppModal {app} published={exportPublishedVersion} />
</Modal>
<Modal bind:this={duplicateModal} padding={false}>
<DuplicateAppModal
appId={app?.devId}
appName={app?.name}
onDuplicateSuccess={async () => {
await licensing.init()
}}
/>
</Modal>

View File

@ -5,14 +5,17 @@
import { goto } from "@roxi/routify"
import { UserAvatars } from "@budibase/frontend-core"
import { sdk } from "@budibase/shared-core"
import AppRowContext from "./AppRowContext.svelte"
import AppContextMenuModals from "./AppContextMenuModals.svelte"
import getAppContextMenuItems from "./getAppContextMenuItems.js"
import FavouriteAppButton from "pages/builder/portal/apps/FavouriteAppButton.svelte"
import { contextMenuStore } from "stores/builder"
export let app
export let lockedAction
let actionsOpen = false
let appContextMenuModals
$: contextMenuOpen = `${app.appId}-index` === $contextMenuStore.id
$: editing = app.sessions?.length
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
$: unclickable = !isBuilder && !app.deployed
@ -40,16 +43,35 @@
window.open(`/app${app.url}`, "_blank")
}
}
const openContextMenu = e => {
e.preventDefault()
e.stopPropagation()
const items = getAppContextMenuItems({
app,
onDuplicate: appContextMenuModals?.showDuplicateModal,
onExportDev: appContextMenuModals?.showExportDevModal,
onExportProd: appContextMenuModals?.showExportProdModal,
onDelete: appContextMenuModals?.showDeleteModal,
})
contextMenuStore.open(`${app.appId}-index`, items, {
x: e.clientX,
y: e.clientY,
})
}
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class:contextMenuOpen
class="app-row"
class:unclickable
class:actionsOpen
class:favourite={app.favourite}
on:click={lockedAction || handleDefaultClick}
on:contextmenu={openContextMenu}
>
<div class="title">
<div class="app-icon">
@ -89,14 +111,11 @@
</Button>
</div>
<div class="row-action">
<AppRowContext
{app}
on:open={() => {
actionsOpen = true
}}
on:close={() => {
actionsOpen = false
}}
<Icon
on:click={openContextMenu}
size="S"
hoverable
name="MoreSmallList"
/>
</div>
{:else}
@ -109,6 +128,7 @@
<FavouriteAppButton {app} noWrap />
</div>
</div>
<AppContextMenuModals {app} bind:this={appContextMenuModals} />
</div>
<style>
@ -123,7 +143,8 @@
transition: border 130ms ease-out;
border: 1px solid transparent;
}
.app-row:not(.unclickable):hover {
.app-row:not(.unclickable):hover,
.contextMenuOpen {
cursor: pointer;
border-color: var(--spectrum-global-color-gray-300);
}
@ -132,9 +153,9 @@
display: none;
}
.app-row.contextMenuOpen .favourite-icon,
.app-row:hover .favourite-icon,
.app-row.favourite .favourite-icon,
.app-row.actionsOpen .favourite-icon {
.app-row.favourite .favourite-icon {
display: flex;
}
@ -176,8 +197,8 @@
display: none;
}
.app-row:hover .app-row-actions,
.app-row.actionsOpen .app-row-actions {
.app-row.contextMenuOpen .app-row-actions,
.app-row:hover .app-row-actions {
gap: var(--spacing-m);
flex-direction: row;
justify-content: flex-end;

View File

@ -1,108 +0,0 @@
<script>
import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui"
import DeleteModal from "components/deploy/DeleteModal.svelte"
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
import ExportAppModal from "./ExportAppModal.svelte"
import DuplicateAppModal from "./DuplicateAppModal.svelte"
import { onMount } from "svelte"
import { licensing } from "stores/portal"
export let app
export let align = "right"
export let options
let deleteModal
let exportModal
let duplicateModal
let exportPublishedVersion = false
let loaded = false
const getActions = app => {
if (!loaded) {
return []
}
return [
{
id: "duplicate",
icon: "Copy",
onClick: duplicateModal.show,
body: "Duplicate",
},
{
id: "exportDev",
icon: "Export",
onClick: () => {
exportPublishedVersion = false
exportModal.show()
},
body: "Export latest edited app",
},
{
id: "exportProd",
icon: "Export",
onClick: () => {
exportPublishedVersion = true
exportModal.show()
},
body: "Export latest published app",
},
{
id: "delete",
icon: "Delete",
onClick: deleteModal.show,
body: "Delete",
},
].filter(action => {
if (action.id === "exportProd" && app.deployed !== true) {
return false
} else if (Array.isArray(options) && !options.includes(action.id)) {
return false
}
return true
})
}
$: actions = getActions(app, loaded)
onMount(() => {
loaded = true
})
let appLimitModal
</script>
<DeleteModal
bind:this={deleteModal}
appId={app.devId}
appName={app.name}
onDeleteSuccess={async () => {
await licensing.init()
}}
/>
<AppLimitModal bind:this={appLimitModal} />
<Modal bind:this={exportModal} padding={false}>
<ExportAppModal {app} published={exportPublishedVersion} />
</Modal>
<Modal bind:this={duplicateModal} padding={false}>
<DuplicateAppModal
appId={app.devId}
appName={app.name}
onDuplicateSuccess={async () => {
await licensing.init()
}}
/>
</Modal>
<ActionMenu {align} on:open on:close>
<div slot="control" class="icon">
<Icon size="S" hoverable name="MoreSmallList" />
</div>
{#each actions as action}
<MenuItem icon={action.icon} on:click={action.onClick}>
{action.body}
</MenuItem>
{/each}
</ActionMenu>

View File

@ -0,0 +1,44 @@
const getAppContextMenuItems = ({
app,
onDuplicate,
onExportDev,
onExportProd,
onDelete,
}) => {
return [
{
icon: "Copy",
name: "Duplicate",
keyBind: null,
visible: true,
disabled: false,
callback: onDuplicate,
},
{
icon: "Export",
name: "Export latest edited app",
keyBind: null,
visible: true,
disabled: false,
callback: onExportDev,
},
{
icon: "Export",
name: "Export latest published app",
keyBind: null,
visible: true,
disabled: !app.deployed,
callback: onExportProd,
},
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: onDelete,
},
]
}
export default getAppContextMenuItems

View File

@ -299,15 +299,6 @@ export const PaginationLocations = [
{ label: "Request body", value: "body" },
]
export const BannedSearchTypes = [
FieldType.LINK,
FieldType.ATTACHMENTS,
FieldType.FORMULA,
FieldType.JSON,
"jsonarray",
"queryarray",
]
export const DatasourceTypes = {
RELATIONAL: "Relational",
NON_RELATIONAL: "Non-relational",

View File

@ -5,6 +5,7 @@
import { CookieUtils, Constants } from "@budibase/frontend-core"
import { API } from "api"
import Branding from "./Branding.svelte"
import ContextMenu from "components/ContextMenu.svelte"
let loaded = false
@ -160,6 +161,7 @@
<!--Portal branding overrides -->
<Branding />
<ContextMenu />
{#if loaded}
<slot />

View File

@ -1,129 +0,0 @@
<script>
import { componentStore } from "stores/builder"
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
export let component
export let opened
$: definition = componentStore.getDefinition(component?._component)
$: noPaste = !$componentStore.componentToPaste
$: isBlock = definition?.block === true
$: canEject = !(definition?.ejectable === false)
const keyboardEvent = (key, ctrlKey = false) => {
document.dispatchEvent(
new CustomEvent("component-menu", {
detail: {
key,
ctrlKey,
id: component?._id,
},
})
)
}
</script>
<ActionMenu>
<div slot="control" class="icon">
<Icon size="S" hoverable name="MoreSmallList" />
</div>
<MenuItem
icon="Delete"
keyBind="!BackAndroid"
on:click={() => keyboardEvent("Delete")}
>
Delete
</MenuItem>
{#if isBlock && canEject}
<MenuItem
icon="Export"
keyBind="Ctrl+E"
on:click={() => keyboardEvent("e", true)}
>
Eject block
</MenuItem>
{/if}
<MenuItem
icon="ChevronUp"
keyBind="Ctrl+!ArrowUp"
on:click={() => keyboardEvent("ArrowUp", true)}
>
Move up
</MenuItem>
<MenuItem
icon="ChevronDown"
keyBind="Ctrl+!ArrowDown"
on:click={() => keyboardEvent("ArrowDown", true)}
>
Move down
</MenuItem>
<MenuItem
icon="Duplicate"
keyBind="Ctrl+D"
on:click={() => keyboardEvent("d", true)}
>
Duplicate
</MenuItem>
<MenuItem
icon="Cut"
keyBind="Ctrl+X"
on:click={() => keyboardEvent("x", true)}
>
Cut
</MenuItem>
<MenuItem
icon="Copy"
keyBind="Ctrl+C"
on:click={() => keyboardEvent("c", true)}
>
Copy
</MenuItem>
<MenuItem
icon="LayersSendToBack"
keyBind="Ctrl+V"
on:click={() => keyboardEvent("v", true)}
disabled={noPaste}
>
Paste
</MenuItem>
{#if component?._children?.length}
<MenuItem
icon="TreeExpand"
keyBind="!ArrowRight"
on:click={() => keyboardEvent("ArrowRight", false)}
disabled={opened}
>
Expand
</MenuItem>
<MenuItem
icon="TreeCollapse"
keyBind="!ArrowLeft"
on:click={() => keyboardEvent("ArrowLeft", false)}
disabled={!opened}
>
Collapse
</MenuItem>
<MenuItem
icon="TreeExpandAll"
keyBind="Ctrl+!ArrowRight"
on:click={() => keyboardEvent("ArrowRight", true)}
>
Expand All
</MenuItem>
<MenuItem
icon="TreeCollapseAll"
keyBind="Ctrl+!ArrowLeft"
on:click={() => keyboardEvent("ArrowLeft", true)}
>
Collapse All
</MenuItem>
{/if}
</ActionMenu>
<style>
.icon {
display: grid;
place-items: center;
}
</style>

View File

@ -1,7 +1,6 @@
<script>
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import NavItem from "components/common/NavItem.svelte"
import { notifications } from "@budibase/bbui"
import { Icon, notifications } from "@budibase/bbui"
import {
selectedScreen,
componentStore,
@ -9,6 +8,7 @@
selectedComponent,
hoverStore,
componentTreeNodesStore,
contextMenuStore,
} from "stores/builder"
import {
findComponentPath,
@ -17,6 +17,7 @@
} from "helpers/components"
import { get } from "svelte/store"
import { dndStore } from "./dndStore"
import getComponentContextMenuItems from "./getComponentContextMenuItems"
export let components = []
export let level = 0
@ -85,6 +86,18 @@
}
const hover = hoverStore.hover
const openContextMenu = (e, component, opened) => {
e.preventDefault()
e.stopPropagation()
const items = getComponentContextMenuItems(
component,
!opened,
componentStore
)
contextMenuStore.open(component._id, items, { x: e.clientX, y: e.clientY })
}
</script>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions-->
@ -93,6 +106,7 @@
{#each filteredComponents || [] as component, index (component._id)}
{@const opened = isOpen(component, openNodes)}
<li
on:contextmenu={e => openContextMenu(e, component, opened)}
on:click|stopPropagation={() => {
componentStore.select(component._id)
}}
@ -107,7 +121,8 @@
on:dragover={dragover(component, index)}
on:iconClick={() => handleIconClick(component._id)}
on:drop={onDrop}
hovering={$hoverStore.componentId === component._id}
hovering={$hoverStore.componentId === component._id ||
component._id === $contextMenuStore.id}
on:mouseenter={() => hover(component._id)}
on:mouseleave={() => hover(null)}
text={getComponentText(component)}
@ -120,7 +135,12 @@
highlighted={isChildOfSelectedComponent(component)}
selectedBy={$userSelectedResourceMap[component._id]}
>
<ComponentDropdownMenu {opened} {component} />
<Icon
size="S"
hoverable
name="MoreSmallList"
on:click={e => openContextMenu(e, component, opened)}
/>
</NavItem>
{#if opened}

View File

@ -1,57 +0,0 @@
<script>
import { componentStore } from "stores/builder"
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
export let component
$: definition = componentStore.getDefinition(component?._component)
$: noPaste = !$componentStore.componentToPaste
// "editable" has been repurposed for inline text editing.
// It remains here for legacy compatibility.
// Future components should define "static": true for indicate they should
// not show a context menu.
$: showMenu = definition?.editable !== false && definition?.static !== true
const storeComponentForCopy = (cut = false) => {
componentStore.copy(component, cut)
}
const pasteComponent = mode => {
try {
componentStore.paste(component, mode)
} catch (error) {
notifications.error("Error saving component")
}
}
</script>
{#if showMenu}
<ActionMenu>
<div slot="control" class="icon">
<Icon size="S" hoverable name="MoreSmallList" />
</div>
<MenuItem
icon="Copy"
keyBind="Ctrl+C"
on:click={() => storeComponentForCopy(false)}
>
Copy
</MenuItem>
<MenuItem
icon="LayersSendToBack"
keyBind="Ctrl+V"
on:click={() => pasteComponent("inside")}
disabled={noPaste}
>
Paste
</MenuItem>
</ActionMenu>
{/if}
<style>
.icon {
display: grid;
place-items: center;
}
</style>

View File

@ -0,0 +1,123 @@
import { get } from "svelte/store"
import { componentStore } from "stores/builder"
const getContextMenuItems = (component, componentCollapsed) => {
const definition = componentStore.getDefinition(component?._component)
const noPaste = !get(componentStore).componentToPaste
const isBlock = definition?.block === true
const canEject = !(definition?.ejectable === false)
const hasChildren = component?._children?.length
const keyboardEvent = (key, ctrlKey = false) => {
document.dispatchEvent(
new CustomEvent("component-menu", {
detail: {
key,
ctrlKey,
id: component?._id,
},
})
)
}
return [
{
icon: "Delete",
name: "Delete",
keyBind: "!BackAndroid",
visible: true,
disabled: false,
callback: () => keyboardEvent("Delete"),
},
{
icon: "ChevronUp",
name: "Move up",
keyBind: "Ctrl+!ArrowUp",
visible: true,
disabled: false,
callback: () => keyboardEvent("ArrowUp", true),
},
{
icon: "ChevronDown",
name: "Move down",
keyBind: "Ctrl+!ArrowDown",
visible: true,
disabled: false,
callback: () => keyboardEvent("ArrowDown", true),
},
{
icon: "Duplicate",
name: "Duplicate",
keyBind: "Ctrl+D",
visible: true,
disabled: false,
callback: () => keyboardEvent("d", true),
},
{
icon: "Cut",
name: "Cut",
keyBind: "Ctrl+X",
visible: true,
disabled: false,
callback: () => keyboardEvent("x", true),
},
{
icon: "Copy",
name: "Copy",
keyBind: "Ctrl+C",
visible: true,
disabled: false,
callback: () => keyboardEvent("c", true),
},
{
icon: "LayersSendToBack",
name: "Paste",
keyBind: "Ctrl+V",
visible: true,
disabled: noPaste,
callback: () => keyboardEvent("v", true),
},
{
icon: "Export",
name: "Eject block",
keyBind: "Ctrl+E",
visible: isBlock && canEject,
disabled: false,
callback: () => keyboardEvent("e", true),
},
{
icon: "TreeExpand",
name: "Expand",
keyBind: "!ArrowRight",
visible: hasChildren,
disabled: !componentCollapsed,
callback: () => keyboardEvent("ArrowRight", false),
},
{
icon: "TreeExpandAll",
name: "Expand All",
keyBind: "Ctrl+!ArrowRight",
visible: hasChildren,
disabled: !componentCollapsed,
callback: () => keyboardEvent("ArrowRight", true),
},
{
icon: "TreeCollapse",
name: "Collapse",
keyBind: "!ArrowLeft",
visible: hasChildren,
disabled: componentCollapsed,
callback: () => keyboardEvent("ArrowLeft", false),
},
{
icon: "TreeCollapseAll",
name: "Collapse All",
keyBind: "Ctrl+!ArrowLeft",
visible: hasChildren,
disabled: componentCollapsed,
callback: () => keyboardEvent("ArrowLeft", true),
},
]
}
export default getContextMenuItems

View File

@ -0,0 +1,40 @@
import { get } from "svelte/store"
import { componentStore } from "stores/builder"
import { notifications } from "@budibase/bbui"
const getContextMenuItems = (component, showCopy) => {
const noPaste = !get(componentStore).componentToPaste
const storeComponentForCopy = (cut = false) => {
componentStore.copy(component, cut)
}
const pasteComponent = mode => {
try {
componentStore.paste(component, mode)
} catch (error) {
notifications.error("Error saving component")
}
}
return [
{
icon: "Copy",
name: "Copy",
keyBind: "Ctrl+C",
visible: showCopy,
disabled: false,
callback: () => storeComponentForCopy(false),
},
{
icon: "LayersSendToBack",
name: "Paste",
keyBind: "Ctrl+V",
visible: true,
disabled: noPaste,
callback: () => pasteComponent("inside"),
},
]
}
export default getContextMenuItems

View File

@ -7,14 +7,15 @@
componentStore,
userSelectedResourceMap,
hoverStore,
contextMenuStore,
} from "stores/builder"
import NavItem from "components/common/NavItem.svelte"
import ComponentTree from "./ComponentTree.svelte"
import { dndStore, DropPosition } from "./dndStore.js"
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
import getScreenContextMenuItems from "./getScreenContextMenuItems"
let scrolling = false
@ -43,6 +44,32 @@
}
const hover = hoverStore.hover
// showCopy is used to hide the copy button when the user right-clicks the empty
// background of their component tree. Pasting in the empty space makes sense,
// but copying it doesn't
const openScreenContextMenu = (e, showCopy) => {
const screenComponent = $selectedScreen?.props
const definition = componentStore.getDefinition(screenComponent?._component)
// "editable" has been repurposed for inline text editing.
// It remains here for legacy compatibility.
// Future components should define "static": true for indicate they should
// not show a context menu.
if (definition?.editable !== false && definition?.static !== true) {
e.preventDefault()
e.stopPropagation()
const items = getScreenContextMenuItems(screenComponent, showCopy)
contextMenuStore.open(
`${showCopy ? "background-" : ""}screenComponent._id`,
items,
{
x: e.clientX,
y: e.clientY,
}
)
}
}
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
@ -56,8 +83,11 @@
</div>
<div class="list-panel">
<ComponentScrollWrapper on:scroll={handleScroll}>
<ul>
<li>
<ul
class="componentTree"
on:contextmenu={e => openScreenContextMenu(e, false)}
>
<li on:contextmenu={e => openScreenContextMenu(e, true)}>
<NavItem
text="Screen"
indentLevel={0}
@ -70,14 +100,22 @@
on:click={() => {
componentStore.select(`${$screenStore.selectedScreenId}-screen`)
}}
hovering={$hoverStore.componentId === screenComponentId}
hovering={$hoverStore.componentId === screenComponentId ||
$selectedScreen?.props._id === $contextMenuStore.id}
on:mouseenter={() => hover(screenComponentId)}
on:mouseleave={() => hover(null)}
id="component-screen"
selectedBy={$userSelectedResourceMap[screenComponentId]}
>
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
<Icon
size="S"
hoverable
name="MoreSmallList"
on:click={e => openScreenContextMenu(e, $selectedScreen?.props)}
/>
</NavItem>
</li>
<li on:contextmenu|stopPropagation>
<NavItem
text="Navigation"
indentLevel={0}
@ -165,6 +203,10 @@
flex: 1;
}
.componentTree {
min-height: 100%;
}
ul {
list-style: none;
padding-left: 0;

View File

@ -1,39 +1,25 @@
<script>
import { screenStore, componentStore, navigationStore } from "stores/builder"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { Modal, Helpers, notifications, Icon } from "@budibase/bbui"
import {
ActionMenu,
MenuItem,
Icon,
Modal,
Helpers,
notifications,
} from "@budibase/bbui"
navigationStore,
screenStore,
userSelectedResourceMap,
contextMenuStore,
componentStore,
} from "stores/builder"
import NavItem from "components/common/NavItem.svelte"
import RoleIndicator from "./RoleIndicator.svelte"
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
import sanitizeUrl from "helpers/sanitizeUrl"
import { makeComponentUnique } from "helpers/components"
import { capitalise } from "helpers"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
export let screenId
export let screen
let confirmDeleteDialog
let screenDetailsModal
$: screen = $screenStore.screens.find(screen => screen._id === screenId)
$: noPaste = !$componentStore.componentToPaste
const pasteComponent = mode => {
try {
componentStore.paste(screen.props, mode, screen)
} catch (error) {
notifications.error("Error saving component")
}
}
const duplicateScreen = () => {
screenDetailsModal.show()
}
const createDuplicateScreen = async ({ screenName, screenUrl }) => {
// Create a dupe and ensure it is unique
let duplicateScreen = Helpers.cloneDeep(screen)
@ -69,22 +55,75 @@
notifications.error("Error deleting screen")
}
}
$: noPaste = !$componentStore.componentToPaste
const pasteComponent = mode => {
try {
componentStore.paste(screen.props, mode, screen)
} catch (error) {
notifications.error("Error saving component")
}
}
const openContextMenu = (e, screen) => {
e.preventDefault()
e.stopPropagation()
const items = [
{
icon: "ShowOneLayer",
name: "Paste inside",
keyBind: null,
visible: true,
disabled: noPaste,
callback: () => pasteComponent("inside"),
},
{
icon: "Duplicate",
name: "Duplicate",
keyBind: null,
visible: true,
disabled: false,
callback: screenDetailsModal.show,
},
{
icon: "Delete",
name: "Delete",
keyBind: null,
visible: true,
disabled: false,
callback: confirmDeleteDialog.show,
},
]
contextMenuStore.open(screen._id, items, { x: e.clientX, y: e.clientY })
}
</script>
<ActionMenu>
<div slot="control" class="icon">
<Icon size="S" hoverable name="MoreSmallList" />
<NavItem
on:contextmenu={e => openContextMenu(e, screen)}
scrollable
icon={screen.routing.homeScreen ? "Home" : null}
indentLevel={0}
selected={$screenStore.selectedScreenId === screen._id}
hovering={screen._id === $contextMenuStore.id}
text={screen.routing.route}
on:click={() => screenStore.select(screen._id)}
rightAlignIcon
showTooltip
selectedBy={$userSelectedResourceMap[screen._id]}
>
<Icon
on:click={e => openContextMenu(e, screen)}
size="S"
hoverable
name="MoreSmallList"
/>
<div slot="icon" class="icon">
<RoleIndicator roleId={screen.routing.roleId} />
</div>
<MenuItem
icon="ShowOneLayer"
on:click={() => pasteComponent("inside")}
disabled={noPaste}
>
Paste inside
</MenuItem>
<MenuItem icon="Duplicate" on:click={duplicateScreen}>Duplicate</MenuItem>
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
</ActionMenu>
</NavItem>
<ConfirmDialog
bind:this={confirmDeleteDialog}
@ -105,7 +144,7 @@
<style>
.icon {
display: grid;
place-items: center;
margin-left: 4px;
margin-right: 4px;
}
</style>

View File

@ -1,13 +1,7 @@
<script>
import { Layout } from "@budibase/bbui"
import {
screenStore,
sortedScreens,
userSelectedResourceMap,
} from "stores/builder"
import NavItem from "components/common/NavItem.svelte"
import RoleIndicator from "./RoleIndicator.svelte"
import DropdownMenu from "./DropdownMenu.svelte"
import { sortedScreens } from "stores/builder"
import ScreenNavItem from "./ScreenNavItem.svelte"
import { goto } from "@roxi/routify"
import { getVerticalResizeActions } from "components/common/resizable"
import NavHeader from "components/common/NavHeader.svelte"
@ -55,22 +49,7 @@
<div on:scroll={handleScroll} bind:this={screensContainer} class="content">
{#if filteredScreens?.length}
{#each filteredScreens as screen (screen._id)}
<NavItem
scrollable
icon={screen.routing.homeScreen ? "Home" : null}
indentLevel={0}
selected={$screenStore.selectedScreenId === screen._id}
text={screen.routing.route}
on:click={() => screenStore.select(screen._id)}
rightAlignIcon
showTooltip
selectedBy={$userSelectedResourceMap[screen._id]}
>
<DropdownMenu screenId={screen._id} />
<div slot="icon" class="icon">
<RoleIndicator roleId={screen.routing.roleId} />
</div>
</NavItem>
<ScreenNavItem {screen} />
{/each}
{:else}
<Layout paddingY="none" paddingX="L">
@ -129,11 +108,6 @@
padding-right: 8px !important;
}
.icon {
margin-left: 4px;
margin-right: 4px;
}
.no-results {
color: var(--spectrum-global-color-gray-600);
}

View File

@ -7,7 +7,8 @@
sideBarCollapsed,
enrichedApps,
} from "stores/portal"
import AppRowContext from "components/start/AppRowContext.svelte"
import AppContextMenuModals from "components/start/AppContextMenuModals.svelte"
import getAppContextMenuItems from "components/start/getAppContextMenuItems.js"
import FavouriteAppButton from "../FavouriteAppButton.svelte"
import {
Link,
@ -21,12 +22,14 @@
import { API } from "api"
import ErrorSVG from "./ErrorSVG.svelte"
import { getBaseTheme, ClientAppSkeleton } from "@budibase/frontend-core"
import { contextMenuStore } from "stores/builder"
$: app = $enrichedApps.find(app => app.appId === $params.appId)
$: iframeUrl = getIframeURL(app)
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
let loading = true
let appContextMenuModals
const getIframeURL = app => {
loading = true
@ -62,6 +65,24 @@
onDestroy(() => {
window.removeEventListener("message", receiveMessage)
})
const openContextMenu = e => {
e.preventDefault()
e.stopPropagation()
const items = getAppContextMenuItems({
app,
onDuplicate: appContextMenuModals.showDuplicateModal,
onExportDev: appContextMenuModals.showExportDevModal,
onExportProd: appContextMenuModals.showExportProdModal,
onDelete: appContextMenuModals.showDeleteModal,
})
contextMenuStore.open(`${app.appId}-view`, items, {
x: e.clientX,
y: e.clientY,
})
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
@ -116,10 +137,15 @@
size="S"
/>
</div>
<AppRowContext
{app}
options={["duplicate", "delete", "exportDev", "exportProd"]}
align="left"
<Icon
color={`${app.appId}-view` === $contextMenuStore.id
? "var(--hover-color)"
: null}
on:contextmenu={openContextMenu}
on:click={openContextMenu}
size="S"
hoverable
name="MoreSmallList"
/>
</div>
{#if noScreens}
@ -155,6 +181,7 @@
/>
{/if}
</div>
<AppContextMenuModals {app} bind:this={appContextMenuModals} />
<style>
.headerButton {

View File

@ -0,0 +1,91 @@
<script>
import { auth } from "stores/portal"
import { params, goto } from "@roxi/routify"
import NavItem from "components/common/NavItem.svelte"
import AppContextMenuModals from "components/start/AppContextMenuModals.svelte"
import getAppContextMenuItems from "components/start/getAppContextMenuItems.js"
import FavouriteAppButton from "../FavouriteAppButton.svelte"
import { sdk } from "@budibase/shared-core"
import { Icon } from "@budibase/bbui"
import { contextMenuStore } from "stores/builder"
export let app
let opened
let appContextMenuModals
$: contextMenuOpen = `${app.appId}-sideBar` === $contextMenuStore.id
const openContextMenu = (e, app) => {
e.preventDefault()
e.stopPropagation()
const items = getAppContextMenuItems({
app,
onDuplicate: appContextMenuModals.showDuplicateModal,
onExportDev: appContextMenuModals.showExportDevModal,
onExportProd: appContextMenuModals.showExportProdModal,
onDelete: appContextMenuModals.showDeleteModal,
})
contextMenuStore.open(`${app.appId}-sideBar`, items, {
x: e.clientX,
y: e.clientY,
})
}
</script>
<span
class="side-bar-app-entry"
class:favourite={app.favourite}
class:actionsOpen={opened == app.appId || contextMenuOpen}
>
<NavItem
on:contextmenu={e => openContextMenu(e, app)}
text={app.name}
icon={app.icon?.name || "Apps"}
iconColor={app.icon?.color}
selected={$params.appId === app.appId}
hovering={contextMenuOpen}
highlighted={opened == app.appId}
on:click={() => $goto(`./${app.appId}`)}
withActions
showActions
>
<div class="app-entry-actions">
{#if sdk.users.isBuilder($auth.user, app?.devId)}
<Icon
on:click={e => openContextMenu(e, app)}
size="S"
hoverable
name="MoreSmallList"
/>
{/if}
</div>
<div class="favourite-icon">
<FavouriteAppButton {app} size="XS" />
</div>
</NavItem>
</span>
<AppContextMenuModals {app} bind:this={appContextMenuModals} />
<style>
.side-bar-app-entry :global(.nav-item-content .actions) {
width: auto;
display: flex;
gap: var(--spacing-s);
}
.side-bar-app-entry:hover .app-entry-actions,
.side-bar-app-entry:hover .favourite-icon,
.side-bar-app-entry.favourite .favourite-icon,
.side-bar-app-entry.actionsOpen .app-entry-actions,
.side-bar-app-entry.actionsOpen .favourite-icon {
opacity: 1;
}
.side-bar-app-entry .app-entry-actions,
.side-bar-app-entry .favourite-icon {
opacity: 0;
}
</style>

View File

@ -1,11 +1,9 @@
<script>
import { sideBarCollapsed, enrichedApps, auth } from "stores/portal"
import { sideBarCollapsed, enrichedApps } from "stores/portal"
import { params, goto } from "@roxi/routify"
import NavItem from "components/common/NavItem.svelte"
import NavHeader from "components/common/NavHeader.svelte"
import AppRowContext from "components/start/AppRowContext.svelte"
import FavouriteAppButton from "../FavouriteAppButton.svelte"
import { sdk } from "@budibase/shared-core"
import AppNavItem from "./AppNavItem.svelte"
let searchString
let opened
@ -40,34 +38,7 @@
class:favourite={app.favourite}
class:actionsOpen={opened == app.appId}
>
<NavItem
text={app.name}
icon={app.icon?.name || "Apps"}
iconColor={app.icon?.color}
selected={$params.appId === app.appId}
highlighted={opened == app.appId}
on:click={() => $goto(`./${app.appId}`)}
withActions
showActions
>
<div class="app-entry-actions">
{#if sdk.users.isBuilder($auth.user, app?.devId)}
<AppRowContext
{app}
align="left"
on:open={() => {
opened = app.appId
}}
on:close={() => {
opened = null
}}
/>
{/if}
</div>
<div class="favourite-icon">
<FavouriteAppButton {app} size="XS" />
</div>
</NavItem>
<AppNavItem {app} />
</span>
{/each}
</div>
@ -117,17 +88,4 @@
display: flex;
gap: var(--spacing-s);
}
.side-bar-app-entry:hover .app-entry-actions,
.side-bar-app-entry:hover .favourite-icon,
.side-bar-app-entry.favourite .favourite-icon,
.side-bar-app-entry.actionsOpen .app-entry-actions,
.side-bar-app-entry.actionsOpen .favourite-icon {
opacity: 1;
}
.side-bar-app-entry .app-entry-actions,
.side-bar-app-entry .favourite-icon {
opacity: 0;
}
</style>

View File

@ -0,0 +1,28 @@
import { writable } from "svelte/store"
export const INITIAL_CONTEXT_MENU_STATE = {
id: null,
items: [],
position: { x: 0, y: 0 },
visible: false,
}
export function createViewsStore() {
const store = writable({ ...INITIAL_CONTEXT_MENU_STATE })
const open = (id, items, position) => {
store.set({ id, items, position, visible: true })
}
const close = () => {
store.set({ ...INITIAL_CONTEXT_MENU_STATE })
}
return {
subscribe: store.subscribe,
open,
close,
}
}
export const contextMenuStore = createViewsStore()

View File

@ -14,6 +14,7 @@ import {
} from "./automations.js"
import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js"
import { deploymentStore } from "./deployments.js"
import { contextMenuStore } from "./contextMenu.js"
import { snippets } from "./snippets"
// Backend
@ -48,6 +49,7 @@ export {
userStore,
isOnlyUser,
deploymentStore,
contextMenuStore,
selectedComponent,
tables,
views,

View File

@ -28,11 +28,9 @@ export class ScreenStore extends BudiStore {
this.reset = this.reset.bind(this)
this.syncAppScreens = this.syncAppScreens.bind(this)
this.validate = this.validate.bind(this)
this.save = this.save.bind(this)
this.patch = this.patch.bind(this)
this.replace = this.replace.bind(this)
this.saveScreen = this.saveScreen.bind(this)
this.delete = this.delete.bind(this)
this.deleteScreen = this.deleteScreen.bind(this)
this.syncScreenData = this.syncScreenData.bind(this)
this.updateSetting = this.updateSetting.bind(this)
@ -52,6 +50,9 @@ export class ScreenStore extends BudiStore {
}
},
})
this.delete = this.history.wrapDeleteDoc(this.deleteScreen)
this.save = this.history.wrapSaveDoc(this.saveScreen)
}
/**
@ -382,25 +383,6 @@ export class ScreenStore extends BudiStore {
return null
}
/**
* {@link deleteScreen} wrapped to enable history tracking
* @param {object | array} screen
*
*/
async delete(screens_x) {
const wrappedFn = this.history.wrapDeleteDoc(this.deleteScreen)
return wrappedFn(screens_x)
}
/**
* {@link saveScreen} wrapped to enable history tracking
* @param {object} screen
*/
async save(screen) {
const wrappedFn = this.history.wrapSaveDoc(this.saveScreen)
return wrappedFn(screen)
}
/**
* Update a screen by deep setting a property value by name
*

View File

@ -3,7 +3,7 @@
import { getContext, onDestroy } from "svelte"
import { ModalContent, Modal } from "@budibase/bbui"
import FilterModal from "./FilterModal.svelte"
import { QueryUtils } from "@budibase/frontend-core"
import { QueryUtils, Constants } from "@budibase/frontend-core"
import Button from "../Button.svelte"
export let dataProvider
@ -22,6 +22,7 @@
$: dataProviderId = dataProvider?.id
$: datasource = dataProvider?.datasource
$: isDSPlus = ["table", "link", "viewV2"].includes(datasource?.type)
$: addExtension = getAction(
dataProviderId,
ActionTypes.AddDataProviderQueryExtension
@ -69,7 +70,10 @@
}
})
}
return Object.values(clonedSchema || {})
.filter(field => !Constants.BannedSearchTypes.includes(field.type))
.concat(isDSPlus ? [{ name: "_id", type: "string" }] : [])
}
const openEditor = () => {
@ -94,7 +98,7 @@
onClick={openEditor}
icon="ri-filter-3-line"
text="Filter"
size="XL"
{size}
type="secondary"
quiet
active={filters?.length > 0}

View File

@ -36,11 +36,7 @@
) &&
!schemaFields.some(field => field.name === "_id")
) {
schemaFields = [
...schemaFields,
{ name: "_id", type: "string" },
{ name: "_rev", type: "string" },
]
schemaFields = [...schemaFields, { name: "_id", type: "string" }]
}
}

View File

@ -16,6 +16,7 @@ export const BannedSearchTypes = [
FieldType.ATTACHMENTS,
FieldType.FORMULA,
FieldType.JSON,
FieldType.ATTACHMENT_SINGLE,
FieldType.SIGNATURE_SINGLE,
"jsonarray",
"queryarray",