Merge branch 'master' of github.com:budibase/budibase into budi-7754-make-our-helm-chart-work-out-of-the-box
This commit is contained in:
commit
e70b57c936
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.13.19",
|
||||
"version": "2.13.20",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
* @Budibase/backend
|
|
@ -18,6 +18,7 @@
|
|||
checked={value}
|
||||
{disabled}
|
||||
on:change={onChange}
|
||||
on:click
|
||||
{id}
|
||||
type="checkbox"
|
||||
class="spectrum-Switch-input"
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
let focus = false
|
||||
|
||||
const updateValue = newValue => {
|
||||
if (readonly) {
|
||||
if (readonly || disabled) {
|
||||
return
|
||||
}
|
||||
if (type === "number") {
|
||||
|
@ -31,14 +31,14 @@
|
|||
}
|
||||
|
||||
const onFocus = () => {
|
||||
if (readonly) {
|
||||
if (readonly || disabled) {
|
||||
return
|
||||
}
|
||||
focus = true
|
||||
}
|
||||
|
||||
const onBlur = event => {
|
||||
if (readonly) {
|
||||
if (readonly || disabled) {
|
||||
return
|
||||
}
|
||||
focus = false
|
||||
|
@ -46,14 +46,14 @@
|
|||
}
|
||||
|
||||
const onInput = event => {
|
||||
if (readonly || !updateOnChange) {
|
||||
if (readonly || !updateOnChange || disabled) {
|
||||
return
|
||||
}
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const updateValueOnEnter = event => {
|
||||
if (readonly) {
|
||||
if (readonly || disabled) {
|
||||
return
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
|
@ -69,6 +69,7 @@
|
|||
}
|
||||
|
||||
onMount(() => {
|
||||
if (disabled) return
|
||||
focus = autofocus
|
||||
if (focus) field.focus()
|
||||
})
|
||||
|
@ -108,4 +109,16 @@
|
|||
.spectrum-Textfield {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--grey-7);
|
||||
}
|
||||
|
||||
input:hover::placeholder {
|
||||
color: var(--grey-7) !important;
|
||||
}
|
||||
|
||||
input:focus::placeholder {
|
||||
color: var(--grey-7) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -19,5 +19,5 @@
|
|||
</script>
|
||||
|
||||
<Field {helpText} {label} {labelPosition} {error}>
|
||||
<Switch {error} {disabled} {text} {value} on:change={onChange} />
|
||||
<Switch {error} {disabled} {text} {value} on:change={onChange} on:click />
|
||||
</Field>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { store } from "./index"
|
||||
import { get } from "svelte/store"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import {
|
||||
decodeJSBinding,
|
||||
|
@ -238,6 +239,10 @@ export const makeComponentUnique = component => {
|
|||
}
|
||||
|
||||
export const getComponentText = component => {
|
||||
if (component == null) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (component?._instanceName) {
|
||||
return component._instanceName
|
||||
}
|
||||
|
@ -246,3 +251,16 @@ export const getComponentText = component => {
|
|||
"component"
|
||||
return capitalise(type)
|
||||
}
|
||||
|
||||
export const getComponentName = component => {
|
||||
if (component == null) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const components = get(store)?.components || {}
|
||||
const componentDefinition = components[component._component] || {}
|
||||
const name =
|
||||
componentDefinition.friendlyName || componentDefinition.name || ""
|
||||
|
||||
return name
|
||||
}
|
||||
|
|
|
@ -29,6 +29,12 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
|||
const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g
|
||||
const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g
|
||||
|
||||
const UpdateReferenceAction = {
|
||||
ADD: "add",
|
||||
DELETE: "delete",
|
||||
MOVE: "move",
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all bindable data context fields and instance fields.
|
||||
*/
|
||||
|
@ -1226,3 +1232,81 @@ export const runtimeToReadableBinding = (
|
|||
"readableBinding"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to update binding references for automation or action steps
|
||||
*
|
||||
* @param obj - The object to be updated
|
||||
* @param originalIndex - The original index of the step being moved. Not applicable to add/delete.
|
||||
* @param modifiedIndex - The new index of the step being modified
|
||||
* @param action - Used to determine if a step is being added, deleted or moved
|
||||
* @param label - The binding text that describes the steps
|
||||
*/
|
||||
export const updateReferencesInObject = ({
|
||||
obj,
|
||||
modifiedIndex,
|
||||
action,
|
||||
label,
|
||||
originalIndex,
|
||||
}) => {
|
||||
const stepIndexRegex = new RegExp(`{{\\s*${label}\\.(\\d+)\\.`, "g")
|
||||
const updateActionStep = (str, index, replaceWith) =>
|
||||
str.replace(`{{ ${label}.${index}.`, `{{ ${label}.${replaceWith}.`)
|
||||
for (const key in obj) {
|
||||
if (typeof obj[key] === "string") {
|
||||
let matches
|
||||
while ((matches = stepIndexRegex.exec(obj[key])) !== null) {
|
||||
const referencedStep = parseInt(matches[1])
|
||||
if (
|
||||
action === UpdateReferenceAction.ADD &&
|
||||
referencedStep >= modifiedIndex
|
||||
) {
|
||||
obj[key] = updateActionStep(
|
||||
obj[key],
|
||||
referencedStep,
|
||||
referencedStep + 1
|
||||
)
|
||||
} else if (
|
||||
action === UpdateReferenceAction.DELETE &&
|
||||
referencedStep > modifiedIndex
|
||||
) {
|
||||
obj[key] = updateActionStep(
|
||||
obj[key],
|
||||
referencedStep,
|
||||
referencedStep - 1
|
||||
)
|
||||
} else if (action === UpdateReferenceAction.MOVE) {
|
||||
if (referencedStep === originalIndex) {
|
||||
obj[key] = updateActionStep(obj[key], referencedStep, modifiedIndex)
|
||||
} else if (
|
||||
modifiedIndex <= referencedStep &&
|
||||
modifiedIndex < originalIndex
|
||||
) {
|
||||
obj[key] = updateActionStep(
|
||||
obj[key],
|
||||
referencedStep,
|
||||
referencedStep + 1
|
||||
)
|
||||
} else if (
|
||||
modifiedIndex >= referencedStep &&
|
||||
modifiedIndex > originalIndex
|
||||
) {
|
||||
obj[key] = updateActionStep(
|
||||
obj[key],
|
||||
referencedStep,
|
||||
referencedStep - 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||
updateReferencesInObject({
|
||||
obj: obj[key],
|
||||
modifiedIndex,
|
||||
action,
|
||||
label,
|
||||
originalIndex,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { getTemporalStore } from "./store/temporal"
|
|||
import { getThemeStore } from "./store/theme"
|
||||
import { getUserStore } from "./store/users"
|
||||
import { getDeploymentStore } from "./store/deployments"
|
||||
import { derived, writable, get } from "svelte/store"
|
||||
import { derived, get } from "svelte/store"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { createHistoryStore } from "builderStore/store/history"
|
||||
|
@ -146,5 +146,3 @@ export const userSelectedResourceMap = derived(userStore, $userStore => {
|
|||
export const isOnlyUser = derived(userStore, $userStore => {
|
||||
return $userStore.length < 2
|
||||
})
|
||||
|
||||
export const screensHeight = writable("210px")
|
||||
|
|
|
@ -4,6 +4,7 @@ import { cloneDeep } from "lodash/fp"
|
|||
import { generate } from "shortid"
|
||||
import { selectedAutomation } from "builderStore"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { updateReferencesInObject } from "builderStore/dataBinding"
|
||||
|
||||
const initialAutomationState = {
|
||||
automations: [],
|
||||
|
@ -22,34 +23,14 @@ export const getAutomationStore = () => {
|
|||
return store
|
||||
}
|
||||
|
||||
const updateReferencesInObject = (obj, modifiedIndex, action) => {
|
||||
const regex = /{{\s*steps\.(\d+)\./g
|
||||
for (const key in obj) {
|
||||
if (typeof obj[key] === "string") {
|
||||
let matches
|
||||
while ((matches = regex.exec(obj[key])) !== null) {
|
||||
const referencedStep = parseInt(matches[1])
|
||||
if (action === "add" && referencedStep >= modifiedIndex) {
|
||||
obj[key] = obj[key].replace(
|
||||
`{{ steps.${referencedStep}.`,
|
||||
`{{ steps.${referencedStep + 1}.`
|
||||
)
|
||||
} else if (action === "delete" && referencedStep > modifiedIndex) {
|
||||
obj[key] = obj[key].replace(
|
||||
`{{ steps.${referencedStep}.`,
|
||||
`{{ steps.${referencedStep - 1}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||
updateReferencesInObject(obj[key], modifiedIndex, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateStepReferences = (steps, modifiedIndex, action) => {
|
||||
steps.forEach(step => {
|
||||
updateReferencesInObject(step.inputs, modifiedIndex, action)
|
||||
updateReferencesInObject({
|
||||
obj: step.inputs,
|
||||
modifiedIndex,
|
||||
action,
|
||||
label: "steps",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { expect, describe, it, vi } from "vitest"
|
|||
import {
|
||||
runtimeToReadableBinding,
|
||||
readableToRuntimeBinding,
|
||||
updateReferencesInObject,
|
||||
} from "../dataBinding"
|
||||
|
||||
vi.mock("@budibase/frontend-core")
|
||||
|
@ -84,3 +85,461 @@ describe("readableToRuntimeBinding", () => {
|
|||
).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateReferencesInObject", () => {
|
||||
it("should increment steps in sequence on 'add'", () => {
|
||||
let obj = [
|
||||
{
|
||||
id: "a0",
|
||||
parameters: {
|
||||
text: "Alpha",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.1.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
parameters: {
|
||||
text: "Carrot {{ actions.1.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.4.row }}",
|
||||
},
|
||||
},
|
||||
]
|
||||
updateReferencesInObject({
|
||||
obj,
|
||||
modifiedIndex: 0,
|
||||
action: "add",
|
||||
label: "actions",
|
||||
})
|
||||
|
||||
expect(obj).toEqual([
|
||||
{
|
||||
id: "a0",
|
||||
parameters: {
|
||||
text: "Alpha",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
parameters: {
|
||||
text: "Carrot {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.4.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.5.row }}",
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should decrement steps in sequence on 'delete'", () => {
|
||||
let obj = [
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.1.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.4.row }}",
|
||||
},
|
||||
},
|
||||
]
|
||||
updateReferencesInObject({
|
||||
obj,
|
||||
modifiedIndex: 2,
|
||||
action: "delete",
|
||||
label: "actions",
|
||||
})
|
||||
|
||||
expect(obj).toEqual([
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.1.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle on 'move' to a lower index", () => {
|
||||
let obj = [
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.0.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
parameters: {
|
||||
text: "Carrot {{ actions.0.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
]
|
||||
updateReferencesInObject({
|
||||
obj,
|
||||
modifiedIndex: 2,
|
||||
action: "move",
|
||||
label: "actions",
|
||||
originalIndex: 4,
|
||||
})
|
||||
|
||||
expect(obj).toEqual([
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.0.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.4.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
parameters: {
|
||||
text: "Carrot {{ actions.0.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle on 'move' to a higher index", () => {
|
||||
let obj = [
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.0.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
parameters: {
|
||||
text: "Carrot {{ actions.0.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
]
|
||||
updateReferencesInObject({
|
||||
obj,
|
||||
modifiedIndex: 2,
|
||||
action: "move",
|
||||
label: "actions",
|
||||
originalIndex: 0,
|
||||
})
|
||||
|
||||
expect(obj).toEqual([
|
||||
{
|
||||
id: "b2",
|
||||
parameters: {
|
||||
text: "Banana {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
parameters: {
|
||||
text: "Carrot {{ actions.2.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "a1",
|
||||
parameters: {
|
||||
text: "Apple",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d4",
|
||||
parameters: {
|
||||
text: "Dog {{ actions.1.row }}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "e5",
|
||||
parameters: {
|
||||
text: "Eagle {{ actions.3.row }}",
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle on 'move' of action being referenced, dragged to a higher index", () => {
|
||||
let obj = [
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
id: "cCD0Dwcnq",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
id: "3fbbIOfN0H",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Save Row",
|
||||
parameters: {
|
||||
tableId: "ta_bb_employee",
|
||||
},
|
||||
id: "aehg5cTmhR",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
id: "mzkpf86cxo",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
id: "h0uDFeJa8A",
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
autoDismiss: true,
|
||||
type: "success",
|
||||
message: "{{ actions.1.row }}",
|
||||
},
|
||||
"##eventHandlerType": "Show Notification",
|
||||
id: "JEI5lAyJZ",
|
||||
},
|
||||
]
|
||||
updateReferencesInObject({
|
||||
obj,
|
||||
modifiedIndex: 2,
|
||||
action: "move",
|
||||
label: "actions",
|
||||
originalIndex: 1,
|
||||
})
|
||||
|
||||
expect(obj).toEqual([
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
id: "cCD0Dwcnq",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
id: "3fbbIOfN0H",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Save Row",
|
||||
parameters: {
|
||||
tableId: "ta_bb_employee",
|
||||
},
|
||||
id: "aehg5cTmhR",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
id: "mzkpf86cxo",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
id: "h0uDFeJa8A",
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
autoDismiss: true,
|
||||
type: "success",
|
||||
message: "{{ actions.2.row }}",
|
||||
},
|
||||
"##eventHandlerType": "Show Notification",
|
||||
id: "JEI5lAyJZ",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle on 'move' of action being referenced, dragged to a lower index", () => {
|
||||
let obj = [
|
||||
{
|
||||
"##eventHandlerType": "Save Row",
|
||||
parameters: {
|
||||
tableId: "ta_bb_employee",
|
||||
},
|
||||
id: "aehg5cTmhR",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
id: "cCD0Dwcnq",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
id: "3fbbIOfN0H",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
id: "mzkpf86cxo",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
id: "h0uDFeJa8A",
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
autoDismiss: true,
|
||||
type: "success",
|
||||
message: "{{ actions.4.row }}",
|
||||
},
|
||||
"##eventHandlerType": "Show Notification",
|
||||
id: "JEI5lAyJZ",
|
||||
},
|
||||
]
|
||||
updateReferencesInObject({
|
||||
obj,
|
||||
modifiedIndex: 0,
|
||||
action: "move",
|
||||
label: "actions",
|
||||
originalIndex: 4,
|
||||
})
|
||||
|
||||
expect(obj).toEqual([
|
||||
{
|
||||
"##eventHandlerType": "Save Row",
|
||||
parameters: {
|
||||
tableId: "ta_bb_employee",
|
||||
},
|
||||
id: "aehg5cTmhR",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
id: "cCD0Dwcnq",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
id: "3fbbIOfN0H",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
id: "mzkpf86cxo",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
id: "h0uDFeJa8A",
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
autoDismiss: true,
|
||||
type: "success",
|
||||
message: "{{ actions.0.row }}",
|
||||
},
|
||||
"##eventHandlerType": "Show Notification",
|
||||
id: "JEI5lAyJZ",
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { AbsTooltip, Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
import { UserAvatars } from "@budibase/frontend-core"
|
||||
|
||||
export let icon
|
||||
export let iconTooltip
|
||||
export let withArrow = false
|
||||
export let withActions = true
|
||||
export let indentLevel = 0
|
||||
|
@ -77,7 +78,11 @@
|
|||
{style}
|
||||
{draggable}
|
||||
>
|
||||
<div class="nav-item-content" bind:this={contentRef}>
|
||||
<div
|
||||
class="nav-item-content"
|
||||
bind:this={contentRef}
|
||||
class:right={rightAlignIcon}
|
||||
>
|
||||
{#if withArrow}
|
||||
<div
|
||||
class:opened
|
||||
|
@ -98,7 +103,9 @@
|
|||
</div>
|
||||
{:else if icon}
|
||||
<div class="icon" class:right={rightAlignIcon}>
|
||||
<Icon color={iconColor} size="S" name={icon} />
|
||||
<AbsTooltip type="info" position="right" text={iconTooltip}>
|
||||
<Icon color={iconColor} size="S" name={icon} />
|
||||
</AbsTooltip>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text" title={showTooltip ? text : null}>
|
||||
|
@ -166,6 +173,11 @@
|
|||
width: max-content;
|
||||
position: relative;
|
||||
padding-left: var(--spacing-l);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav-item-content.right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Needed to fully display the actions icon */
|
||||
|
@ -264,6 +276,7 @@
|
|||
}
|
||||
|
||||
.right {
|
||||
margin-left: auto;
|
||||
order: 10;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
const getResizeActions = (
|
||||
cssProperty,
|
||||
mouseMoveEventProperty,
|
||||
elementProperty,
|
||||
initialValue,
|
||||
setValue = () => {}
|
||||
) => {
|
||||
let element = null
|
||||
|
||||
const elementAction = node => {
|
||||
element = node
|
||||
|
||||
if (initialValue != null) {
|
||||
element.style[cssProperty] = `${initialValue}px`
|
||||
}
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
element = null
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const dragHandleAction = node => {
|
||||
let startProperty = null
|
||||
let startPosition = null
|
||||
|
||||
const handleMouseMove = e => {
|
||||
e.preventDefault() // Prevent highlighting while dragging
|
||||
const change = e[mouseMoveEventProperty] - startPosition
|
||||
element.style[cssProperty] = `${startProperty + change}px`
|
||||
}
|
||||
|
||||
const handleMouseUp = e => {
|
||||
e.preventDefault() // Prevent highlighting while dragging
|
||||
window.removeEventListener("mousemove", handleMouseMove)
|
||||
window.removeEventListener("mouseup", handleMouseUp)
|
||||
|
||||
element.style.removeProperty("transition") // remove temporary transition override
|
||||
for (let item of document.getElementsByTagName("iframe")) {
|
||||
item.style.removeProperty("pointer-events")
|
||||
}
|
||||
|
||||
setValue(element[elementProperty])
|
||||
}
|
||||
|
||||
const handleMouseDown = e => {
|
||||
if (e.detail > 1) {
|
||||
// e.detail is the number of rapid clicks, so e.detail = 2 is
|
||||
// a double click. We want to prevent default behaviour in
|
||||
// this case as it highlights nearby selectable elements, which
|
||||
// then interferes with the resizing mousemove.
|
||||
// Putting this on the double click handler doesn't seem to
|
||||
// work, so it must go here.
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (
|
||||
e.target.hasAttribute("disabled") &&
|
||||
e.target.getAttribute("disabled") !== "false"
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
element.style.transition = `${cssProperty} 0ms` // temporarily override any height transitions
|
||||
|
||||
// iframes swallow mouseup events if your cursor ends up over it during a drag, so make them
|
||||
// temporarily non-interactive
|
||||
for (let item of document.getElementsByTagName("iframe")) {
|
||||
item.style.pointerEvents = "none"
|
||||
}
|
||||
|
||||
startProperty = element[elementProperty]
|
||||
startPosition = e[mouseMoveEventProperty]
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove)
|
||||
window.addEventListener("mouseup", handleMouseUp)
|
||||
}
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
element.style.removeProperty(cssProperty)
|
||||
}
|
||||
|
||||
node.addEventListener("mousedown", handleMouseDown)
|
||||
node.addEventListener("dblclick", handleDoubleClick)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener("mousedown", handleMouseDown)
|
||||
node.removeEventListener("dblclick", handleDoubleClick)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return [elementAction, dragHandleAction]
|
||||
}
|
||||
|
||||
export const getVerticalResizeActions = (initialValue, setValue = () => {}) => {
|
||||
return getResizeActions(
|
||||
"height",
|
||||
"pageY",
|
||||
"clientHeight",
|
||||
initialValue,
|
||||
setValue
|
||||
)
|
||||
}
|
||||
|
||||
export const getHorizontalResizeActions = (
|
||||
initialValue,
|
||||
setValue = () => {}
|
||||
) => {
|
||||
return getResizeActions(
|
||||
"width",
|
||||
"pageX",
|
||||
"clientWidth",
|
||||
initialValue,
|
||||
setValue
|
||||
)
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
<script>
|
||||
import { Icon, Body } from "@budibase/bbui"
|
||||
import { AbsTooltip, Icon, Body } from "@budibase/bbui"
|
||||
|
||||
export let title
|
||||
export let icon
|
||||
export let iconTooltip
|
||||
export let showAddButton = false
|
||||
export let showBackButton = false
|
||||
export let showCloseButton = false
|
||||
|
@ -36,7 +37,9 @@
|
|||
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
||||
{/if}
|
||||
{#if icon}
|
||||
<Icon name={icon} />
|
||||
<AbsTooltip type="info" text={iconTooltip}>
|
||||
<Icon name={icon} />
|
||||
</AbsTooltip>
|
||||
{/if}
|
||||
<div class="title">
|
||||
{#if customTitleContent}
|
||||
|
@ -68,6 +71,7 @@
|
|||
|
||||
<style>
|
||||
.panel {
|
||||
min-width: 260px;
|
||||
width: 260px;
|
||||
flex: 0 0 260px;
|
||||
background: var(--background);
|
||||
|
@ -85,6 +89,7 @@
|
|||
border-right: var(--border-light);
|
||||
}
|
||||
.panel.wide {
|
||||
min-width: 310px;
|
||||
width: 310px;
|
||||
flex: 0 0 310px;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
getEventContextBindings,
|
||||
getActionBindings,
|
||||
makeStateBinding,
|
||||
updateReferencesInObject,
|
||||
} from "builderStore/dataBinding"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
|
@ -30,6 +31,7 @@
|
|||
|
||||
let actionQuery
|
||||
let selectedAction = actions?.length ? actions[0] : null
|
||||
let originalActionIndex
|
||||
|
||||
const setUpdateActions = actions => {
|
||||
return actions
|
||||
|
@ -115,6 +117,14 @@
|
|||
if (isSelected) {
|
||||
selectedAction = actions?.length ? actions[0] : null
|
||||
}
|
||||
|
||||
// Update action binding references
|
||||
updateReferencesInObject({
|
||||
obj: actions,
|
||||
modifiedIndex: index,
|
||||
action: "delete",
|
||||
label: "actions",
|
||||
})
|
||||
}
|
||||
|
||||
const toggleActionList = () => {
|
||||
|
@ -146,9 +156,29 @@
|
|||
|
||||
function handleDndConsider(e) {
|
||||
actions = e.detail.items
|
||||
|
||||
// set the initial index of the action being dragged
|
||||
if (e.detail.info.trigger === "draggedEntered") {
|
||||
originalActionIndex = actions.findIndex(
|
||||
action => action.id === e.detail.info.id
|
||||
)
|
||||
}
|
||||
}
|
||||
function handleDndFinalize(e) {
|
||||
actions = e.detail.items
|
||||
|
||||
// Update action binding references
|
||||
updateReferencesInObject({
|
||||
obj: actions,
|
||||
modifiedIndex: actions.findIndex(
|
||||
action => action.id === e.detail.info.id
|
||||
),
|
||||
action: "move",
|
||||
label: "actions",
|
||||
originalIndex: originalActionIndex,
|
||||
})
|
||||
|
||||
originalActionIndex = -1
|
||||
}
|
||||
|
||||
const getAllBindings = (actionBindings, eventContextBindings, actions) => {
|
||||
|
@ -289,7 +319,7 @@
|
|||
</Layout>
|
||||
<Layout noPadding>
|
||||
{#if selectedActionComponent && !showAvailableActions}
|
||||
{#key selectedAction.id}
|
||||
{#key (selectedAction.id, originalActionIndex)}
|
||||
<div class="selected-action-container">
|
||||
<svelte:component
|
||||
this={selectedActionComponent}
|
||||
|
|
|
@ -55,7 +55,10 @@
|
|||
size="S"
|
||||
name="Close"
|
||||
hoverable
|
||||
on:click={() => removeButton(item._id)}
|
||||
on:click={e => {
|
||||
e.stopPropagation()
|
||||
removeButton(item._id)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -32,11 +32,14 @@
|
|||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const flipDurationMs = 150
|
||||
|
||||
let anchors = {}
|
||||
let draggableItems = []
|
||||
|
||||
// Used for controlling cursor behaviour in order to limit drag behaviour
|
||||
// to the drag handle
|
||||
let inactive = true
|
||||
|
||||
const buildDraggable = items => {
|
||||
return items
|
||||
.map(item => {
|
||||
|
@ -64,6 +67,7 @@
|
|||
}
|
||||
|
||||
const handleFinalize = e => {
|
||||
inactive = true
|
||||
updateRowOrder(e)
|
||||
dispatch("change", serialiseUpdate())
|
||||
}
|
||||
|
@ -77,24 +81,36 @@
|
|||
class="list-wrap"
|
||||
use:dndzone={{
|
||||
items: draggableItems,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: { outline: "none" },
|
||||
dragDisabled: !draggable,
|
||||
dragDisabled: !draggable || inactive,
|
||||
}}
|
||||
on:finalize={handleFinalize}
|
||||
on:consider={updateRowOrder}
|
||||
>
|
||||
{#each draggableItems as draggable (draggable.id)}
|
||||
{#each draggableItems as draggableItem (draggableItem.id)}
|
||||
<li
|
||||
on:click={() => {
|
||||
get(store).actions.select(draggableItem.id)
|
||||
}}
|
||||
on:mousedown={() => {
|
||||
get(store).actions.select()
|
||||
}}
|
||||
bind:this={anchors[draggable.id]}
|
||||
class:highlighted={draggable.id === $store.selected}
|
||||
bind:this={anchors[draggableItem.id]}
|
||||
class:highlighted={draggableItem.id === $store.selected}
|
||||
>
|
||||
<div class="left-content">
|
||||
{#if showHandle}
|
||||
<div class="handle">
|
||||
<div
|
||||
class="handle"
|
||||
aria-label="drag-handle"
|
||||
style={!inactive ? "cursor:grabbing" : "cursor:grab"}
|
||||
on:mousedown={() => {
|
||||
inactive = false
|
||||
}}
|
||||
on:mouseup={() => {
|
||||
inactive = true
|
||||
}}
|
||||
>
|
||||
<DragHandle />
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -102,8 +118,8 @@
|
|||
<div class="right-content">
|
||||
<svelte:component
|
||||
this={listType}
|
||||
anchor={anchors[draggable.item._id]}
|
||||
item={draggable.item}
|
||||
anchor={anchors[draggableItem.item._id]}
|
||||
item={draggableItem.item}
|
||||
{...listTypeProps}
|
||||
on:change={onItemChanged}
|
||||
/>
|
||||
|
@ -143,6 +159,7 @@
|
|||
--spectrum-table-row-background-color-hover,
|
||||
var(--spectrum-alias-highlight-hover)
|
||||
);
|
||||
cursor: pointer;
|
||||
}
|
||||
.list-wrap > li:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
|
@ -165,6 +182,9 @@
|
|||
display: flex;
|
||||
height: var(--spectrum-global-dimension-size-150);
|
||||
}
|
||||
.handle:hover {
|
||||
cursor: grab;
|
||||
}
|
||||
.handle :global(svg) {
|
||||
fill: var(--spectrum-global-color-gray-500);
|
||||
margin-right: var(--spacing-m);
|
||||
|
|
|
@ -156,7 +156,7 @@
|
|||
|
||||
<div class="field-configuration">
|
||||
<div class="toggle-all">
|
||||
<span />
|
||||
<span>Fields</span>
|
||||
<Toggle
|
||||
on:change={() => {
|
||||
let update = fieldList.map(field => ({
|
||||
|
@ -186,6 +186,9 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.field-configuration {
|
||||
padding-top: 8px;
|
||||
}
|
||||
.field-configuration :global(.spectrum-ActionButton) {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -204,6 +207,5 @@
|
|||
.toggle-all span {
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
font-size: 12px;
|
||||
margin-left: calc(var(--spacing-s) - 1px);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -58,7 +58,15 @@
|
|||
<div class="field-label">{readableText}</div>
|
||||
</div>
|
||||
<div class="list-item-right">
|
||||
<Toggle on:change={onToggle(item)} text="" value={item.active} thin />
|
||||
<Toggle
|
||||
on:change={onToggle(item)}
|
||||
on:click={e => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
text=""
|
||||
value={item.active}
|
||||
thin
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { store, selectedComponent, selectedScreen } from "builderStore"
|
||||
import { getComponentText } from "builderStore/componentUtils"
|
||||
import { getComponentName } from "builderStore/componentUtils"
|
||||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
||||
import DesignSection from "./DesignSection.svelte"
|
||||
import CustomStylesSection from "./CustomStylesSection.svelte"
|
||||
|
@ -43,17 +43,25 @@
|
|||
|
||||
$: id = $selectedComponent?._id
|
||||
$: id, (section = tabs[0])
|
||||
|
||||
$: componentName = getComponentName(componentInstance)
|
||||
</script>
|
||||
|
||||
{#if $selectedComponent}
|
||||
{#key $selectedComponent._id}
|
||||
<Panel {title} icon={componentDefinition?.icon} borderLeft wide>
|
||||
<Panel
|
||||
{title}
|
||||
icon={componentDefinition?.icon}
|
||||
iconTooltip={componentName}
|
||||
borderLeft
|
||||
wide
|
||||
>
|
||||
<span class="panel-title-content" slot="panel-title-content">
|
||||
<input
|
||||
class="input"
|
||||
value={title}
|
||||
{title}
|
||||
placeholder={getComponentText(componentInstance)}
|
||||
placeholder={componentName}
|
||||
on:keypress={e => {
|
||||
if (e.key.toLowerCase() === "enter") {
|
||||
e.target.blur()
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
<style>
|
||||
.app-panel {
|
||||
min-width: 410px;
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
import {
|
||||
findComponentPath,
|
||||
getComponentText,
|
||||
getComponentName,
|
||||
} from "builderStore/componentUtils"
|
||||
import { get } from "svelte/store"
|
||||
import { dndStore } from "./dndStore"
|
||||
|
@ -110,6 +111,7 @@
|
|||
on:drop={onDrop}
|
||||
text={getComponentText(component)}
|
||||
icon={getComponentIcon(component)}
|
||||
iconTooltip={getComponentName(component)}
|
||||
withArrow={componentHasChildren(component)}
|
||||
indentLevel={level}
|
||||
selected={$store.selectedComponentId === component._id}
|
||||
|
|
|
@ -1,21 +1,55 @@
|
|||
<script>
|
||||
import ScreenList from "./ScreenList/index.svelte"
|
||||
import ComponentList from "./ComponentList/index.svelte"
|
||||
import { getHorizontalResizeActions } from "components/common/resizable"
|
||||
|
||||
const [resizable, resizableHandle] = getHorizontalResizeActions()
|
||||
</script>
|
||||
|
||||
<div class="panel">
|
||||
<ScreenList />
|
||||
<ComponentList />
|
||||
<div class="panel" use:resizable>
|
||||
<div class="content">
|
||||
<ScreenList />
|
||||
<ComponentList />
|
||||
</div>
|
||||
<div class="divider">
|
||||
<div class="dividerClickExtender" role="separator" use:resizableHandle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.panel {
|
||||
display: flex;
|
||||
min-width: 270px;
|
||||
width: 310px;
|
||||
height: 100%;
|
||||
border-right: var(--border-light);
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.divider {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 2px;
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
transition: background 130ms ease-out;
|
||||
}
|
||||
.divider:hover {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.dividerClickExtender {
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
height: 100%;
|
||||
width: 12px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,108 +1,50 @@
|
|||
<script>
|
||||
import { Layout } from "@budibase/bbui"
|
||||
import {
|
||||
store,
|
||||
sortedScreens,
|
||||
userSelectedResourceMap,
|
||||
screensHeight,
|
||||
} from "builderStore"
|
||||
import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import RoleIndicator from "./RoleIndicator.svelte"
|
||||
import DropdownMenu from "./DropdownMenu.svelte"
|
||||
import { onMount } from "svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { getVerticalResizeActions } from "components/common/resizable"
|
||||
import NavHeader from "components/common/NavHeader.svelte"
|
||||
|
||||
let search = false
|
||||
let resizing = false
|
||||
let searchValue = ""
|
||||
const [resizable, resizableHandle] = getVerticalResizeActions()
|
||||
|
||||
let container
|
||||
let searching = false
|
||||
let searchValue = ""
|
||||
let screensContainer
|
||||
let scrolling = false
|
||||
let previousHeight = null
|
||||
let dragOffset
|
||||
|
||||
$: filteredScreens = getFilteredScreens($sortedScreens, searchValue)
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
$: search ? openSearch() : closeSearch()
|
||||
|
||||
const openSearch = async () => {
|
||||
const handleOpenSearch = async () => {
|
||||
screensContainer.scroll({ top: 0, behavior: "smooth" })
|
||||
previousHeight = $screensHeight
|
||||
$screensHeight = "calc(100% + 1px)"
|
||||
}
|
||||
|
||||
const closeSearch = async () => {
|
||||
if (previousHeight) {
|
||||
// Restore previous height and wait for animation
|
||||
$screensHeight = previousHeight
|
||||
previousHeight = null
|
||||
await sleep(300)
|
||||
$: {
|
||||
if (searching) {
|
||||
handleOpenSearch()
|
||||
}
|
||||
}
|
||||
|
||||
const getFilteredScreens = (screens, search) => {
|
||||
const getFilteredScreens = (screens, searchValue) => {
|
||||
return screens.filter(screen => {
|
||||
return !search || screen.routing.route.includes(search)
|
||||
return !searchValue || screen.routing.route.includes(searchValue)
|
||||
})
|
||||
}
|
||||
|
||||
const handleScroll = e => {
|
||||
scrolling = e.target.scrollTop !== 0
|
||||
}
|
||||
|
||||
const startResizing = e => {
|
||||
// Reset the height store to match the true height
|
||||
$screensHeight = `${container.getBoundingClientRect().height}px`
|
||||
|
||||
// Store an offset to easily compute new height when moving the mouse
|
||||
dragOffset = parseInt($screensHeight) - e.clientY
|
||||
|
||||
// Add event listeners
|
||||
resizing = true
|
||||
document.addEventListener("mousemove", resize)
|
||||
document.addEventListener("mouseup", stopResizing)
|
||||
}
|
||||
|
||||
const resize = e => {
|
||||
// Prevent negative heights as this screws with layout
|
||||
const newHeight = Math.max(0, e.clientY + dragOffset)
|
||||
if (newHeight == null || isNaN(newHeight)) {
|
||||
return
|
||||
}
|
||||
$screensHeight = `${newHeight}px`
|
||||
}
|
||||
|
||||
const stopResizing = () => {
|
||||
resizing = false
|
||||
document.removeEventListener("mousemove", resize)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Ensure we aren't stuck at 100% height from leaving while searching
|
||||
if ($screensHeight == null || isNaN(parseInt($screensHeight))) {
|
||||
$screensHeight = "210px"
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window />
|
||||
<div
|
||||
class="screens"
|
||||
class:search
|
||||
class:resizing
|
||||
style={`height:${$screensHeight};`}
|
||||
bind:this={container}
|
||||
>
|
||||
<div class="screens" class:searching use:resizable>
|
||||
<div class="header" class:scrolling>
|
||||
<NavHeader
|
||||
title="Screens"
|
||||
placeholder="Search for screens"
|
||||
bind:value={searchValue}
|
||||
bind:search
|
||||
bind:search={searching}
|
||||
onAdd={() => $goto("../new")}
|
||||
/>
|
||||
</div>
|
||||
|
@ -110,6 +52,7 @@
|
|||
{#if filteredScreens?.length}
|
||||
{#each filteredScreens as screen (screen._id)}
|
||||
<NavItem
|
||||
scrollable
|
||||
icon={screen.routing.homeScreen ? "Home" : null}
|
||||
indentLevel={0}
|
||||
selected={$store.selectedScreenId === screen._id}
|
||||
|
@ -135,9 +78,11 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
role="separator"
|
||||
disabled={searching}
|
||||
class="divider"
|
||||
on:mousedown={startResizing}
|
||||
on:dblclick={() => screensHeight.set("210px")}
|
||||
class:disabled={searching}
|
||||
use:resizableHandle
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -148,14 +93,12 @@
|
|||
min-height: 147px;
|
||||
max-height: calc(100% - 147px);
|
||||
position: relative;
|
||||
transition: height 300ms ease-out;
|
||||
transition: height 300ms ease-out, max-height 300ms ease-out;
|
||||
height: 210px;
|
||||
}
|
||||
.screens.search {
|
||||
max-height: none;
|
||||
}
|
||||
.screens.resizing {
|
||||
user-select: none;
|
||||
cursor: row-resize;
|
||||
.screens.searching {
|
||||
max-height: 100%;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
@ -177,9 +120,6 @@
|
|||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.screens.resizing .content {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.screens :global(.nav-item) {
|
||||
padding-right: 8px !important;
|
||||
|
@ -217,4 +157,10 @@
|
|||
.divider:hover:after {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.divider.disabled {
|
||||
cursor: auto;
|
||||
}
|
||||
.divider.disabled:after {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
}
|
||||
|
||||
.content {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
|
|
@ -6056,18 +6056,6 @@
|
|||
"options": ["Create", "Update", "View"],
|
||||
"defaultValue": "Create"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Title",
|
||||
"key": "title",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Description",
|
||||
"key": "description",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"dependsOn": {
|
||||
|
@ -6075,7 +6063,7 @@
|
|||
"value": "Create",
|
||||
"invert": true
|
||||
},
|
||||
"name": "Row details",
|
||||
"name": "Row ID",
|
||||
"info": "<a href='https://docs.budibase.com/docs/form-block' target='_blank'>How to pass a row ID using bindings</a>",
|
||||
"settings": [
|
||||
{
|
||||
|
@ -6095,8 +6083,20 @@
|
|||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Fields",
|
||||
"name": "Details",
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Title",
|
||||
"key": "title",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Description",
|
||||
"key": "description",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "fieldConfiguration",
|
||||
"key": "fields",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
* @Budibase/backend
|
|
@ -133,9 +133,14 @@ export async function exportRows(
|
|||
|
||||
let result = await search({ tableId, query: requestQuery, sort, sortOrder })
|
||||
let rows: Row[] = []
|
||||
let headers
|
||||
|
||||
if (!tableName) {
|
||||
throw new HTTPError("Could not find table name.", 400)
|
||||
}
|
||||
const schema = datasource.entities[tableName].schema
|
||||
|
||||
// Filter data to only specified columns if required
|
||||
|
||||
if (columns && columns.length) {
|
||||
for (let i = 0; i < result.rows.length; i++) {
|
||||
rows[i] = {}
|
||||
|
@ -143,22 +148,17 @@ export async function exportRows(
|
|||
rows[i][column] = result.rows[i][column]
|
||||
}
|
||||
}
|
||||
headers = columns
|
||||
} else {
|
||||
rows = result.rows
|
||||
}
|
||||
|
||||
if (!tableName) {
|
||||
throw new HTTPError("Could not find table name.", 400)
|
||||
}
|
||||
const schema = datasource.entities[tableName].schema
|
||||
let exportRows = cleanExportRows(rows, schema, format, columns)
|
||||
|
||||
let headers = Object.keys(schema)
|
||||
|
||||
let content: string
|
||||
switch (format) {
|
||||
case exporters.Format.CSV:
|
||||
content = exporters.csv(headers, exportRows)
|
||||
content = exporters.csv(headers ?? Object.keys(schema), exportRows)
|
||||
break
|
||||
case exporters.Format.JSON:
|
||||
content = exporters.json(exportRows)
|
||||
|
|
|
@ -110,7 +110,7 @@ export async function exportRows(
|
|||
|
||||
let rows: Row[] = []
|
||||
let schema = table.schema
|
||||
|
||||
let headers
|
||||
// Filter data to only specified columns if required
|
||||
if (columns && columns.length) {
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
|
@ -119,6 +119,7 @@ export async function exportRows(
|
|||
rows[i][column] = result[i][column]
|
||||
}
|
||||
}
|
||||
headers = columns
|
||||
} else {
|
||||
rows = result
|
||||
}
|
||||
|
@ -127,7 +128,7 @@ export async function exportRows(
|
|||
if (format === Format.CSV) {
|
||||
return {
|
||||
fileName: "export.csv",
|
||||
content: csv(Object.keys(rows[0]), exportRows),
|
||||
content: csv(headers ?? Object.keys(rows[0]), exportRows),
|
||||
}
|
||||
} else if (format === Format.JSON) {
|
||||
return {
|
||||
|
|
|
@ -18,7 +18,6 @@ jest.mock("../../../utilities/rowProcessor", () => ({
|
|||
|
||||
jest.mock("../../../api/controllers/view/exporters", () => ({
|
||||
...jest.requireActual("../../../api/controllers/view/exporters"),
|
||||
csv: jest.fn(),
|
||||
Format: {
|
||||
CSV: "csv",
|
||||
},
|
||||
|
@ -102,5 +101,32 @@ describe("external row sdk", () => {
|
|||
new HTTPError("Could not find table name.", 400)
|
||||
)
|
||||
})
|
||||
|
||||
it("should only export specified columns", async () => {
|
||||
mockDatasourcesGet.mockImplementation(async () => ({
|
||||
entities: {
|
||||
tablename: {
|
||||
schema: {
|
||||
name: {},
|
||||
age: {},
|
||||
dob: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
const headers = ["name", "dob"]
|
||||
|
||||
const result = await exportRows({
|
||||
tableId: "datasource__tablename",
|
||||
format: Format.CSV,
|
||||
query: {},
|
||||
columns: headers,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
fileName: "export.csv",
|
||||
content: `"name","dob"`,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -315,7 +315,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
|||
new Date(docValue).getTime() > new Date(testValue.high).getTime()
|
||||
)
|
||||
}
|
||||
throw "Cannot perform range filter - invalid type."
|
||||
return false
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -130,32 +130,28 @@ describe("runLuceneQuery", () => {
|
|||
expect(runLuceneQuery(docs, query).map(row => row.order_id)).toEqual([2])
|
||||
})
|
||||
|
||||
it("should throw an error is an invalid doc value is passed into a range filter", async () => {
|
||||
it("should return return all docs if an invalid doc value is passed into a range filter", async () => {
|
||||
const docs = [
|
||||
{
|
||||
order_id: 4,
|
||||
customer_id: 1758,
|
||||
order_status: 5,
|
||||
order_date: "{{ Binding.INVALID }}",
|
||||
required_date: "2017-03-05T00:00:00.000Z",
|
||||
shipped_date: "2017-03-03T00:00:00.000Z",
|
||||
store_id: 2,
|
||||
staff_id: 7,
|
||||
description: undefined,
|
||||
label: "",
|
||||
},
|
||||
]
|
||||
const query = buildQuery("range", {
|
||||
order_date: {
|
||||
low: "2016-01-04T00:00:00.000Z",
|
||||
high: "2016-01-11T00:00:00.000Z",
|
||||
},
|
||||
})
|
||||
expect(() =>
|
||||
runLuceneQuery(
|
||||
[
|
||||
{
|
||||
order_id: 4,
|
||||
customer_id: 1758,
|
||||
order_status: 5,
|
||||
order_date: "INVALID",
|
||||
required_date: "2017-03-05T00:00:00.000Z",
|
||||
shipped_date: "2017-03-03T00:00:00.000Z",
|
||||
store_id: 2,
|
||||
staff_id: 7,
|
||||
description: undefined,
|
||||
label: "",
|
||||
},
|
||||
],
|
||||
query
|
||||
)
|
||||
).toThrowError("Cannot perform range filter - invalid type.")
|
||||
expect(runLuceneQuery(docs, query)).toEqual(docs)
|
||||
})
|
||||
|
||||
it("should return rows with matches on empty filter", () => {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
* @Budibase/backend
|
Loading…
Reference in New Issue