Merge pull request #4821 from Budibase/copy-paste-improvements
Copy paste improvements + extras
This commit is contained in:
commit
cf9105c9dc
25 changed files with 410 additions and 211 deletions
|
@ -56,6 +56,7 @@
|
||||||
$: if (!loading) loaded = true
|
$: if (!loading) loaded = true
|
||||||
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
|
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
|
||||||
$: rows = fields?.length ? data || [] : []
|
$: rows = fields?.length ? data || [] : []
|
||||||
|
$: totalRowCount = rows?.length || 0
|
||||||
$: visibleRowCount = getVisibleRowCount(
|
$: visibleRowCount = getVisibleRowCount(
|
||||||
loaded,
|
loaded,
|
||||||
height,
|
height,
|
||||||
|
@ -63,7 +64,12 @@
|
||||||
rowCount,
|
rowCount,
|
||||||
rowHeight
|
rowHeight
|
||||||
)
|
)
|
||||||
$: contentStyle = getContentStyle(visibleRowCount, rowCount, rowHeight)
|
$: heightStyle = getHeightStyle(
|
||||||
|
visibleRowCount,
|
||||||
|
rowCount,
|
||||||
|
totalRowCount,
|
||||||
|
rowHeight
|
||||||
|
)
|
||||||
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
|
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
|
||||||
$: gridStyle = getGridStyle(fields, schema, showEditColumn)
|
$: gridStyle = getGridStyle(fields, schema, showEditColumn)
|
||||||
$: showEditColumn = allowEditRows || allowSelectRows
|
$: showEditColumn = allowEditRows || allowSelectRows
|
||||||
|
@ -107,11 +113,16 @@
|
||||||
return Math.min(allRows, Math.ceil(height / rowHeight))
|
return Math.min(allRows, Math.ceil(height / rowHeight))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getContentStyle = (visibleRows, rowCount, rowHeight) => {
|
const getHeightStyle = (
|
||||||
if (!rowCount || !visibleRows) {
|
visibleRowCount,
|
||||||
|
rowCount,
|
||||||
|
totalRowCount,
|
||||||
|
rowHeight
|
||||||
|
) => {
|
||||||
|
if (!rowCount || !visibleRowCount || totalRowCount <= rowCount) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return `height: ${headerHeight + visibleRows * rowHeight}px;`
|
return `height: ${headerHeight + visibleRowCount * rowHeight}px;`
|
||||||
}
|
}
|
||||||
|
|
||||||
const getGridStyle = (fields, schema, showEditColumn) => {
|
const getGridStyle = (fields, schema, showEditColumn) => {
|
||||||
|
@ -264,11 +275,11 @@
|
||||||
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
|
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
|
||||||
>
|
>
|
||||||
{#if !loaded}
|
{#if !loaded}
|
||||||
<div class="loading" style={contentStyle}>
|
<div class="loading" style={heightStyle}>
|
||||||
<ProgressCircle />
|
<ProgressCircle />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="spectrum-Table" style={`${contentStyle}${gridStyle}`}>
|
<div class="spectrum-Table" style={`${heightStyle}${gridStyle}`}>
|
||||||
{#if fields.length}
|
{#if fields.length}
|
||||||
<div class="spectrum-Table-head">
|
<div class="spectrum-Table-head">
|
||||||
{#if showEditColumn}
|
{#if showEditColumn}
|
||||||
|
|
|
@ -247,7 +247,7 @@ Cypress.Commands.add("createScreen", (screenName, route) => {
|
||||||
cy.get("[aria-label=AddCircle]").click()
|
cy.get("[aria-label=AddCircle]").click()
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get(".item").contains("Blank").click()
|
cy.get(".item").contains("Blank").click()
|
||||||
cy.get(".spectrum-Button").contains("Add Screens").click({ force: true })
|
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
@ -265,7 +265,7 @@ Cypress.Commands.add("createAutogeneratedScreens", screenNames => {
|
||||||
for (let i = 0; i < screenNames.length; i++) {
|
for (let i = 0; i < screenNames.length; i++) {
|
||||||
cy.get(".item").contains(screenNames[i]).click()
|
cy.get(".item").contains(screenNames[i]).click()
|
||||||
}
|
}
|
||||||
cy.get(".spectrum-Button").contains("Add Screens").click({ force: true })
|
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
|
||||||
cy.wait(4000)
|
cy.wait(4000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { store } from "./index"
|
import { store } from "./index"
|
||||||
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
decodeJSBinding,
|
||||||
|
encodeJSBinding,
|
||||||
|
findHBSBlocks,
|
||||||
|
} from "@budibase/string-templates"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively searches for a specific component ID
|
* Recursively searches for a specific component ID
|
||||||
|
@ -161,3 +167,58 @@ export const getComponentSettings = componentType => {
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Randomises a components ID's, including all child component IDs, and also
|
||||||
|
* updates all data bindings to still be valid.
|
||||||
|
* This mutates the object in place.
|
||||||
|
* @param component the component to randomise
|
||||||
|
*/
|
||||||
|
export const makeComponentUnique = component => {
|
||||||
|
if (!component) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace component ID
|
||||||
|
const oldId = component._id
|
||||||
|
const newId = Helpers.uuid()
|
||||||
|
component._id = newId
|
||||||
|
|
||||||
|
if (component._children?.length) {
|
||||||
|
let children = JSON.stringify(component._children)
|
||||||
|
|
||||||
|
// Replace all instances of this ID in child HBS bindings
|
||||||
|
children = children.replace(new RegExp(oldId, "g"), newId)
|
||||||
|
|
||||||
|
// Replace all instances of this ID in child JS bindings
|
||||||
|
const bindings = findHBSBlocks(children)
|
||||||
|
bindings.forEach(binding => {
|
||||||
|
// JSON.stringify will have escaped double quotes, so we need
|
||||||
|
// to account for that
|
||||||
|
let sanitizedBinding = binding.replace(/\\"/g, '"')
|
||||||
|
|
||||||
|
// Check if this is a valid JS binding
|
||||||
|
let js = decodeJSBinding(sanitizedBinding)
|
||||||
|
if (js != null) {
|
||||||
|
// Replace ID inside JS binding
|
||||||
|
js = js.replace(new RegExp(oldId, "g"), newId)
|
||||||
|
|
||||||
|
// Create new valid JS binding
|
||||||
|
let newBinding = encodeJSBinding(js)
|
||||||
|
|
||||||
|
// Replace escaped double quotes
|
||||||
|
newBinding = newBinding.replace(/"/g, '\\"')
|
||||||
|
|
||||||
|
// Insert new JS back into binding.
|
||||||
|
// A single string replace here is better than a regex as
|
||||||
|
// the binding contains special characters, and we only need
|
||||||
|
// to replace a single instance.
|
||||||
|
children = children.replace(binding, newBinding)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Recurse on all children
|
||||||
|
component._children = JSON.parse(children)
|
||||||
|
component._children.forEach(makeComponentUnique)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -126,7 +126,7 @@ export const getDatasourceForProvider = (asset, component) => {
|
||||||
if (dataProviderSetting) {
|
if (dataProviderSetting) {
|
||||||
const settingValue = component[dataProviderSetting.key]
|
const settingValue = component[dataProviderSetting.key]
|
||||||
const providerId = extractLiteralHandlebarsID(settingValue)
|
const providerId = extractLiteralHandlebarsID(settingValue)
|
||||||
const provider = findComponent(asset.props, providerId)
|
const provider = findComponent(asset?.props, providerId)
|
||||||
return getDatasourceForProvider(asset, provider)
|
return getDatasourceForProvider(asset, provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,7 +458,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
||||||
// Determine the entity which backs this datasource.
|
// Determine the entity which backs this datasource.
|
||||||
// "provider" datasources are those targeting another data provider
|
// "provider" datasources are those targeting another data provider
|
||||||
if (type === "provider") {
|
if (type === "provider") {
|
||||||
const component = findComponent(asset.props, datasource.providerId)
|
const component = findComponent(asset?.props, datasource.providerId)
|
||||||
const source = getDatasourceForProvider(asset, component)
|
const source = getDatasourceForProvider(asset, component)
|
||||||
return getSchemaForDatasource(asset, source, options)
|
return getSchemaForDatasource(asset, source, options)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export const selectedComponent = derived(
|
||||||
if (!$currentAsset || !$store.selectedComponentId) {
|
if (!$currentAsset || !$store.selectedComponentId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return findComponent($currentAsset.props, $store.selectedComponentId)
|
return findComponent($currentAsset?.props, $store.selectedComponentId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -24,9 +24,9 @@ import {
|
||||||
findAllMatchingComponents,
|
findAllMatchingComponents,
|
||||||
findComponent,
|
findComponent,
|
||||||
getComponentSettings,
|
getComponentSettings,
|
||||||
|
makeComponentUnique,
|
||||||
} from "../componentUtils"
|
} from "../componentUtils"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { removeBindings } from "../dataBinding"
|
|
||||||
|
|
||||||
const INITIAL_FRONTEND_STATE = {
|
const INITIAL_FRONTEND_STATE = {
|
||||||
apps: [],
|
apps: [],
|
||||||
|
@ -400,11 +400,11 @@ export const getFrontendStore = () => {
|
||||||
parentComponent = selected
|
parentComponent = selected
|
||||||
} else {
|
} else {
|
||||||
// Otherwise we need to use the parent of this component
|
// Otherwise we need to use the parent of this component
|
||||||
parentComponent = findComponentParent(asset.props, selected._id)
|
parentComponent = findComponentParent(asset?.props, selected._id)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use screen or layout if no component is selected
|
// Use screen or layout if no component is selected
|
||||||
parentComponent = asset.props
|
parentComponent = asset?.props
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach component
|
// Attach component
|
||||||
|
@ -490,37 +490,22 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
paste: async (targetComponent, mode, preserveBindings = false) => {
|
paste: async (targetComponent, mode) => {
|
||||||
let promises = []
|
let promises = []
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
// Stop if we have nothing to paste
|
// Stop if we have nothing to paste
|
||||||
if (!state.componentToPaste) {
|
if (!state.componentToPaste) {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
// defines if this is a copy or a cut
|
|
||||||
const cut = state.componentToPaste.isCut
|
const cut = state.componentToPaste.isCut
|
||||||
|
|
||||||
// immediately need to remove bindings, currently these aren't valid when pasted
|
// Clone the component to paste and make unique if copying
|
||||||
if (!cut && !preserveBindings) {
|
|
||||||
state.componentToPaste = removeBindings(state.componentToPaste, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone the component to paste
|
|
||||||
// Retain the same ID if cutting as things may be referencing this component
|
|
||||||
delete state.componentToPaste.isCut
|
delete state.componentToPaste.isCut
|
||||||
let componentToPaste = cloneDeep(state.componentToPaste)
|
let componentToPaste = cloneDeep(state.componentToPaste)
|
||||||
if (cut) {
|
if (cut) {
|
||||||
state.componentToPaste = null
|
state.componentToPaste = null
|
||||||
} else {
|
} else {
|
||||||
const randomizeIds = component => {
|
makeComponentUnique(componentToPaste)
|
||||||
if (!component) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
component._id = Helpers.uuid()
|
|
||||||
component._children?.forEach(randomizeIds)
|
|
||||||
}
|
|
||||||
randomizeIds(componentToPaste)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === "inside") {
|
if (mode === "inside") {
|
||||||
|
|
|
@ -10,17 +10,18 @@ const allTemplates = tables => [
|
||||||
]
|
]
|
||||||
|
|
||||||
// Allows us to apply common behaviour to all create() functions
|
// Allows us to apply common behaviour to all create() functions
|
||||||
const createTemplateOverride = (frontendState, create) => () => {
|
const createTemplateOverride = (frontendState, template) => () => {
|
||||||
const screen = create()
|
const screen = template.create()
|
||||||
screen.name = screen.props._id
|
screen.name = screen.props._id
|
||||||
screen.routing.route = screen.routing.route.toLowerCase()
|
screen.routing.route = screen.routing.route.toLowerCase()
|
||||||
|
screen.template = template.id
|
||||||
return screen
|
return screen
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (frontendState, tables) => {
|
export default (frontendState, tables) => {
|
||||||
const enrichTemplate = template => ({
|
const enrichTemplate = template => ({
|
||||||
...template,
|
...template,
|
||||||
create: createTemplateOverride(frontendState, template.create),
|
create: createTemplateOverride(frontendState, template),
|
||||||
})
|
})
|
||||||
|
|
||||||
const fromScratch = enrichTemplate(createFromScratchScreen)
|
const fromScratch = enrichTemplate(createFromScratchScreen)
|
||||||
|
|
|
@ -160,6 +160,11 @@
|
||||||
await store.actions.components.updateProp(data.prop, data.value)
|
await store.actions.components.updateProp(data.prop, data.value)
|
||||||
} else if (type === "delete-component" && data.id) {
|
} else if (type === "delete-component" && data.id) {
|
||||||
confirmDeleteComponent(data.id)
|
confirmDeleteComponent(data.id)
|
||||||
|
} else if (type === "duplicate-component" && data.id) {
|
||||||
|
const rootComponent = get(currentAsset).props
|
||||||
|
const component = findComponent(rootComponent, data.id)
|
||||||
|
store.actions.components.copy(component)
|
||||||
|
await store.actions.components.paste(component)
|
||||||
} else if (type === "preview-loaded") {
|
} else if (type === "preview-loaded") {
|
||||||
// Wait for this event to show the client library if intelligent
|
// Wait for this event to show the client library if intelligent
|
||||||
// loading is supported
|
// loading is supported
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
const moveUpComponent = () => {
|
const moveUpComponent = () => {
|
||||||
const asset = get(currentAsset)
|
const asset = get(currentAsset)
|
||||||
const parent = findComponentParent(asset.props, component._id)
|
const parent = findComponentParent(asset?.props, component._id)
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
const moveDownComponent = () => {
|
const moveDownComponent = () => {
|
||||||
const asset = get(currentAsset)
|
const asset = get(currentAsset)
|
||||||
const parent = findComponentParent(asset.props, component._id)
|
const parent = findComponentParent(asset?.props, component._id)
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
const duplicateComponent = () => {
|
const duplicateComponent = () => {
|
||||||
storeComponentForCopy(false)
|
storeComponentForCopy(false)
|
||||||
pasteComponent("below", true)
|
pasteComponent("below")
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteComponent = async () => {
|
const deleteComponent = async () => {
|
||||||
|
@ -73,14 +73,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const storeComponentForCopy = (cut = false) => {
|
const storeComponentForCopy = (cut = false) => {
|
||||||
// lives in store - also used by drag drop
|
|
||||||
store.actions.components.copy(component, cut)
|
store.actions.components.copy(component, cut)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pasteComponent = (mode, preserveBindings = false) => {
|
const pasteComponent = mode => {
|
||||||
try {
|
try {
|
||||||
// lives in store - also used by drag drop
|
store.actions.components.paste(component, mode)
|
||||||
store.actions.components.paste(component, mode, preserveBindings)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error saving component")
|
notifications.error("Error saving component")
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { DropEffect, DropPosition } from "./dragDropStore"
|
import { DropEffect, DropPosition } from "./dragDropStore"
|
||||||
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
|
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
bind:this={confirmDeleteDialog}
|
bind:this={confirmDeleteDialog}
|
||||||
title="Confirm Deletion"
|
title="Confirm Deletion"
|
||||||
body={"Are you sure you wish to delete this layout?"}
|
body={"Are you sure you wish to delete this layout?"}
|
||||||
okText="Delete Layout"
|
okText="Delete layout"
|
||||||
onOk={deleteLayout}
|
onOk={deleteLayout}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import {
|
||||||
|
ActionMenu,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
Layout,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
|
export let path
|
||||||
|
export let screens
|
||||||
|
|
||||||
|
let confirmDeleteDialog
|
||||||
|
|
||||||
|
const deleteScreens = async () => {
|
||||||
|
if (!screens?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (let { id } of screens) {
|
||||||
|
// We have to fetch the screen to be deleted immediately before deleting
|
||||||
|
// as otherwise we're very likely to 409
|
||||||
|
const screen = get(store).screens.find(screen => screen._id === id)
|
||||||
|
if (!screen) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await store.actions.screens.delete(screen)
|
||||||
|
}
|
||||||
|
notifications.success("Screens deleted successfully")
|
||||||
|
$goto("../")
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error deleting screens")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionMenu>
|
||||||
|
<div slot="control" class="icon">
|
||||||
|
<Icon size="S" hoverable name="MoreSmallList" />
|
||||||
|
</div>
|
||||||
|
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>
|
||||||
|
Delete all screens
|
||||||
|
</MenuItem>
|
||||||
|
</ActionMenu>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmDeleteDialog}
|
||||||
|
title="Confirm Deletion"
|
||||||
|
okText="Delete screens"
|
||||||
|
onOk={deleteScreens}
|
||||||
|
>
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<div>
|
||||||
|
Are you sure you want to delete all screens under the <b>{path}</b> route?
|
||||||
|
</div>
|
||||||
|
<div>The following screens will be deleted:</div>
|
||||||
|
<div class="to-delete">
|
||||||
|
{#each screens as screen}
|
||||||
|
<div>{screen.route}</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.to-delete {
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-left: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -8,6 +8,7 @@
|
||||||
import instantiateStore from "./dragDropStore"
|
import instantiateStore from "./dragDropStore"
|
||||||
import ComponentTree from "./ComponentTree.svelte"
|
import ComponentTree from "./ComponentTree.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
import PathDropdownMenu from "./PathDropdownMenu.svelte"
|
||||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
|
@ -73,7 +74,9 @@
|
||||||
opened={routeOpened}
|
opened={routeOpened}
|
||||||
{border}
|
{border}
|
||||||
withArrow={route.subpaths}
|
withArrow={route.subpaths}
|
||||||
/>
|
>
|
||||||
|
<PathDropdownMenu screens={allScreens} {path} />
|
||||||
|
</NavItem>
|
||||||
|
|
||||||
{#if routeOpened}
|
{#if routeOpened}
|
||||||
{#each filteredScreens as screen (screen.id)}
|
{#each filteredScreens as screen (screen.id)}
|
||||||
|
|
|
@ -2,14 +2,57 @@
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { store, allScreens } from "builderStore"
|
import { store, allScreens } from "builderStore"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
|
import {
|
||||||
|
ActionMenu,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
Modal,
|
||||||
|
Helpers,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import ScreenDetailsModal from "../ScreenDetailsModal.svelte"
|
||||||
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||||
|
import analytics, { Events } from "analytics"
|
||||||
|
import { makeComponentUnique } from "builderStore/componentUtils"
|
||||||
|
|
||||||
export let screenId
|
export let screenId
|
||||||
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
|
let screenDetailsModal
|
||||||
|
|
||||||
$: screen = $allScreens.find(screen => screen._id === screenId)
|
$: screen = $allScreens.find(screen => screen._id === screenId)
|
||||||
|
|
||||||
|
const duplicateScreen = () => {
|
||||||
|
screenDetailsModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDuplicateScreen = async ({ screenName, screenUrl }) => {
|
||||||
|
// Create a dupe and ensure it is unique
|
||||||
|
let duplicateScreen = Helpers.cloneDeep(screen)
|
||||||
|
delete duplicateScreen._id
|
||||||
|
delete duplicateScreen._rev
|
||||||
|
makeComponentUnique(duplicateScreen.props)
|
||||||
|
|
||||||
|
// Attach the new name and URL
|
||||||
|
duplicateScreen.routing.route = sanitizeUrl(screenUrl)
|
||||||
|
duplicateScreen.props._instanceName = screenName
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create the screen
|
||||||
|
await store.actions.screens.save(duplicateScreen)
|
||||||
|
|
||||||
|
// Analytics
|
||||||
|
if (screen.template) {
|
||||||
|
analytics.captureEvent(Events.SCREEN.CREATED, {
|
||||||
|
template: "createFromScratch",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error duplicating screen")
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const deleteScreen = async () => {
|
const deleteScreen = async () => {
|
||||||
try {
|
try {
|
||||||
await store.actions.screens.delete(screen)
|
await store.actions.screens.delete(screen)
|
||||||
|
@ -19,12 +62,28 @@
|
||||||
notifications.error("Error deleting screen")
|
notifications.error("Error deleting screen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pasteComponent = mode => {
|
||||||
|
try {
|
||||||
|
store.actions.components.paste(screen?.props, mode)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error saving component")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionMenu>
|
<ActionMenu>
|
||||||
<div slot="control" class="icon">
|
<div slot="control" class="icon">
|
||||||
<Icon size="S" hoverable name="MoreSmallList" />
|
<Icon size="S" hoverable name="MoreSmallList" />
|
||||||
</div>
|
</div>
|
||||||
|
<MenuItem icon="Duplicate" on:click={duplicateScreen}>Duplicate</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon="ShowOneLayer"
|
||||||
|
on:click={() => pasteComponent("inside")}
|
||||||
|
disabled={!$store.componentToPaste}
|
||||||
|
>
|
||||||
|
Paste inside
|
||||||
|
</MenuItem>
|
||||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
|
|
||||||
|
@ -32,6 +91,15 @@
|
||||||
bind:this={confirmDeleteDialog}
|
bind:this={confirmDeleteDialog}
|
||||||
title="Confirm Deletion"
|
title="Confirm Deletion"
|
||||||
body={"Are you sure you wish to delete this screen?"}
|
body={"Are you sure you wish to delete this screen?"}
|
||||||
okText="Delete Screen"
|
okText="Delete screen"
|
||||||
onOk={deleteScreen}
|
onOk={deleteScreen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal bind:this={screenDetailsModal}>
|
||||||
|
<ScreenDetailsModal
|
||||||
|
onConfirm={createDuplicateScreen}
|
||||||
|
screenName={screen?.props._instanceName}
|
||||||
|
screenUrl={screen?.routing.route}
|
||||||
|
confirmText="Duplicate"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
|
@ -10,39 +10,19 @@
|
||||||
ProgressCircle,
|
ProgressCircle,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import getTemplates from "builderStore/store/screenTemplates"
|
import getTemplates from "builderStore/store/screenTemplates"
|
||||||
import { onDestroy } from "svelte"
|
|
||||||
|
|
||||||
import { createEventDispatcher } from "svelte"
|
export let onConfirm
|
||||||
|
export let onCancel
|
||||||
export let chooseModal
|
|
||||||
export let save
|
|
||||||
export let showProgressCircle = false
|
export let showProgressCircle = false
|
||||||
|
|
||||||
let selectedScreens = []
|
|
||||||
|
|
||||||
const blankScreen = "createFromScratch"
|
const blankScreen = "createFromScratch"
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
function setScreens() {
|
let selectedScreens = []
|
||||||
dispatch("save", {
|
let templates = getTemplates($store, $tables.list)
|
||||||
screens: selectedScreens,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$: blankSelected = selectedScreens?.length === 1
|
$: blankSelected = selectedScreens?.length === 1
|
||||||
$: autoSelected = selectedScreens?.length > 0 && !blankSelected
|
$: autoSelected = selectedScreens?.length > 0 && !blankSelected
|
||||||
|
|
||||||
let templates = getTemplates($store, $tables.list)
|
|
||||||
|
|
||||||
const confirm = async () => {
|
|
||||||
if (autoSelected) {
|
|
||||||
setScreens()
|
|
||||||
await save()
|
|
||||||
} else {
|
|
||||||
setScreens()
|
|
||||||
chooseModal(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const toggleScreenSelection = table => {
|
const toggleScreenSelection = table => {
|
||||||
if (selectedScreens.find(s => s.table === table.name)) {
|
if (selectedScreens.find(s => s.table === table.name)) {
|
||||||
selectedScreens = selectedScreens.filter(
|
selectedScreens = selectedScreens.filter(
|
||||||
|
@ -56,25 +36,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
const confirmScreenSelection = async () => {
|
||||||
selectedScreens = []
|
await onConfirm(selectedScreens)
|
||||||
})
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Add screens"
|
title="Add screens"
|
||||||
confirmText="Add Screens"
|
confirmText="Add screens"
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
onConfirm={() => confirm()}
|
onConfirm={confirmScreenSelection}
|
||||||
|
{onCancel}
|
||||||
disabled={!selectedScreens.length}
|
disabled={!selectedScreens.length}
|
||||||
size="L"
|
size="L"
|
||||||
>
|
>
|
||||||
<Body size="S"
|
<Body size="S">
|
||||||
>Please select the screens you would like to add to your application.
|
Please select the screens you would like to add to your application.
|
||||||
Autogenerated screens come with CRUD functionality.</Body
|
Autogenerated screens come with CRUD functionality.
|
||||||
>
|
</Body>
|
||||||
|
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<Detail size="S">Blank screen</Detail>
|
<Detail size="S">Blank screen</Detail>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -2,58 +2,62 @@
|
||||||
import { ModalContent, Input, ProgressCircle } from "@budibase/bbui"
|
import { ModalContent, Input, ProgressCircle } from "@budibase/bbui"
|
||||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||||
import { selectedAccessRole, allScreens } from "builderStore"
|
import { selectedAccessRole, allScreens } from "builderStore"
|
||||||
import { onDestroy } from "svelte"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export let screenName
|
export let onConfirm
|
||||||
export let url
|
export let onCancel
|
||||||
export let chooseModal
|
|
||||||
export let save
|
|
||||||
export let showProgressCircle = false
|
export let showProgressCircle = false
|
||||||
|
export let screenName
|
||||||
|
export let screenUrl
|
||||||
|
export let confirmText = "Continue"
|
||||||
|
|
||||||
let routeError
|
let routeError
|
||||||
let roleId = $selectedAccessRole || "BASIC"
|
let touched = false
|
||||||
|
|
||||||
const routeChanged = event => {
|
const routeChanged = event => {
|
||||||
if (!event.detail.startsWith("/")) {
|
if (!event.detail.startsWith("/")) {
|
||||||
url = "/" + event.detail
|
screenUrl = "/" + event.detail
|
||||||
}
|
}
|
||||||
url = sanitizeUrl(url)
|
touched = true
|
||||||
|
screenUrl = sanitizeUrl(screenUrl)
|
||||||
if (routeExists(url, roleId)) {
|
if (routeExists(screenUrl)) {
|
||||||
routeError = "This URL is already taken for this access role"
|
routeError = "This URL is already taken for this access role"
|
||||||
} else {
|
} else {
|
||||||
routeError = ""
|
routeError = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const routeExists = (url, roleId) => {
|
const routeExists = url => {
|
||||||
return $allScreens.some(
|
const roleId = get(selectedAccessRole) || "BASIC"
|
||||||
|
return get(allScreens).some(
|
||||||
screen =>
|
screen =>
|
||||||
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
||||||
screen.routing.roleId === roleId
|
screen.routing.roleId === roleId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
const confirmScreenDetails = async () => {
|
||||||
screenName = ""
|
await onConfirm({
|
||||||
url = ""
|
screenName,
|
||||||
|
screenUrl,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
size="M"
|
size="M"
|
||||||
title={"Enter details"}
|
title={"Enter details"}
|
||||||
confirmText={"Continue"}
|
{confirmText}
|
||||||
onCancel={() => chooseModal(0)}
|
onConfirm={confirmScreenDetails}
|
||||||
onConfirm={() => save()}
|
{onCancel}
|
||||||
cancelText={"Back"}
|
cancelText={"Back"}
|
||||||
disabled={!screenName || !url || routeError}
|
disabled={!screenName || !screenUrl || routeError || !touched}
|
||||||
>
|
>
|
||||||
<Input label="Name" bind:value={screenName} />
|
<Input label="Name" bind:value={screenName} />
|
||||||
<Input
|
<Input
|
||||||
label="URL"
|
label="URL"
|
||||||
error={routeError}
|
error={routeError}
|
||||||
bind:value={url}
|
bind:value={screenUrl}
|
||||||
on:change={routeChanged}
|
on:change={routeChanged}
|
||||||
/>
|
/>
|
||||||
<div slot="footer">
|
<div slot="footer">
|
||||||
|
|
|
@ -3,141 +3,133 @@
|
||||||
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
|
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
|
||||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||||
import { Modal, notifications } from "@budibase/bbui"
|
import { Modal, notifications } from "@budibase/bbui"
|
||||||
import { store, selectedAccessRole, allScreens } from "builderStore"
|
import { store, selectedAccessRole } from "builderStore"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
let newScreenModal
|
let pendingScreen
|
||||||
let navigationSelectionModal
|
|
||||||
let screenDetailsModal
|
|
||||||
let screenName = ""
|
|
||||||
let url = ""
|
|
||||||
let selectedScreens = []
|
|
||||||
let showProgressCircle = false
|
let showProgressCircle = false
|
||||||
let routeError
|
|
||||||
let createdScreens = []
|
|
||||||
|
|
||||||
$: roleId = $selectedAccessRole || "BASIC"
|
// Modal refs
|
||||||
|
let newScreenModal
|
||||||
|
let screenDetailsModal
|
||||||
|
|
||||||
const createScreens = async () => {
|
// External handler to show the screen wizard
|
||||||
for (let screen of selectedScreens) {
|
export const showModal = () => {
|
||||||
let test = screen.create()
|
newScreenModal.show()
|
||||||
createdScreens.push(test)
|
|
||||||
analytics.captureEvent(Events.SCREEN.CREATED, {
|
|
||||||
template: screen.id || screen.name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = async () => {
|
// Reset state when showing modal again
|
||||||
showProgressCircle = true
|
pendingScreen = null
|
||||||
try {
|
|
||||||
await createScreens()
|
|
||||||
for (let screen of createdScreens) {
|
|
||||||
await saveScreens(screen)
|
|
||||||
}
|
|
||||||
await store.actions.routing.fetch()
|
|
||||||
selectedScreens = []
|
|
||||||
createdScreens = []
|
|
||||||
screenName = ""
|
|
||||||
url = ""
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error creating screens")
|
|
||||||
}
|
|
||||||
showProgressCircle = false
|
showProgressCircle = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveScreens = async draftScreen => {
|
// Creates an array of screens, checking and sanitising their URLs
|
||||||
let existingScreenCount = $store.screens.filter(
|
const createScreens = async screens => {
|
||||||
s => s.props._instanceName == draftScreen.props._instanceName
|
if (!screens?.length) {
|
||||||
).length
|
return
|
||||||
if (existingScreenCount > 0) {
|
|
||||||
let oldUrlArr = draftScreen.routing.route.split("/")
|
|
||||||
oldUrlArr[1] = `${oldUrlArr[1]}-${existingScreenCount + 1}`
|
|
||||||
draftScreen.routing.route = oldUrlArr.join("/")
|
|
||||||
}
|
}
|
||||||
|
showProgressCircle = true
|
||||||
|
|
||||||
let route = url ? sanitizeUrl(`${url}`) : draftScreen.routing.route
|
|
||||||
if (draftScreen) {
|
|
||||||
if (!route) {
|
|
||||||
routeError = "URL is required"
|
|
||||||
} else {
|
|
||||||
if (routeExists(route, roleId)) {
|
|
||||||
routeError = "This URL is already taken for this access role"
|
|
||||||
} else {
|
|
||||||
routeError = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeError) return false
|
|
||||||
|
|
||||||
if (screenName) {
|
|
||||||
draftScreen.props._instanceName = screenName
|
|
||||||
}
|
|
||||||
|
|
||||||
draftScreen.routing.route = route
|
|
||||||
draftScreen.routing.roleId = roleId
|
|
||||||
|
|
||||||
await store.actions.screens.save(draftScreen)
|
|
||||||
if (draftScreen.props._instanceName.endsWith("List")) {
|
|
||||||
try {
|
try {
|
||||||
|
for (let screen of screens) {
|
||||||
|
// Check we aren't clashing with an existing URL
|
||||||
|
if (hasExistingUrl(screen.routing.route)) {
|
||||||
|
let suffix = 2
|
||||||
|
let candidateUrl = makeCandidateUrl(screen, suffix)
|
||||||
|
while (hasExistingUrl(candidateUrl)) {
|
||||||
|
candidateUrl = makeCandidateUrl(screen, ++suffix)
|
||||||
|
}
|
||||||
|
screen.routing.route = candidateUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitise URL
|
||||||
|
screen.routing.route = sanitizeUrl(screen.routing.route)
|
||||||
|
|
||||||
|
// Use the currently selected role
|
||||||
|
screen.routing.roleId = get(selectedAccessRole) || "BASIC"
|
||||||
|
|
||||||
|
// Create the screen
|
||||||
|
await store.actions.screens.save(screen)
|
||||||
|
|
||||||
|
// Analytics
|
||||||
|
if (screen.template) {
|
||||||
|
analytics.captureEvent(Events.SCREEN.CREATED, {
|
||||||
|
template: screen.template,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add link in layout for list screens
|
||||||
|
if (screen.props._instanceName.endsWith("List")) {
|
||||||
await store.actions.components.links.save(
|
await store.actions.components.links.save(
|
||||||
draftScreen.routing.route,
|
screen.routing.route,
|
||||||
draftScreen.routing.route.split("/")[1]
|
screen.routing.route.split("/")[1]
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error creating link to screen")
|
notifications.error("Error creating screens")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showProgressCircle = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks if any screens exist in the store with the given route and
|
||||||
|
// currently selected role
|
||||||
|
const hasExistingUrl = url => {
|
||||||
|
const roleId = get(selectedAccessRole) || "BASIC"
|
||||||
|
const screens = get(store).screens.filter(s => s.routing.roleId === roleId)
|
||||||
|
return !!screens.find(s => s.routing?.route === url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructs a candidate URL for a new screen, suffixing the base of the
|
||||||
|
// screen's URL with a given suffix.
|
||||||
|
// e.g. "/sales/:id" => "/sales-1/:id"
|
||||||
|
const makeCandidateUrl = (screen, suffix) => {
|
||||||
|
let url = screen.routing?.route || ""
|
||||||
|
if (url.startsWith("/")) {
|
||||||
|
url = url.slice(1)
|
||||||
|
}
|
||||||
|
if (!url.includes("/")) {
|
||||||
|
return `/${url}-${suffix}`
|
||||||
|
} else {
|
||||||
|
const split = url.split("/")
|
||||||
|
return `/${split[0]}-${suffix}/${split.slice(1).join("/")}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const routeExists = (route, roleId) => {
|
// Handler for NewScreenModal
|
||||||
return $allScreens.some(
|
const confirmScreenSelection = async templates => {
|
||||||
screen =>
|
// Handle template selection
|
||||||
screen.routing.route.toLowerCase() === route.toLowerCase() &&
|
if (templates?.length > 1) {
|
||||||
screen.routing.roleId === roleId
|
// Autoscreens, so create immediately
|
||||||
)
|
const screens = templates.map(template => template.create())
|
||||||
}
|
await createScreens(screens)
|
||||||
|
} else {
|
||||||
export const showModal = () => {
|
// Empty screen, so proceed to the next modal
|
||||||
newScreenModal.show()
|
pendingScreen = templates[0].create()
|
||||||
}
|
|
||||||
|
|
||||||
const setScreens = evt => {
|
|
||||||
selectedScreens = evt.detail.screens
|
|
||||||
}
|
|
||||||
|
|
||||||
const chooseModal = index => {
|
|
||||||
/*
|
|
||||||
0 = newScreenModal
|
|
||||||
1 = screenDetailsModal
|
|
||||||
2 = navigationSelectionModal
|
|
||||||
*/
|
|
||||||
if (index === 0) {
|
|
||||||
newScreenModal.show()
|
|
||||||
} else if (index === 1) {
|
|
||||||
screenDetailsModal.show()
|
screenDetailsModal.show()
|
||||||
} else if (index === 2) {
|
|
||||||
navigationSelectionModal.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler for ScreenDetailsModal
|
||||||
|
const confirmScreenDetails = async ({ screenName, screenUrl }) => {
|
||||||
|
if (!pendingScreen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pendingScreen.props._instanceName = screenName
|
||||||
|
pendingScreen.routing.route = screenUrl
|
||||||
|
await createScreens([pendingScreen])
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={newScreenModal}>
|
<Modal bind:this={newScreenModal}>
|
||||||
<NewScreenModal
|
<NewScreenModal onConfirm={confirmScreenSelection} {showProgressCircle} />
|
||||||
on:save={setScreens}
|
|
||||||
{showProgressCircle}
|
|
||||||
{save}
|
|
||||||
{chooseModal}
|
|
||||||
/>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={screenDetailsModal}>
|
<Modal bind:this={screenDetailsModal}>
|
||||||
<ScreenDetailsModal
|
<ScreenDetailsModal
|
||||||
bind:screenName
|
|
||||||
bind:url
|
|
||||||
{showProgressCircle}
|
{showProgressCircle}
|
||||||
{save}
|
onConfirm={confirmScreenDetails}
|
||||||
{chooseModal}
|
onCancel={() => newScreenModal.show()}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
const customSections = settings.filter(setting => setting.section)
|
const customSections = settings.filter(setting => setting.section)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: "General",
|
name: componentDefinition?.name || "General",
|
||||||
info: componentDefinition?.info,
|
info: componentDefinition?.info,
|
||||||
settings: generalSettings,
|
settings: generalSettings,
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
|
||||||
$: components = findAllMatchingComponents($currentAsset.props, component =>
|
$: components = findAllMatchingComponents($currentAsset?.props, component =>
|
||||||
component._component.endsWith("s3upload")
|
component._component.endsWith("s3upload")
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
||||||
|
|
||||||
$: path = findComponentPath($currentAsset.props, $store.selectedComponentId)
|
$: path = findComponentPath($currentAsset?.props, $store.selectedComponentId)
|
||||||
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
|
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
|
||||||
|
|
||||||
// Set initial value to closest data provider
|
// Set initial value to closest data provider
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
export let type
|
export let type
|
||||||
|
|
||||||
$: form = findClosestMatchingComponent(
|
$: form = findClosestMatchingComponent(
|
||||||
$currentAsset.props,
|
$currentAsset?.props,
|
||||||
componentInstance._id,
|
componentInstance._id,
|
||||||
component => component._component === "@budibase/standard-components/form"
|
component => component._component === "@budibase/standard-components/form"
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
const resetFormFields = async () => {
|
const resetFormFields = async () => {
|
||||||
const form = findClosestMatchingComponent(
|
const form = findClosestMatchingComponent(
|
||||||
$currentAsset.props,
|
$currentAsset?.props,
|
||||||
componentInstance._id,
|
componentInstance._id,
|
||||||
component => component._component.endsWith("/form")
|
component => component._component.endsWith("/form")
|
||||||
)
|
)
|
||||||
|
|
|
@ -135,7 +135,7 @@
|
||||||
if (asset?._id) {
|
if (asset?._id) {
|
||||||
url += `/${asset._id}`
|
url += `/${asset._id}`
|
||||||
if (componentId) {
|
if (componentId) {
|
||||||
const componentPath = findComponentPath(asset.props, componentId)
|
const componentPath = findComponentPath(asset?.props, componentId)
|
||||||
const componentURL = componentPath
|
const componentURL = componentPath
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.map(comp => comp._id)
|
.map(comp => comp._id)
|
||||||
|
|
|
@ -146,6 +146,15 @@
|
||||||
<div class="divider" />
|
<div class="divider" />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
<SettingsButton
|
||||||
|
icon="Duplicate"
|
||||||
|
on:click={() => {
|
||||||
|
builderStore.actions.duplicateComponent(
|
||||||
|
$builderStore.selectedComponent._id
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
title="Duplicate component"
|
||||||
|
/>
|
||||||
<SettingsButton
|
<SettingsButton
|
||||||
icon="Delete"
|
icon="Delete"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -153,6 +162,7 @@
|
||||||
$builderStore.selectedComponent._id
|
$builderStore.selectedComponent._id
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
title="Delete component"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -62,6 +62,9 @@ const createBuilderStore = () => {
|
||||||
deleteComponent: id => {
|
deleteComponent: id => {
|
||||||
dispatchEvent("delete-component", { id })
|
dispatchEvent("delete-component", { id })
|
||||||
},
|
},
|
||||||
|
duplicateComponent: id => {
|
||||||
|
dispatchEvent("duplicate-component", { id })
|
||||||
|
},
|
||||||
notifyLoaded: () => {
|
notifyLoaded: () => {
|
||||||
dispatchEvent("preview-loaded")
|
dispatchEvent("preview-loaded")
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue