Merge branch 'master' into chore/remove-unused-packages
This commit is contained in:
commit
7951aac36b
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"root": true,
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"es6": true,
|
"es6": true,
|
||||||
|
|
|
@ -12,4 +12,5 @@ packages/pro/coverage
|
||||||
packages/account-portal/packages/ui/build
|
packages/account-portal/packages/ui/build
|
||||||
packages/account-portal/packages/ui/.routify
|
packages/account-portal/packages/ui/.routify
|
||||||
packages/account-portal/packages/server/build
|
packages/account-portal/packages/server/build
|
||||||
|
packages/account-portal/packages/server/coverage
|
||||||
**/*.ivm.bundle.js
|
**/*.ivm.bundle.js
|
|
@ -70,10 +70,10 @@ sed -i "s#COUCHDB_ERLANG_COOKIE#${COUCHDB_ERLANG_COOKIE}#g" /opt/clouseau/clouse
|
||||||
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
|
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
|
||||||
|
|
||||||
# Start CouchDB.
|
# Start CouchDB.
|
||||||
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
/docker-entrypoint.sh /opt/couchdb/bin/couchdb > /dev/stdout 2>&1 &
|
||||||
|
|
||||||
# Start SQS.
|
# Start SQS. Use 127.0.0.1 instead of localhost to avoid IPv6 issues.
|
||||||
/opt/sqs/sqs --server "http://localhost:5984" --data-dir ${DATA_DIR}/sqs --bind-address=0.0.0.0 &
|
/opt/sqs/sqs --server "http://127.0.0.1:5984" --data-dir ${DATA_DIR}/sqs --bind-address=0.0.0.0 > /dev/stdout 2>&1 &
|
||||||
|
|
||||||
# Wait for CouchDB to start up.
|
# Wait for CouchDB to start up.
|
||||||
while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do
|
while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.24.0",
|
"version": "2.24.1",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -1,22 +1,25 @@
|
||||||
|
// These class names will never trigger a callback if clicked, no matter what
|
||||||
const ignoredClasses = [
|
const ignoredClasses = [
|
||||||
".download-js-link",
|
".download-js-link",
|
||||||
".spectrum-Menu",
|
".spectrum-Menu",
|
||||||
".date-time-popover",
|
".date-time-popover",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// These class names will only trigger a callback when clicked if the registered
|
||||||
|
// component is not nested inside them. For example, clicking inside a modal
|
||||||
|
// will not close the modal, or clicking inside a popover will not close the
|
||||||
|
// popover.
|
||||||
const conditionallyIgnoredClasses = [
|
const conditionallyIgnoredClasses = [
|
||||||
".spectrum-Underlay",
|
".spectrum-Underlay",
|
||||||
".drawer-wrapper",
|
".drawer-wrapper",
|
||||||
".spectrum-Popover",
|
".spectrum-Popover",
|
||||||
]
|
]
|
||||||
let clickHandlers = []
|
let clickHandlers = []
|
||||||
|
let candidateTarget
|
||||||
|
|
||||||
/**
|
// Processes a "click outside" event and invokes callbacks if our source element
|
||||||
* Handle a body click event
|
// is valid
|
||||||
*/
|
|
||||||
const handleClick = event => {
|
const handleClick = event => {
|
||||||
// Treat right clicks (context menu events) as normal clicks
|
|
||||||
const eventType = event.type === "contextmenu" ? "click" : event.type
|
|
||||||
|
|
||||||
// Ignore click if this is an ignored class
|
// Ignore click if this is an ignored class
|
||||||
if (event.target.closest('[data-ignore-click-outside="true"]')) {
|
if (event.target.closest('[data-ignore-click-outside="true"]')) {
|
||||||
return
|
return
|
||||||
|
@ -29,11 +32,6 @@ const handleClick = event => {
|
||||||
|
|
||||||
// Process handlers
|
// Process handlers
|
||||||
clickHandlers.forEach(handler => {
|
clickHandlers.forEach(handler => {
|
||||||
// Check that we're the right kind of click event
|
|
||||||
if (handler.allowedType && eventType !== handler.allowedType) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the click isn't inside the target
|
// Check that the click isn't inside the target
|
||||||
if (handler.element.contains(event.target)) {
|
if (handler.element.contains(event.target)) {
|
||||||
return
|
return
|
||||||
|
@ -51,17 +49,43 @@ const handleClick = event => {
|
||||||
handler.callback?.(event)
|
handler.callback?.(event)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
document.documentElement.addEventListener("click", handleClick, true)
|
|
||||||
document.documentElement.addEventListener("mousedown", handleClick, true)
|
// On mouse up we only trigger a "click outside" callback if we targetted the
|
||||||
document.documentElement.addEventListener("contextmenu", handleClick, true)
|
// same element that we did on mouse down. This fixes all sorts of issues where
|
||||||
|
// we get annoying callbacks firing when we drag to select text.
|
||||||
|
const handleMouseUp = e => {
|
||||||
|
if (candidateTarget === e.target) {
|
||||||
|
handleClick(e)
|
||||||
|
}
|
||||||
|
candidateTarget = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// On mouse down we store which element was targetted for comparison later
|
||||||
|
const handleMouseDown = e => {
|
||||||
|
// Only handle the primary mouse button here.
|
||||||
|
// We handle context menu (right click) events in another handler.
|
||||||
|
if (e.button !== 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
candidateTarget = e.target
|
||||||
|
|
||||||
|
// Clear any previous listeners in case of multiple down events, and register
|
||||||
|
// a single mouse up listener
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp)
|
||||||
|
document.addEventListener("mouseup", handleMouseUp, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global singleton listeners for our events
|
||||||
|
document.addEventListener("mousedown", handleMouseDown)
|
||||||
|
document.addEventListener("contextmenu", handleClick)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds or updates a click handler
|
* Adds or updates a click handler
|
||||||
*/
|
*/
|
||||||
const updateHandler = (id, element, anchor, callback, allowedType) => {
|
const updateHandler = (id, element, anchor, callback) => {
|
||||||
let existingHandler = clickHandlers.find(x => x.id === id)
|
let existingHandler = clickHandlers.find(x => x.id === id)
|
||||||
if (!existingHandler) {
|
if (!existingHandler) {
|
||||||
clickHandlers.push({ id, element, anchor, callback, allowedType })
|
clickHandlers.push({ id, element, anchor, callback })
|
||||||
} else {
|
} else {
|
||||||
existingHandler.callback = callback
|
existingHandler.callback = callback
|
||||||
}
|
}
|
||||||
|
@ -88,8 +112,7 @@ export default (element, opts) => {
|
||||||
const callback =
|
const callback =
|
||||||
newOpts?.callback || (typeof newOpts === "function" ? newOpts : null)
|
newOpts?.callback || (typeof newOpts === "function" ? newOpts : null)
|
||||||
const anchor = newOpts?.anchor || element
|
const anchor = newOpts?.anchor || element
|
||||||
const allowedType = newOpts?.allowedType || "click"
|
updateHandler(id, element, anchor, callback)
|
||||||
updateHandler(id, element, anchor, callback, allowedType)
|
|
||||||
}
|
}
|
||||||
update(opts)
|
update(opts)
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import NumberInput from "./NumberInput.svelte"
|
import NumberInput from "./NumberInput.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import isoWeek from "dayjs/plugin/isoWeek"
|
||||||
|
|
||||||
|
dayjs.extend(isoWeek)
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
|
@ -43,7 +46,7 @@
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
let monthEnd = monthStart.endOf("month")
|
let monthEnd = monthStart.endOf("month")
|
||||||
let calendarStart = monthStart.startOf("week")
|
let calendarStart = monthStart.startOf("isoWeek")
|
||||||
const numWeeks = Math.ceil((monthEnd.diff(calendarStart, "day") + 1) / 7)
|
const numWeeks = Math.ceil((monthEnd.diff(calendarStart, "day") + 1) / 7)
|
||||||
|
|
||||||
let mondays = []
|
let mondays = []
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { Body, Label, Input } from "@budibase/bbui"
|
import { Body, Label } from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
export let parameters
|
export let parameters
|
||||||
|
export let bindings
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!parameters.confirm) {
|
if (!parameters.confirm) {
|
||||||
|
@ -15,11 +17,18 @@
|
||||||
<Body size="S">Enter the message you wish to display to the user.</Body>
|
<Body size="S">Enter the message you wish to display to the user.</Body>
|
||||||
<div class="params">
|
<div class="params">
|
||||||
<Label small>Title</Label>
|
<Label small>Title</Label>
|
||||||
<Input placeholder="Prompt User" bind:value={parameters.customTitleText} />
|
<DrawerBindableInput
|
||||||
|
placeholder="Title"
|
||||||
|
value={parameters.customTitleText}
|
||||||
|
on:change={e => (parameters.customTitleText = e.detail)}
|
||||||
|
{bindings}
|
||||||
|
/>
|
||||||
<Label small>Message</Label>
|
<Label small>Message</Label>
|
||||||
<Input
|
<DrawerBindableInput
|
||||||
placeholder="Are you sure you want to continue?"
|
placeholder="Are you sure you want to continue?"
|
||||||
bind:value={parameters.confirmText}
|
value={parameters.confirmText}
|
||||||
|
on:change={e => (parameters.confirmText = e.detail)}
|
||||||
|
{bindings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,26 +21,24 @@
|
||||||
const currentStep = derived(multiStepStore, state => state.currentStep)
|
const currentStep = derived(multiStepStore, state => state.currentStep)
|
||||||
const componentType = "@budibase/standard-components/multistepformblockstep"
|
const componentType = "@budibase/standard-components/multistepformblockstep"
|
||||||
|
|
||||||
|
setContext("multi-step-form-block", multiStepStore)
|
||||||
|
|
||||||
let cachedValue
|
let cachedValue
|
||||||
let cachedInstance = {}
|
let cachedInstance = {}
|
||||||
|
|
||||||
$: if (!isEqual(cachedValue, value)) {
|
$: if (!isEqual(cachedValue, value)) {
|
||||||
cachedValue = value
|
cachedValue = value
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (!isEqual(componentInstance, cachedInstance)) {
|
$: if (!isEqual(componentInstance, cachedInstance)) {
|
||||||
cachedInstance = componentInstance
|
cachedInstance = componentInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext("multi-step-form-block", multiStepStore)
|
|
||||||
|
|
||||||
$: stepCount = cachedValue?.length || 0
|
$: stepCount = cachedValue?.length || 0
|
||||||
$: updateStore(stepCount)
|
$: updateStore(stepCount)
|
||||||
$: dataSource = getDatasourceForProvider($selectedScreen, cachedInstance)
|
$: dataSource = getDatasourceForProvider($selectedScreen, cachedInstance)
|
||||||
$: emitCurrentStep($currentStep)
|
$: emitCurrentStep($currentStep)
|
||||||
$: stepLabel = getStepLabel($multiStepStore)
|
$: stepLabel = getStepLabel($multiStepStore)
|
||||||
$: stepDef = getDefinition(stepLabel)
|
$: stepDef = getDefinition(stepLabel)
|
||||||
$: stepSettings = cachedValue?.[$currentStep] || {}
|
$: savedInstance = cachedValue?.[$currentStep] || {}
|
||||||
$: defaults = Utils.buildMultiStepFormBlockDefaultProps({
|
$: defaults = Utils.buildMultiStepFormBlockDefaultProps({
|
||||||
_id: cachedInstance._id,
|
_id: cachedInstance._id,
|
||||||
stepCount: $multiStepStore.stepCount,
|
stepCount: $multiStepStore.stepCount,
|
||||||
|
@ -48,14 +46,16 @@
|
||||||
actionType: cachedInstance.actionType,
|
actionType: cachedInstance.actionType,
|
||||||
dataSource: cachedInstance.dataSource,
|
dataSource: cachedInstance.dataSource,
|
||||||
})
|
})
|
||||||
|
// For backwards compatibility we need to sometimes manually set base
|
||||||
|
// properties like _id and _component as we didn't used to save these
|
||||||
$: stepInstance = {
|
$: stepInstance = {
|
||||||
_id: Helpers.uuid(),
|
_id: savedInstance._id || Helpers.uuid(),
|
||||||
_component: componentType,
|
_component: savedInstance._component || componentType,
|
||||||
_instanceName: `Step ${currentStep + 1}`,
|
_instanceName: `Step ${currentStep + 1}`,
|
||||||
title: stepSettings.title ?? defaults?.title,
|
title: savedInstance.title ?? defaults?.title,
|
||||||
buttons: stepSettings.buttons || defaults?.buttons,
|
buttons: savedInstance.buttons || defaults?.buttons,
|
||||||
fields: stepSettings.fields,
|
fields: savedInstance.fields,
|
||||||
desc: stepSettings.desc,
|
desc: savedInstance.desc,
|
||||||
|
|
||||||
// Needed for field configuration
|
// Needed for field configuration
|
||||||
dataSource,
|
dataSource,
|
||||||
|
@ -92,7 +92,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const addStep = () => {
|
const addStep = () => {
|
||||||
value = value.toSpliced($currentStep + 1, 0, {})
|
const newInstance = componentStore.createInstance(componentType)
|
||||||
|
value = value.toSpliced($currentStep + 1, 0, newInstance)
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
multiStepStore.update(state => ({
|
multiStepStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
import { ActionButton } from "@budibase/bbui"
|
import { ActionButton, AbsTooltip } from "@budibase/bbui"
|
||||||
|
|
||||||
const multiStepStore = getContext("multi-step-form-block")
|
const multiStepStore = getContext("multi-step-form-block")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -28,45 +28,49 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="step-actions">
|
<div class="step-actions">
|
||||||
<ActionButton
|
<AbsTooltip text="Previous step" noWrap>
|
||||||
size="S"
|
<ActionButton
|
||||||
secondary
|
size="S"
|
||||||
icon="ChevronLeft"
|
secondary
|
||||||
disabled={currentStep === 0}
|
icon="ChevronLeft"
|
||||||
on:click={() => {
|
disabled={currentStep === 0}
|
||||||
stepAction("previousStep")
|
on:click={() => {
|
||||||
}}
|
stepAction("previousStep")
|
||||||
tooltip={"Previous step"}
|
}}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
</AbsTooltip>
|
||||||
size="S"
|
<AbsTooltip text="Next step" noWrap>
|
||||||
secondary
|
<ActionButton
|
||||||
disabled={currentStep === stepCount - 1}
|
size="S"
|
||||||
icon="ChevronRight"
|
secondary
|
||||||
on:click={() => {
|
disabled={currentStep === stepCount - 1}
|
||||||
stepAction("nextStep")
|
icon="ChevronRight"
|
||||||
}}
|
on:click={() => {
|
||||||
tooltip={"Next step"}
|
stepAction("nextStep")
|
||||||
/>
|
}}
|
||||||
<ActionButton
|
/>
|
||||||
size="S"
|
</AbsTooltip>
|
||||||
secondary
|
<AbsTooltip text="Remove step" noWrap>
|
||||||
icon="Close"
|
<ActionButton
|
||||||
disabled={stepCount === 1}
|
size="S"
|
||||||
on:click={() => {
|
secondary
|
||||||
stepAction("removeStep")
|
icon="Close"
|
||||||
}}
|
disabled={stepCount === 1}
|
||||||
tooltip={"Remove step"}
|
on:click={() => {
|
||||||
/>
|
stepAction("removeStep")
|
||||||
<ActionButton
|
}}
|
||||||
size="S"
|
/>
|
||||||
secondary
|
</AbsTooltip>
|
||||||
icon="MultipleAdd"
|
<AbsTooltip text="Add step" noWrap>
|
||||||
on:click={() => {
|
<ActionButton
|
||||||
stepAction("addStep")
|
size="S"
|
||||||
}}
|
secondary
|
||||||
tooltip={"Add step"}
|
icon="MultipleAdd"
|
||||||
/>
|
on:click={() => {
|
||||||
|
stepAction("addStep")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AbsTooltip>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,7 @@ const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
|
||||||
return createComponent(
|
return createComponent(
|
||||||
"@budibase/standard-components/labelfield",
|
"@budibase/standard-components/labelfield",
|
||||||
{
|
{
|
||||||
|
_id: column.field,
|
||||||
_instanceName: column.field,
|
_instanceName: column.field,
|
||||||
active: column.active,
|
active: column.active,
|
||||||
field: column.field,
|
field: column.field,
|
||||||
|
|
|
@ -65,6 +65,7 @@ describe("getColumns", () => {
|
||||||
it("returns the selected and unselected fields in the modern format, respecting the original order", ctx => {
|
it("returns the selected and unselected fields in the modern format, respecting the original order", ctx => {
|
||||||
expect(ctx.columns.sortable).toEqual([
|
expect(ctx.columns.sortable).toEqual([
|
||||||
{
|
{
|
||||||
|
_id: "three",
|
||||||
_instanceName: "three",
|
_instanceName: "three",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -73,6 +74,7 @@ describe("getColumns", () => {
|
||||||
label: "three label",
|
label: "three label",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "two",
|
||||||
_instanceName: "two",
|
_instanceName: "two",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -81,6 +83,7 @@ describe("getColumns", () => {
|
||||||
label: "two label",
|
label: "two label",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "one",
|
||||||
_instanceName: "one",
|
_instanceName: "one",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -91,6 +94,7 @@ describe("getColumns", () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(ctx.columns.primary).toEqual({
|
expect(ctx.columns.primary).toEqual({
|
||||||
|
_id: "four",
|
||||||
_instanceName: "four",
|
_instanceName: "four",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -115,6 +119,7 @@ describe("getColumns", () => {
|
||||||
it("returns all columns, with non-hidden columns automatically selected", ctx => {
|
it("returns all columns, with non-hidden columns automatically selected", ctx => {
|
||||||
expect(ctx.columns.sortable).toEqual([
|
expect(ctx.columns.sortable).toEqual([
|
||||||
{
|
{
|
||||||
|
_id: "two",
|
||||||
_instanceName: "two",
|
_instanceName: "two",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -123,6 +128,7 @@ describe("getColumns", () => {
|
||||||
label: "two",
|
label: "two",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "three",
|
||||||
_instanceName: "three",
|
_instanceName: "three",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -131,6 +137,7 @@ describe("getColumns", () => {
|
||||||
label: "three",
|
label: "three",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "one",
|
||||||
_instanceName: "one",
|
_instanceName: "one",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -141,6 +148,7 @@ describe("getColumns", () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(ctx.columns.primary).toEqual({
|
expect(ctx.columns.primary).toEqual({
|
||||||
|
_id: "four",
|
||||||
_instanceName: "four",
|
_instanceName: "four",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -173,6 +181,7 @@ describe("getColumns", () => {
|
||||||
it("returns all columns, including those missing from the initial data", ctx => {
|
it("returns all columns, including those missing from the initial data", ctx => {
|
||||||
expect(ctx.columns.sortable).toEqual([
|
expect(ctx.columns.sortable).toEqual([
|
||||||
{
|
{
|
||||||
|
_id: "three",
|
||||||
_instanceName: "three",
|
_instanceName: "three",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -181,6 +190,7 @@ describe("getColumns", () => {
|
||||||
label: "three label",
|
label: "three label",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "two",
|
||||||
_instanceName: "two",
|
_instanceName: "two",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -189,6 +199,7 @@ describe("getColumns", () => {
|
||||||
label: "two",
|
label: "two",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "one",
|
||||||
_instanceName: "one",
|
_instanceName: "one",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -199,6 +210,7 @@ describe("getColumns", () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(ctx.columns.primary).toEqual({
|
expect(ctx.columns.primary).toEqual({
|
||||||
|
_id: "four",
|
||||||
_instanceName: "four",
|
_instanceName: "four",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -228,6 +240,7 @@ describe("getColumns", () => {
|
||||||
it("returns all valid columns, excluding those that aren't valid for the schema", ctx => {
|
it("returns all valid columns, excluding those that aren't valid for the schema", ctx => {
|
||||||
expect(ctx.columns.sortable).toEqual([
|
expect(ctx.columns.sortable).toEqual([
|
||||||
{
|
{
|
||||||
|
_id: "three",
|
||||||
_instanceName: "three",
|
_instanceName: "three",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -236,6 +249,7 @@ describe("getColumns", () => {
|
||||||
label: "three label",
|
label: "three label",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "two",
|
||||||
_instanceName: "two",
|
_instanceName: "two",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -244,6 +258,7 @@ describe("getColumns", () => {
|
||||||
label: "two",
|
label: "two",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "one",
|
||||||
_instanceName: "one",
|
_instanceName: "one",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -254,6 +269,7 @@ describe("getColumns", () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(ctx.columns.primary).toEqual({
|
expect(ctx.columns.primary).toEqual({
|
||||||
|
_id: "four",
|
||||||
_instanceName: "four",
|
_instanceName: "four",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -318,6 +334,7 @@ describe("getColumns", () => {
|
||||||
beforeEach(ctx => {
|
beforeEach(ctx => {
|
||||||
ctx.updateSortable([
|
ctx.updateSortable([
|
||||||
{
|
{
|
||||||
|
_id: "three",
|
||||||
_instanceName: "three",
|
_instanceName: "three",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -326,6 +343,7 @@ describe("getColumns", () => {
|
||||||
label: "three",
|
label: "three",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "one",
|
||||||
_instanceName: "one",
|
_instanceName: "one",
|
||||||
active: true,
|
active: true,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
@ -334,6 +352,7 @@ describe("getColumns", () => {
|
||||||
label: "one",
|
label: "one",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
_id: "two",
|
||||||
_instanceName: "two",
|
_instanceName: "two",
|
||||||
active: false,
|
active: false,
|
||||||
columnType: "foo",
|
columnType: "foo",
|
||||||
|
|
|
@ -25,6 +25,6 @@
|
||||||
name="field"
|
name="field"
|
||||||
headings
|
headings
|
||||||
options={SchemaTypeOptionsExpanded}
|
options={SchemaTypeOptionsExpanded}
|
||||||
compare={(option, value) => option.type === value.type}
|
compare={(option, value) => option.type === value?.type}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
|
@ -695,7 +695,7 @@
|
||||||
menuItems={schemaMenuItems}
|
menuItems={schemaMenuItems}
|
||||||
showMenu={!schemaReadOnly}
|
showMenu={!schemaReadOnly}
|
||||||
readOnly={schemaReadOnly}
|
readOnly={schemaReadOnly}
|
||||||
compare={(option, value) => option.type === value.type}
|
compare={(option, value) => option.type === value?.type}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -253,6 +253,7 @@ export const SchemaTypeOptions = [
|
||||||
{ label: "Number", value: FieldType.NUMBER },
|
{ label: "Number", value: FieldType.NUMBER },
|
||||||
{ label: "Boolean", value: FieldType.BOOLEAN },
|
{ label: "Boolean", value: FieldType.BOOLEAN },
|
||||||
{ label: "Datetime", value: FieldType.DATETIME },
|
{ label: "Datetime", value: FieldType.DATETIME },
|
||||||
|
{ label: "JSON", value: FieldType.JSON },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const SchemaTypeOptionsExpanded = SchemaTypeOptions.map(el => ({
|
export const SchemaTypeOptionsExpanded = SchemaTypeOptions.map(el => ({
|
||||||
|
|
|
@ -1106,50 +1106,51 @@ export const getAllStateVariables = () => {
|
||||||
getAllAssets().forEach(asset => {
|
getAllAssets().forEach(asset => {
|
||||||
findAllMatchingComponents(asset.props, component => {
|
findAllMatchingComponents(asset.props, component => {
|
||||||
const settings = componentStore.getComponentSettings(component._component)
|
const settings = componentStore.getComponentSettings(component._component)
|
||||||
|
const nestedTypes = [
|
||||||
|
"buttonConfiguration",
|
||||||
|
"fieldConfiguration",
|
||||||
|
"stepConfiguration",
|
||||||
|
]
|
||||||
|
|
||||||
|
// Extracts all event settings from a component instance.
|
||||||
|
// Recurses into nested types to find all event-like settings at any
|
||||||
|
// depth.
|
||||||
const parseEventSettings = (settings, comp) => {
|
const parseEventSettings = (settings, comp) => {
|
||||||
|
if (!settings?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract top level event settings
|
||||||
settings
|
settings
|
||||||
.filter(setting => setting.type === "event")
|
.filter(setting => setting.type === "event")
|
||||||
.forEach(setting => {
|
.forEach(setting => {
|
||||||
eventSettings.push(comp[setting.key])
|
eventSettings.push(comp[setting.key])
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const parseComponentSettings = (settings, component) => {
|
// Recurse into any nested instance types
|
||||||
// Parse the nested button configurations
|
|
||||||
settings
|
settings
|
||||||
.filter(setting => setting.type === "buttonConfiguration")
|
.filter(setting => nestedTypes.includes(setting.type))
|
||||||
.forEach(setting => {
|
.forEach(setting => {
|
||||||
const buttonConfig = component[setting.key]
|
const instances = comp[setting.key]
|
||||||
|
if (Array.isArray(instances) && instances.length) {
|
||||||
|
instances.forEach(instance => {
|
||||||
|
let type = instance?._component
|
||||||
|
|
||||||
if (Array.isArray(buttonConfig)) {
|
// Backwards compatibility for multi-step from blocks which
|
||||||
buttonConfig.forEach(button => {
|
// didn't set a proper component type previously.
|
||||||
const nestedSettings = componentStore.getComponentSettings(
|
if (setting.type === "stepConfiguration" && !type) {
|
||||||
button._component
|
type = "@budibase/standard-components/multistepformblockstep"
|
||||||
)
|
}
|
||||||
parseEventSettings(nestedSettings, button)
|
|
||||||
|
// Parsed nested component instances inside this setting
|
||||||
|
const nestedSettings = componentStore.getComponentSettings(type)
|
||||||
|
parseEventSettings(nestedSettings, instance)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
parseEventSettings(settings, component)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the base component settings
|
parseEventSettings(settings, component)
|
||||||
parseComponentSettings(settings, component)
|
|
||||||
|
|
||||||
// Parse step configuration
|
|
||||||
const stepSetting = settings.find(
|
|
||||||
setting => setting.type === "stepConfiguration"
|
|
||||||
)
|
|
||||||
const steps = stepSetting ? component[stepSetting.key] : []
|
|
||||||
const stepDefinition = componentStore.getComponentSettings(
|
|
||||||
"@budibase/standard-components/multistepformblockstep"
|
|
||||||
)
|
|
||||||
|
|
||||||
steps?.forEach(step => {
|
|
||||||
parseComponentSettings(stepDefinition, step)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -324,10 +324,7 @@
|
||||||
<div
|
<div
|
||||||
id="side-panel-container"
|
id="side-panel-container"
|
||||||
class:open={$sidePanelStore.open}
|
class:open={$sidePanelStore.open}
|
||||||
use:clickOutside={{
|
use:clickOutside={autoCloseSidePanel ? sidePanelStore.actions.close : null}
|
||||||
callback: autoCloseSidePanel ? sidePanelStore.actions.close : null,
|
|
||||||
allowedType: "mousedown",
|
|
||||||
}}
|
|
||||||
class:builder={$builderStore.inBuilder}
|
class:builder={$builderStore.inBuilder}
|
||||||
>
|
>
|
||||||
<div class="side-panel-header">
|
<div class="side-panel-header">
|
||||||
|
|
|
@ -542,16 +542,22 @@ export const enrichButtonActions = (actions, context) => {
|
||||||
// then execute the rest of the actions in the chain
|
// then execute the rest of the actions in the chain
|
||||||
const result = await callback()
|
const result = await callback()
|
||||||
if (result !== false) {
|
if (result !== false) {
|
||||||
// Generate a new total context to pass into the next enrichment
|
// Generate a new total context for the next enrichment
|
||||||
buttonContext.push(result)
|
buttonContext.push(result)
|
||||||
const newContext = { ...context, actions: buttonContext }
|
const newContext = { ...context, actions: buttonContext }
|
||||||
|
|
||||||
// Enrich and call the next button action if there is more than one action remaining
|
// Enrich and call the next button action if there is more
|
||||||
|
// than one action remaining
|
||||||
const next = enrichButtonActions(
|
const next = enrichButtonActions(
|
||||||
actions.slice(i + 1),
|
actions.slice(i + 1),
|
||||||
newContext
|
newContext
|
||||||
)
|
)
|
||||||
resolve(typeof next === "function" ? await next() : true)
|
if (typeof next === "function") {
|
||||||
|
// Pass the event context back into the new action chain
|
||||||
|
resolve(await next(eventContext))
|
||||||
|
} else {
|
||||||
|
resolve(true)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
resolve(false)
|
resolve(false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,6 +124,7 @@
|
||||||
const fieldSchema = schemaFields.find(x => x.name === filter.field)
|
const fieldSchema = schemaFields.find(x => x.name === filter.field)
|
||||||
filter.type = fieldSchema?.type
|
filter.type = fieldSchema?.type
|
||||||
filter.subtype = fieldSchema?.subtype
|
filter.subtype = fieldSchema?.subtype
|
||||||
|
filter.formulaType = fieldSchema?.formulaType
|
||||||
|
|
||||||
// Update external type based on field
|
// Update external type based on field
|
||||||
filter.externalType = getSchema(filter)?.externalType
|
filter.externalType = getSchema(filter)?.externalType
|
||||||
|
|
|
@ -121,8 +121,14 @@
|
||||||
|
|
||||||
const onContextMenu = e => {
|
const onContextMenu = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ui.actions.blur()
|
|
||||||
open = !open
|
// The timeout allows time for clickoutside to close other open popvers
|
||||||
|
// before we show this one. Without the timeout, this popover closes again
|
||||||
|
// before it's even visible as clickoutside closes it.
|
||||||
|
setTimeout(() => {
|
||||||
|
ui.actions.blur()
|
||||||
|
open = !open
|
||||||
|
}, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortAscending = () => {
|
const sortAscending = () => {
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
focusedCellAPI,
|
focusedCellAPI,
|
||||||
focusedRowId,
|
focusedRowId,
|
||||||
notifications,
|
notifications,
|
||||||
isDatasourcePlus,
|
hasBudibaseIdentifiers,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
let anchor
|
let anchor
|
||||||
|
@ -82,7 +82,7 @@
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="Copy"
|
icon="Copy"
|
||||||
disabled={isNewRow || !$focusedRow?._id || !$isDatasourcePlus}
|
disabled={isNewRow || !$focusedRow?._id || !$hasBudibaseIdentifiers}
|
||||||
on:click={() => copyToClipboard($focusedRow?._id)}
|
on:click={() => copyToClipboard($focusedRow?._id)}
|
||||||
on:click={menu.actions.close}
|
on:click={menu.actions.close}
|
||||||
>
|
>
|
||||||
|
@ -90,7 +90,7 @@
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="Copy"
|
icon="Copy"
|
||||||
disabled={isNewRow || !$focusedRow?._rev}
|
disabled={isNewRow || !$focusedRow?._rev || !$hasBudibaseIdentifiers}
|
||||||
on:click={() => copyToClipboard($focusedRow?._rev)}
|
on:click={() => copyToClipboard($focusedRow?._rev)}
|
||||||
on:click={menu.actions.close}
|
on:click={menu.actions.close}
|
||||||
>
|
>
|
||||||
|
|
|
@ -75,14 +75,18 @@ export const deriveStores = context => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const isDatasourcePlus = derived(datasource, $datasource => {
|
const hasBudibaseIdentifiers = derived(datasource, $datasource => {
|
||||||
return ["table", "viewV2"].includes($datasource?.type)
|
let type = $datasource?.type
|
||||||
|
if (type === "provider") {
|
||||||
|
type = $datasource.value?.datasource?.type
|
||||||
|
}
|
||||||
|
return ["table", "viewV2", "link"].includes(type)
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
schema,
|
schema,
|
||||||
enrichedSchema,
|
enrichedSchema,
|
||||||
isDatasourcePlus,
|
hasBudibaseIdentifiers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const createActions = context => {
|
||||||
|
|
||||||
const open = (cellId, e) => {
|
const open = (cellId, e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
// Get DOM node for grid data wrapper to compute relative position to
|
// Get DOM node for grid data wrapper to compute relative position to
|
||||||
const gridNode = document.getElementById(gridID)
|
const gridNode = document.getElementById(gridID)
|
||||||
|
|
|
@ -83,7 +83,7 @@ export const createActions = context => {
|
||||||
error,
|
error,
|
||||||
notifications,
|
notifications,
|
||||||
fetch,
|
fetch,
|
||||||
isDatasourcePlus,
|
hasBudibaseIdentifiers,
|
||||||
refreshing,
|
refreshing,
|
||||||
} = context
|
} = context
|
||||||
const instanceLoaded = writable(false)
|
const instanceLoaded = writable(false)
|
||||||
|
@ -196,9 +196,16 @@ export const createActions = context => {
|
||||||
// Handles validation errors from the rows API and updates local validation
|
// Handles validation errors from the rows API and updates local validation
|
||||||
// state, storing error messages against relevant cells
|
// state, storing error messages against relevant cells
|
||||||
const handleValidationError = (rowId, error) => {
|
const handleValidationError = (rowId, error) => {
|
||||||
|
let errorString
|
||||||
|
if (typeof error === "string") {
|
||||||
|
errorString = error
|
||||||
|
} else if (typeof error?.message === "string") {
|
||||||
|
errorString = error.message
|
||||||
|
}
|
||||||
|
|
||||||
// If the server doesn't reply with a valid error, assume that the source
|
// If the server doesn't reply with a valid error, assume that the source
|
||||||
// of the error is the focused cell's column
|
// of the error is the focused cell's column
|
||||||
if (!error?.json?.validationErrors && error?.message) {
|
if (!error?.json?.validationErrors && errorString) {
|
||||||
const focusedColumn = get(focusedCellId)?.split("-")[1]
|
const focusedColumn = get(focusedCellId)?.split("-")[1]
|
||||||
if (focusedColumn) {
|
if (focusedColumn) {
|
||||||
error = {
|
error = {
|
||||||
|
@ -261,7 +268,7 @@ export const createActions = context => {
|
||||||
focusedCellId.set(`${rowId}-${erroredColumns[0]}`)
|
focusedCellId.set(`${rowId}-${erroredColumns[0]}`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
get(notifications).error(error?.message || "An unknown error occurred")
|
get(notifications).error(errorString || "An unknown error occurred")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,14 +465,14 @@ export const createActions = context => {
|
||||||
}
|
}
|
||||||
let rowsToAppend = []
|
let rowsToAppend = []
|
||||||
let newRow
|
let newRow
|
||||||
const $isDatasourcePlus = get(isDatasourcePlus)
|
const $hasBudibaseIdentifiers = get(hasBudibaseIdentifiers)
|
||||||
for (let i = 0; i < newRows.length; i++) {
|
for (let i = 0; i < newRows.length; i++) {
|
||||||
newRow = newRows[i]
|
newRow = newRows[i]
|
||||||
|
|
||||||
// Ensure we have a unique _id.
|
// Ensure we have a unique _id.
|
||||||
// This means generating one for non DS+, overwriting any that may already
|
// This means generating one for non DS+, overwriting any that may already
|
||||||
// exist as we cannot allow duplicates.
|
// exist as we cannot allow duplicates.
|
||||||
if (!$isDatasourcePlus) {
|
if (!$hasBudibaseIdentifiers) {
|
||||||
newRow._id = Helpers.uuid()
|
newRow._id = Helpers.uuid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -510,7 +517,7 @@ export const createActions = context => {
|
||||||
const cleanRow = row => {
|
const cleanRow = row => {
|
||||||
let clone = { ...row }
|
let clone = { ...row }
|
||||||
delete clone.__idx
|
delete clone.__idx
|
||||||
if (!get(isDatasourcePlus)) {
|
if (!get(hasBudibaseIdentifiers)) {
|
||||||
delete clone._id
|
delete clone._id
|
||||||
}
|
}
|
||||||
return clone
|
return clone
|
||||||
|
|
|
@ -52,6 +52,8 @@ import {
|
||||||
FetchAppPackageResponse,
|
FetchAppPackageResponse,
|
||||||
DuplicateAppRequest,
|
DuplicateAppRequest,
|
||||||
DuplicateAppResponse,
|
DuplicateAppResponse,
|
||||||
|
UpdateAppRequest,
|
||||||
|
UpdateAppResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
|
@ -450,7 +452,7 @@ export async function create(ctx: UserCtx<CreateAppRequest, App>) {
|
||||||
// This endpoint currently operates as a PATCH rather than a PUT
|
// This endpoint currently operates as a PATCH rather than a PUT
|
||||||
// Thus name and url fields are handled only if present
|
// Thus name and url fields are handled only if present
|
||||||
export async function update(
|
export async function update(
|
||||||
ctx: UserCtx<{ name?: string; url?: string }, App>
|
ctx: UserCtx<UpdateAppRequest, UpdateAppResponse>
|
||||||
) {
|
) {
|
||||||
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
|
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
|
||||||
// validation
|
// validation
|
||||||
|
|
|
@ -156,7 +156,7 @@ describe.each([
|
||||||
return expectSearch({ query })
|
return expectSearch({ query })
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("strings", () => {
|
describe.each([FieldType.STRING, FieldType.LONGFORM])("%s", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await createTable({
|
await createTable({
|
||||||
name: { name: "name", type: FieldType.STRING },
|
name: { name: "name", type: FieldType.STRING },
|
||||||
|
@ -508,7 +508,7 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("array of strings", () => {
|
describe.each([FieldType.ARRAY, FieldType.OPTIONS])("%s", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await createTable({
|
await createTable({
|
||||||
numbers: {
|
numbers: {
|
||||||
|
|
|
@ -5,6 +5,8 @@ import {
|
||||||
type FetchAppDefinitionResponse,
|
type FetchAppDefinitionResponse,
|
||||||
type FetchAppPackageResponse,
|
type FetchAppPackageResponse,
|
||||||
DuplicateAppResponse,
|
DuplicateAppResponse,
|
||||||
|
UpdateAppRequest,
|
||||||
|
UpdateAppResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { Expectations, TestAPI } from "./base"
|
import { Expectations, TestAPI } from "./base"
|
||||||
import { AppStatus } from "../../../db/utils"
|
import { AppStatus } from "../../../db/utils"
|
||||||
|
@ -109,11 +111,11 @@ export class ApplicationAPI extends TestAPI {
|
||||||
|
|
||||||
update = async (
|
update = async (
|
||||||
appId: string,
|
appId: string,
|
||||||
app: { name?: string; url?: string },
|
app: UpdateAppRequest,
|
||||||
expectations?: Expectations
|
expectations?: Expectations
|
||||||
): Promise<App> => {
|
): Promise<UpdateAppResponse> => {
|
||||||
return await this._put<App>(`/api/applications/${appId}`, {
|
return await this._put<App>(`/api/applications/${appId}`, {
|
||||||
fields: app,
|
body: app,
|
||||||
expectations,
|
expectations,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,3 +44,6 @@ export interface PublishResponse {
|
||||||
status: string
|
status: string
|
||||||
appUrl: string
|
appUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateAppRequest extends Partial<App> {}
|
||||||
|
export interface UpdateAppResponse extends App {}
|
||||||
|
|
Loading…
Reference in New Issue