1
0
Fork 0
mirror of synced 2024-09-20 11:27:56 +12:00

Allow Opening Certain Context Menus With Right Click (#14169)

* Allow Opening NavItem Context Menus With Right Click

* dean pr feedback

* PR Feedback 1

* Fix pasting into a component issue

* Remove animation

* Move ContextMenu Into Routify Router Scope
This commit is contained in:
Gerard Burns 2024-07-22 09:27:44 +01:00 committed by GitHub
parent 8633fad7f4
commit 7548b48f9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1287 additions and 912 deletions

View file

@ -29,6 +29,7 @@
> >
<div class="icon" class:newStyles> <div class="icon" class:newStyles>
<svg <svg
on:contextmenu
on:click on:click
class:hoverable class:hoverable
class:disabled 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 { Modal, notifications, Layout } from "@budibase/bbui"
import NavHeader from "components/common/NavHeader.svelte" import NavHeader from "components/common/NavHeader.svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
import { import { automationStore } from "stores/builder"
automationStore, import AutomationNavItem from "./AutomationNavItem.svelte"
selectedAutomation,
userSelectedResourceMap,
} from "stores/builder"
import NavItem from "components/common/NavItem.svelte"
import EditAutomationPopover from "./EditAutomationPopover.svelte"
export let modal export let modal
export let webhookModal export let webhookModal
let searchString let searchString
$: selectedAutomationId = $selectedAutomation?._id
$: filteredAutomations = $automationStore.automations $: filteredAutomations = $automationStore.automations
.filter(automation => { .filter(automation => {
return ( return (
@ -49,10 +42,6 @@
notifications.error("Error getting automations list") notifications.error("Error getting automations list")
} }
}) })
function selectAutomation(id) {
automationStore.actions.select(id)
}
</script> </script>
<div class="side-bar"> <div class="side-bar">
@ -71,17 +60,7 @@
{triggerGroup?.name} {triggerGroup?.name}
</div> </div>
{#each triggerGroup.entries as automation} {#each triggerGroup.entries as automation}
<NavItem <AutomationNavItem {automation} icon={triggerGroup.icon} />
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>
{/each} {/each}
</div> </div>
{/each} {/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

@ -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> <script>
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { datasources } from "stores/builder" 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 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 export let datasource
let confirmDeleteDialog let confirmDeleteDialog
let updateDatasourceDialog
export const show = () => {
confirmDeleteDialog.show()
}
async function deleteDatasource() { async function deleteDatasource() {
try { try {
@ -25,16 +26,6 @@
} }
</script> </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 <ConfirmDialog
bind:this={confirmDeleteDialog} bind:this={confirmDeleteDialog}
okText="Delete Datasource" okText="Delete Datasource"
@ -45,13 +36,3 @@
<i>{datasource.name}?</i> <i>{datasource.name}?</i>
This action cannot be undone. This action cannot be undone.
</ConfirmDialog> </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> <script>
import { goto, isActive, params } from "@roxi/routify" import { goto, isActive, params } from "@roxi/routify"
import { Layout } from "@budibase/bbui" import { Layout } from "@budibase/bbui"
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
import { import {
datasources, datasources,
queries, queries,
@ -10,16 +9,10 @@
viewsV2, viewsV2,
userSelectedResourceMap, userSelectedResourceMap,
} from "stores/builder" } from "stores/builder"
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte" import QueryNavItem from "./QueryNavItem.svelte"
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte" import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
import { import DatasourceNavItem from "./DatasourceNavItem/DatasourceNavItem.svelte"
customQueryIconText,
customQueryIconColor,
customQueryText,
} from "helpers/data/utils"
import IntegrationIcon from "./IntegrationIcon.svelte"
import { TableNames } from "constants" import { TableNames } from "constants"
import { enrichDatasources } from "./datasourceUtils" import { enrichDatasources } from "./datasourceUtils"
import { onMount } from "svelte" import { onMount } from "svelte"
@ -86,44 +79,15 @@
/> />
{/if} {/if}
{#each enrichedDataSources.filter(ds => ds.show) as datasource} {#each enrichedDataSources.filter(ds => ds.show) as datasource}
<NavItem <DatasourceNavItem
border {datasource}
text={datasource.name}
opened={datasource.open}
selected={$isActive("./datasource") && datasource.selected}
withArrow={true}
on:click={() => selectDatasource(datasource)} on:click={() => selectDatasource(datasource)}
on:iconClick={() => toggleNode(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} {#if datasource.open}
<TableNavigator tables={datasource.tables} {selectTable} /> <TableNavigator tables={datasource.tables} {selectTable} />
{#each datasource.queries as query} {#each datasource.queries as query}
<NavItem <QueryNavItem {datasource} {query} />
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>
{/each} {/each}
{/if} {/if}
{/each} {/each}
@ -140,11 +104,6 @@
.hierarchy-items-container { .hierarchy-items-container {
margin: 0 calc(-1 * var(--spacing-l)); margin: 0 calc(-1 * var(--spacing-l));
} }
.datasource-icon {
display: grid;
place-items: center;
flex: 0 0 24px;
}
.no-results { .no-results {
color: var(--spectrum-global-color-gray-600); 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> <script>
import { goto, params } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { cloneDeep } from "lodash/fp"
import { tables, datasources, screenStore } from "stores/builder" import { tables, datasources, screenStore } from "stores/builder"
import { import { Input, notifications } from "@budibase/bbui"
ActionMenu,
Icon,
Input,
MenuItem,
Modal,
ModalContent,
notifications,
} from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { DB_TYPE_EXTERNAL } from "constants/backend" import { DB_TYPE_EXTERNAL } from "constants/backend"
export let table export let table
let editorModal, editTableNameModal
let confirmDeleteDialog let confirmDeleteDialog
let error = ""
let originalName export const show = () => {
let updatedName
let templateScreens
let willBeDeleted
let deleteTableName
$: externalTable = table?.sourceType === DB_TYPE_EXTERNAL
function showDeleteModal() {
templateScreens = $screenStore.screens.filter( templateScreens = $screenStore.screens.filter(
screen => screen.autoTableId === table._id screen => screen.autoTableId === table._id
) )
@ -39,6 +19,10 @@
confirmDeleteDialog.show() confirmDeleteDialog.show()
} }
let templateScreens
let willBeDeleted
let deleteTableName
async function deleteTable() { async function deleteTable() {
const isSelected = $params.tableId === table._id const isSelected = $params.tableId === table._id
try { try {
@ -62,58 +46,8 @@
function hideDeleteDialog() { function hideDeleteDialog() {
deleteTableName = "" 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> </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 <ConfirmDialog
bind:this={confirmDeleteDialog} bind:this={confirmDeleteDialog}
okText="Delete Table" okText="Delete Table"
@ -142,13 +76,6 @@
</ConfirmDialog> </ConfirmDialog>
<style> <style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
div.delete-items { div.delete-items {
margin-top: 10px; margin-top: 10px;
margin-bottom: 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> <script>
import { import { goto } from "@roxi/routify"
tables as tablesStore, import TableNavItem from "./TableNavItem/TableNavItem.svelte"
views, import ViewNavItem from "./ViewNavItem/ViewNavItem.svelte"
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"
export let tables export let tables
export let selectTable export let selectTable
@ -19,37 +11,15 @@
const alphabetical = (a, b) => { const alphabetical = (a, b) => {
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1 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> </script>
<div class="hierarchy-items-container"> <div class="hierarchy-items-container">
{#each sortedTables as table, idx} {#each sortedTables as table, idx}
<NavItem <TableNavItem {table} {idx} on:click={() => selectTable(table._id)} />
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>
{#each [...Object.entries(table.views || {})].sort() as [name, view], idx (idx)} {#each [...Object.entries(table.views || {})].sort() as [name, view], idx (idx)}
<NavItem <ViewNavItem
indentLevel={2} {view}
icon="Remove" {name}
text={name}
selected={isViewActive(view, $isActive, $views, $viewsV2)}
on:click={() => { on:click={() => {
if (view.version === 2) { if (view.version === 2) {
$goto(`./view/v2/${encodeURIComponent(view.id)}`) $goto(`./view/v2/${encodeURIComponent(view.id)}`)
@ -57,11 +27,7 @@
$goto(`./view/v1/${encodeURIComponent(name)}`) $goto(`./view/v1/${encodeURIComponent(name)}`)
} }
}} }}
selectedBy={$userSelectedResourceMap[name] || />
$userSelectedResourceMap[view.id]}
>
<EditViewPopover {view} />
</NavItem>
{/each} {/each}
{/each} {/each}
</div> </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:mouseenter
on:mouseleave on:mouseleave
on:click={onClick} on:click={onClick}
on:contextmenu
ondragover="return false" ondragover="return false"
ondragenter="return false" ondragenter="return false"
{id} {id}

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

@ -5,6 +5,7 @@
import { CookieUtils, Constants } from "@budibase/frontend-core" import { CookieUtils, Constants } from "@budibase/frontend-core"
import { API } from "api" import { API } from "api"
import Branding from "./Branding.svelte" import Branding from "./Branding.svelte"
import ContextMenu from "components/ContextMenu.svelte"
let loaded = false let loaded = false
@ -160,6 +161,7 @@
<!--Portal branding overrides --> <!--Portal branding overrides -->
<Branding /> <Branding />
<ContextMenu />
{#if loaded} {#if loaded}
<slot /> <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> <script>
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import { notifications } from "@budibase/bbui" import { Icon, notifications } from "@budibase/bbui"
import { import {
selectedScreen, selectedScreen,
componentStore, componentStore,
@ -9,6 +8,7 @@
selectedComponent, selectedComponent,
hoverStore, hoverStore,
componentTreeNodesStore, componentTreeNodesStore,
contextMenuStore,
} from "stores/builder" } from "stores/builder"
import { import {
findComponentPath, findComponentPath,
@ -17,6 +17,7 @@
} from "helpers/components" } from "helpers/components"
import { get } from "svelte/store" import { get } from "svelte/store"
import { dndStore } from "./dndStore" import { dndStore } from "./dndStore"
import getComponentContextMenuItems from "./getComponentContextMenuItems"
export let components = [] export let components = []
export let level = 0 export let level = 0
@ -85,6 +86,18 @@
} }
const hover = hoverStore.hover 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> </script>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions--> <!-- svelte-ignore a11y-no-noninteractive-element-interactions-->
@ -93,6 +106,7 @@
{#each filteredComponents || [] as component, index (component._id)} {#each filteredComponents || [] as component, index (component._id)}
{@const opened = isOpen(component, openNodes)} {@const opened = isOpen(component, openNodes)}
<li <li
on:contextmenu={e => openContextMenu(e, component, opened)}
on:click|stopPropagation={() => { on:click|stopPropagation={() => {
componentStore.select(component._id) componentStore.select(component._id)
}} }}
@ -107,7 +121,8 @@
on:dragover={dragover(component, index)} on:dragover={dragover(component, index)}
on:iconClick={() => handleIconClick(component._id)} on:iconClick={() => handleIconClick(component._id)}
on:drop={onDrop} on:drop={onDrop}
hovering={$hoverStore.componentId === component._id} hovering={$hoverStore.componentId === component._id ||
component._id === $contextMenuStore.id}
on:mouseenter={() => hover(component._id)} on:mouseenter={() => hover(component._id)}
on:mouseleave={() => hover(null)} on:mouseleave={() => hover(null)}
text={getComponentText(component)} text={getComponentText(component)}
@ -120,7 +135,12 @@
highlighted={isChildOfSelectedComponent(component)} highlighted={isChildOfSelectedComponent(component)}
selectedBy={$userSelectedResourceMap[component._id]} selectedBy={$userSelectedResourceMap[component._id]}
> >
<ComponentDropdownMenu {opened} {component} /> <Icon
size="S"
hoverable
name="MoreSmallList"
on:click={e => openContextMenu(e, component, opened)}
/>
</NavItem> </NavItem>
{#if opened} {#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, componentStore,
userSelectedResourceMap, userSelectedResourceMap,
hoverStore, hoverStore,
contextMenuStore,
} from "stores/builder" } from "stores/builder"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import ComponentTree from "./ComponentTree.svelte" import ComponentTree from "./ComponentTree.svelte"
import { dndStore, DropPosition } from "./dndStore.js" import { dndStore, DropPosition } from "./dndStore.js"
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
import DNDPositionIndicator from "./DNDPositionIndicator.svelte" import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
import ComponentKeyHandler from "./ComponentKeyHandler.svelte" import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte" import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
import getScreenContextMenuItems from "./getScreenContextMenuItems"
let scrolling = false let scrolling = false
@ -43,6 +44,32 @@
} }
const hover = hoverStore.hover 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> </script>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
@ -56,8 +83,11 @@
</div> </div>
<div class="list-panel"> <div class="list-panel">
<ComponentScrollWrapper on:scroll={handleScroll}> <ComponentScrollWrapper on:scroll={handleScroll}>
<ul> <ul
<li> class="componentTree"
on:contextmenu={e => openScreenContextMenu(e, false)}
>
<li on:contextmenu={e => openScreenContextMenu(e, true)}>
<NavItem <NavItem
text="Screen" text="Screen"
indentLevel={0} indentLevel={0}
@ -70,14 +100,22 @@
on:click={() => { on:click={() => {
componentStore.select(`${$screenStore.selectedScreenId}-screen`) componentStore.select(`${$screenStore.selectedScreenId}-screen`)
}} }}
hovering={$hoverStore.componentId === screenComponentId} hovering={$hoverStore.componentId === screenComponentId ||
$selectedScreen?.props._id === $contextMenuStore.id}
on:mouseenter={() => hover(screenComponentId)} on:mouseenter={() => hover(screenComponentId)}
on:mouseleave={() => hover(null)} on:mouseleave={() => hover(null)}
id="component-screen" id="component-screen"
selectedBy={$userSelectedResourceMap[screenComponentId]} selectedBy={$userSelectedResourceMap[screenComponentId]}
> >
<ScreenslotDropdownMenu component={$selectedScreen?.props} /> <Icon
size="S"
hoverable
name="MoreSmallList"
on:click={e => openScreenContextMenu(e, $selectedScreen?.props)}
/>
</NavItem> </NavItem>
</li>
<li on:contextmenu|stopPropagation>
<NavItem <NavItem
text="Navigation" text="Navigation"
indentLevel={0} indentLevel={0}
@ -165,6 +203,10 @@
flex: 1; flex: 1;
} }
.componentTree {
min-height: 100%;
}
ul { ul {
list-style: none; list-style: none;
padding-left: 0; padding-left: 0;

View file

@ -1,39 +1,25 @@
<script> <script>
import { screenStore, componentStore, navigationStore } from "stores/builder" import { Modal, Helpers, notifications, Icon } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { import {
ActionMenu, navigationStore,
MenuItem, screenStore,
Icon, userSelectedResourceMap,
Modal, contextMenuStore,
Helpers, componentStore,
notifications, } from "stores/builder"
} from "@budibase/bbui" import NavItem from "components/common/NavItem.svelte"
import RoleIndicator from "./RoleIndicator.svelte"
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte" import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
import sanitizeUrl from "helpers/sanitizeUrl" import sanitizeUrl from "helpers/sanitizeUrl"
import { makeComponentUnique } from "helpers/components" import { makeComponentUnique } from "helpers/components"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
export let screenId export let screen
let confirmDeleteDialog let confirmDeleteDialog
let screenDetailsModal 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 }) => { const createDuplicateScreen = async ({ screenName, screenUrl }) => {
// Create a dupe and ensure it is unique // Create a dupe and ensure it is unique
let duplicateScreen = Helpers.cloneDeep(screen) let duplicateScreen = Helpers.cloneDeep(screen)
@ -69,22 +55,75 @@
notifications.error("Error deleting screen") 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> </script>
<ActionMenu> <NavItem
<div slot="control" class="icon"> on:contextmenu={e => openContextMenu(e, screen)}
<Icon size="S" hoverable name="MoreSmallList" /> 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> </div>
<MenuItem </NavItem>
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>
<ConfirmDialog <ConfirmDialog
bind:this={confirmDeleteDialog} bind:this={confirmDeleteDialog}
@ -105,7 +144,7 @@
<style> <style>
.icon { .icon {
display: grid; margin-left: 4px;
place-items: center; margin-right: 4px;
} }
</style> </style>

View file

@ -1,13 +1,7 @@
<script> <script>
import { Layout } from "@budibase/bbui" import { Layout } from "@budibase/bbui"
import { import { sortedScreens } from "stores/builder"
screenStore, import ScreenNavItem from "./ScreenNavItem.svelte"
sortedScreens,
userSelectedResourceMap,
} from "stores/builder"
import NavItem from "components/common/NavItem.svelte"
import RoleIndicator from "./RoleIndicator.svelte"
import DropdownMenu from "./DropdownMenu.svelte"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { getVerticalResizeActions } from "components/common/resizable" import { getVerticalResizeActions } from "components/common/resizable"
import NavHeader from "components/common/NavHeader.svelte" import NavHeader from "components/common/NavHeader.svelte"
@ -55,22 +49,7 @@
<div on:scroll={handleScroll} bind:this={screensContainer} class="content"> <div on:scroll={handleScroll} bind:this={screensContainer} class="content">
{#if filteredScreens?.length} {#if filteredScreens?.length}
{#each filteredScreens as screen (screen._id)} {#each filteredScreens as screen (screen._id)}
<NavItem <ScreenNavItem {screen} />
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>
{/each} {/each}
{:else} {:else}
<Layout paddingY="none" paddingX="L"> <Layout paddingY="none" paddingX="L">
@ -129,11 +108,6 @@
padding-right: 8px !important; padding-right: 8px !important;
} }
.icon {
margin-left: 4px;
margin-right: 4px;
}
.no-results { .no-results {
color: var(--spectrum-global-color-gray-600); color: var(--spectrum-global-color-gray-600);
} }

View file

@ -7,7 +7,8 @@
sideBarCollapsed, sideBarCollapsed,
enrichedApps, enrichedApps,
} from "stores/portal" } 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 FavouriteAppButton from "../FavouriteAppButton.svelte"
import { import {
Link, Link,
@ -21,12 +22,14 @@
import { API } from "api" import { API } from "api"
import ErrorSVG from "./ErrorSVG.svelte" import ErrorSVG from "./ErrorSVG.svelte"
import { getBaseTheme, ClientAppSkeleton } from "@budibase/frontend-core" import { getBaseTheme, ClientAppSkeleton } from "@budibase/frontend-core"
import { contextMenuStore } from "stores/builder"
$: app = $enrichedApps.find(app => app.appId === $params.appId) $: app = $enrichedApps.find(app => app.appId === $params.appId)
$: iframeUrl = getIframeURL(app) $: iframeUrl = getIframeURL(app)
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId) $: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
let loading = true let loading = true
let appContextMenuModals
const getIframeURL = app => { const getIframeURL = app => {
loading = true loading = true
@ -62,6 +65,24 @@
onDestroy(() => { onDestroy(() => {
window.removeEventListener("message", receiveMessage) 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> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
@ -116,10 +137,15 @@
size="S" size="S"
/> />
</div> </div>
<AppRowContext <Icon
{app} color={`${app.appId}-view` === $contextMenuStore.id
options={["duplicate", "delete", "exportDev", "exportProd"]} ? "var(--hover-color)"
align="left" : null}
on:contextmenu={openContextMenu}
on:click={openContextMenu}
size="S"
hoverable
name="MoreSmallList"
/> />
</div> </div>
{#if noScreens} {#if noScreens}
@ -155,6 +181,7 @@
/> />
{/if} {/if}
</div> </div>
<AppContextMenuModals {app} bind:this={appContextMenuModals} />
<style> <style>
.headerButton { .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> <script>
import { sideBarCollapsed, enrichedApps, auth } from "stores/portal" import { sideBarCollapsed, enrichedApps } from "stores/portal"
import { params, goto } from "@roxi/routify" import { params, goto } from "@roxi/routify"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import NavHeader from "components/common/NavHeader.svelte" import NavHeader from "components/common/NavHeader.svelte"
import AppRowContext from "components/start/AppRowContext.svelte" import AppNavItem from "./AppNavItem.svelte"
import FavouriteAppButton from "../FavouriteAppButton.svelte"
import { sdk } from "@budibase/shared-core"
let searchString let searchString
let opened let opened
@ -40,34 +38,7 @@
class:favourite={app.favourite} class:favourite={app.favourite}
class:actionsOpen={opened == app.appId} class:actionsOpen={opened == app.appId}
> >
<NavItem <AppNavItem {app} />
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>
</span> </span>
{/each} {/each}
</div> </div>
@ -117,17 +88,4 @@
display: flex; display: flex;
gap: var(--spacing-s); 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> </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" } from "./automations.js"
import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js" import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js"
import { deploymentStore } from "./deployments.js" import { deploymentStore } from "./deployments.js"
import { contextMenuStore } from "./contextMenu.js"
import { snippets } from "./snippets" import { snippets } from "./snippets"
// Backend // Backend
@ -48,6 +49,7 @@ export {
userStore, userStore,
isOnlyUser, isOnlyUser,
deploymentStore, deploymentStore,
contextMenuStore,
selectedComponent, selectedComponent,
tables, tables,
views, views,