Merge branch 'master' of github.com:Budibase/budibase into conditional-table-colours
This commit is contained in:
commit
a14a737921
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
|
@ -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>
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
|
|
@ -79,9 +79,6 @@
|
||||||
|
|
||||||
table = $tables.list.find(table => table._id === tableId)
|
table = $tables.list.find(table => table._id === tableId)
|
||||||
|
|
||||||
if (table) {
|
|
||||||
editableRow["tableId"] = tableId
|
|
||||||
|
|
||||||
schemaFields = Object.entries(table?.schema ?? {})
|
schemaFields = Object.entries(table?.schema ?? {})
|
||||||
.filter(entry => {
|
.filter(entry => {
|
||||||
const [, field] = entry
|
const [, field] = entry
|
||||||
|
@ -91,6 +88,9 @@
|
||||||
return nameA < nameB ? -1 : 1
|
return nameA < nameB ? -1 : 1
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (table) {
|
||||||
|
editableRow["tableId"] = tableId
|
||||||
|
|
||||||
// Parse out any data not in the schema.
|
// Parse out any data not in the schema.
|
||||||
for (const column in editableFields) {
|
for (const column in editableFields) {
|
||||||
if (!Object.hasOwn(table?.schema, column)) {
|
if (!Object.hasOwn(table?.schema, column)) {
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
|
@ -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;
|
|
@ -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>
|
|
@ -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} />
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
/>
|
|
@ -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>
|
|
@ -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} />
|
|
@ -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"
|
|
||||||
/>
|
|
|
@ -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}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { AbsTooltip, Icon, Body } from "@budibase/bbui"
|
import { Icon, Body, TooltipPosition, TooltipType } from "@budibase/bbui"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let icon
|
export let icon
|
||||||
|
@ -18,6 +18,7 @@
|
||||||
export let closeButtonIcon = "Close"
|
export let closeButtonIcon = "Close"
|
||||||
export let noHeaderBorder = false
|
export let noHeaderBorder = false
|
||||||
export let titleCSS = true
|
export let titleCSS = true
|
||||||
|
|
||||||
$: customHeaderContent = $$slots["panel-header-content"]
|
$: customHeaderContent = $$slots["panel-header-content"]
|
||||||
$: customTitleContent = $$slots["panel-title-content"]
|
$: customTitleContent = $$slots["panel-title-content"]
|
||||||
</script>
|
</script>
|
||||||
|
@ -41,9 +42,12 @@
|
||||||
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<AbsTooltip type="info" text={iconTooltip}>
|
<Icon
|
||||||
<Icon name={icon} />
|
name={icon}
|
||||||
</AbsTooltip>
|
tooltipType={TooltipType.Info}
|
||||||
|
tooltip={iconTooltip}
|
||||||
|
tooltipPosition={TooltipPosition.Top}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div class:title={titleCSS}>
|
<div class:title={titleCSS}>
|
||||||
{#if customTitleContent}
|
{#if customTitleContent}
|
||||||
|
|
|
@ -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>
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
|
|
@ -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
|
|
@ -299,15 +299,6 @@ export const PaginationLocations = [
|
||||||
{ label: "Request body", value: "body" },
|
{ label: "Request body", value: "body" },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const BannedSearchTypes = [
|
|
||||||
FieldType.LINK,
|
|
||||||
FieldType.ATTACHMENTS,
|
|
||||||
FieldType.FORMULA,
|
|
||||||
FieldType.JSON,
|
|
||||||
"jsonarray",
|
|
||||||
"queryarray",
|
|
||||||
]
|
|
||||||
|
|
||||||
export const DatasourceTypes = {
|
export const DatasourceTypes = {
|
||||||
RELATIONAL: "Relational",
|
RELATIONAL: "Relational",
|
||||||
NON_RELATIONAL: "Non-relational",
|
NON_RELATIONAL: "Non-relational",
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
</div>
|
icon={screen.routing.homeScreen ? "Home" : null}
|
||||||
<MenuItem
|
indentLevel={0}
|
||||||
icon="ShowOneLayer"
|
selected={$screenStore.selectedScreenId === screen._id}
|
||||||
on:click={() => pasteComponent("inside")}
|
hovering={screen._id === $contextMenuStore.id}
|
||||||
disabled={noPaste}
|
text={screen.routing.route}
|
||||||
|
on:click={() => screenStore.select(screen._id)}
|
||||||
|
rightAlignIcon
|
||||||
|
showTooltip
|
||||||
|
selectedBy={$userSelectedResourceMap[screen._id]}
|
||||||
>
|
>
|
||||||
Paste inside
|
<Icon
|
||||||
</MenuItem>
|
on:click={e => openContextMenu(e, screen)}
|
||||||
<MenuItem icon="Duplicate" on:click={duplicateScreen}>Duplicate</MenuItem>
|
size="S"
|
||||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
hoverable
|
||||||
</ActionMenu>
|
name="MoreSmallList"
|
||||||
|
/>
|
||||||
|
<div slot="icon" class="icon">
|
||||||
|
<RoleIndicator roleId={screen.routing.roleId} />
|
||||||
|
</div>
|
||||||
|
</NavItem>
|
||||||
|
|
||||||
<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>
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
|
@ -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,
|
||||||
|
|
|
@ -28,11 +28,9 @@ export class ScreenStore extends BudiStore {
|
||||||
this.reset = this.reset.bind(this)
|
this.reset = this.reset.bind(this)
|
||||||
this.syncAppScreens = this.syncAppScreens.bind(this)
|
this.syncAppScreens = this.syncAppScreens.bind(this)
|
||||||
this.validate = this.validate.bind(this)
|
this.validate = this.validate.bind(this)
|
||||||
this.save = this.save.bind(this)
|
|
||||||
this.patch = this.patch.bind(this)
|
this.patch = this.patch.bind(this)
|
||||||
this.replace = this.replace.bind(this)
|
this.replace = this.replace.bind(this)
|
||||||
this.saveScreen = this.saveScreen.bind(this)
|
this.saveScreen = this.saveScreen.bind(this)
|
||||||
this.delete = this.delete.bind(this)
|
|
||||||
this.deleteScreen = this.deleteScreen.bind(this)
|
this.deleteScreen = this.deleteScreen.bind(this)
|
||||||
this.syncScreenData = this.syncScreenData.bind(this)
|
this.syncScreenData = this.syncScreenData.bind(this)
|
||||||
this.updateSetting = this.updateSetting.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
|
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
|
* Update a screen by deep setting a property value by name
|
||||||
*
|
*
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { getContext, onDestroy } from "svelte"
|
import { getContext, onDestroy } from "svelte"
|
||||||
import { ModalContent, Modal } from "@budibase/bbui"
|
import { ModalContent, Modal } from "@budibase/bbui"
|
||||||
import FilterModal from "./FilterModal.svelte"
|
import FilterModal from "./FilterModal.svelte"
|
||||||
import { QueryUtils } from "@budibase/frontend-core"
|
import { QueryUtils, Constants } from "@budibase/frontend-core"
|
||||||
import Button from "../Button.svelte"
|
import Button from "../Button.svelte"
|
||||||
|
|
||||||
export let dataProvider
|
export let dataProvider
|
||||||
|
@ -22,6 +22,7 @@
|
||||||
|
|
||||||
$: dataProviderId = dataProvider?.id
|
$: dataProviderId = dataProvider?.id
|
||||||
$: datasource = dataProvider?.datasource
|
$: datasource = dataProvider?.datasource
|
||||||
|
$: isDSPlus = ["table", "link", "viewV2"].includes(datasource?.type)
|
||||||
$: addExtension = getAction(
|
$: addExtension = getAction(
|
||||||
dataProviderId,
|
dataProviderId,
|
||||||
ActionTypes.AddDataProviderQueryExtension
|
ActionTypes.AddDataProviderQueryExtension
|
||||||
|
@ -69,7 +70,10 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.values(clonedSchema || {})
|
return Object.values(clonedSchema || {})
|
||||||
|
.filter(field => !Constants.BannedSearchTypes.includes(field.type))
|
||||||
|
.concat(isDSPlus ? [{ name: "_id", type: "string" }] : [])
|
||||||
}
|
}
|
||||||
|
|
||||||
const openEditor = () => {
|
const openEditor = () => {
|
||||||
|
@ -94,7 +98,7 @@
|
||||||
onClick={openEditor}
|
onClick={openEditor}
|
||||||
icon="ri-filter-3-line"
|
icon="ri-filter-3-line"
|
||||||
text="Filter"
|
text="Filter"
|
||||||
size="XL"
|
{size}
|
||||||
type="secondary"
|
type="secondary"
|
||||||
quiet
|
quiet
|
||||||
active={filters?.length > 0}
|
active={filters?.length > 0}
|
||||||
|
|
|
@ -36,11 +36,7 @@
|
||||||
) &&
|
) &&
|
||||||
!schemaFields.some(field => field.name === "_id")
|
!schemaFields.some(field => field.name === "_id")
|
||||||
) {
|
) {
|
||||||
schemaFields = [
|
schemaFields = [...schemaFields, { name: "_id", type: "string" }]
|
||||||
...schemaFields,
|
|
||||||
{ name: "_id", type: "string" },
|
|
||||||
{ name: "_rev", type: "string" },
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ export const BannedSearchTypes = [
|
||||||
FieldType.ATTACHMENTS,
|
FieldType.ATTACHMENTS,
|
||||||
FieldType.FORMULA,
|
FieldType.FORMULA,
|
||||||
FieldType.JSON,
|
FieldType.JSON,
|
||||||
|
FieldType.ATTACHMENT_SINGLE,
|
||||||
FieldType.SIGNATURE_SINGLE,
|
FieldType.SIGNATURE_SINGLE,
|
||||||
"jsonarray",
|
"jsonarray",
|
||||||
"queryarray",
|
"queryarray",
|
||||||
|
|
Loading…
Reference in New Issue