Merge master.
This commit is contained in:
commit
384466c754
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "2.29.24",
|
"version": "2.29.25",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"@types/proper-lockfile": "^4.1.4",
|
"@types/proper-lockfile": "^4.1.4",
|
||||||
"@typescript-eslint/parser": "6.9.0",
|
"@typescript-eslint/parser": "6.9.0",
|
||||||
"esbuild": "^0.18.17",
|
"esbuild": "^0.18.17",
|
||||||
"esbuild-node-externals": "^1.8.0",
|
"esbuild-node-externals": "^1.14.0",
|
||||||
"eslint": "^8.52.0",
|
"eslint": "^8.52.0",
|
||||||
"eslint-plugin-import": "^2.29.0",
|
"eslint-plugin-import": "^2.29.0",
|
||||||
"eslint-plugin-jest": "^27.9.0",
|
"eslint-plugin-jest": "^27.9.0",
|
||||||
|
|
|
@ -56,24 +56,24 @@ class CouchDBError extends Error implements DBError {
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
info: {
|
info: {
|
||||||
status: number | undefined
|
status?: number
|
||||||
statusCode: number | undefined
|
statusCode?: number
|
||||||
name: string
|
name: string
|
||||||
errid: string
|
errid?: string
|
||||||
description: string
|
description?: string
|
||||||
reason: string
|
reason?: string
|
||||||
error: string
|
error?: string
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
super(message)
|
super(message)
|
||||||
const statusCode = info.status || info.statusCode || 500
|
const statusCode = info.status || info.statusCode || 500
|
||||||
this.status = statusCode
|
this.status = statusCode
|
||||||
this.statusCode = statusCode
|
this.statusCode = statusCode
|
||||||
this.reason = info.reason
|
this.reason = info.reason || "Unknown"
|
||||||
this.name = info.name
|
this.name = info.name
|
||||||
this.errid = info.errid
|
this.errid = info.errid || "Unknown"
|
||||||
this.description = info.description
|
this.description = info.description || "Unknown"
|
||||||
this.error = info.error
|
this.error = info.error || "Not found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,6 +246,35 @@ export class DatabaseImpl implements Database {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async bulkRemove(documents: Document[], opts?: { silenceErrors?: boolean }) {
|
||||||
|
const response: Nano.DocumentBulkResponse[] = await this.performCall(db => {
|
||||||
|
return () =>
|
||||||
|
db.bulk({
|
||||||
|
docs: documents.map(doc => ({
|
||||||
|
...doc,
|
||||||
|
_deleted: true,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (opts?.silenceErrors) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let errorFound = false
|
||||||
|
let errorMessage: string = "Unable to bulk remove documents: "
|
||||||
|
for (let res of response) {
|
||||||
|
if (res.error) {
|
||||||
|
errorFound = true
|
||||||
|
errorMessage += res.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errorFound) {
|
||||||
|
throw new CouchDBError(errorMessage, {
|
||||||
|
name: this.name,
|
||||||
|
status: 400,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async post(document: AnyDocument, opts?: DatabasePutOpts) {
|
async post(document: AnyDocument, opts?: DatabasePutOpts) {
|
||||||
if (!document._id) {
|
if (!document._id) {
|
||||||
document._id = newid()
|
document._id = newid()
|
||||||
|
|
|
@ -71,6 +71,16 @@ export class DDInstrumentedDatabase implements Database {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bulkRemove(
|
||||||
|
documents: Document[],
|
||||||
|
opts?: { silenceErrors?: boolean }
|
||||||
|
): Promise<void> {
|
||||||
|
return tracer.trace("db.bulkRemove", span => {
|
||||||
|
span?.addTags({ db_name: this.name, num_docs: documents.length })
|
||||||
|
return this.db.bulkRemove(documents, opts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
put(
|
put(
|
||||||
document: AnyDocument,
|
document: AnyDocument,
|
||||||
opts?: DatabasePutOpts | undefined
|
opts?: DatabasePutOpts | undefined
|
||||||
|
|
|
@ -199,9 +199,8 @@ export const createPlatformUserView = async () => {
|
||||||
|
|
||||||
export const queryPlatformView = async <T extends Document>(
|
export const queryPlatformView = async <T extends Document>(
|
||||||
viewName: ViewName,
|
viewName: ViewName,
|
||||||
params: DatabaseQueryOpts,
|
params: DatabaseQueryOpts
|
||||||
opts?: QueryViewOptions
|
): Promise<T[]> => {
|
||||||
): Promise<T[] | T> => {
|
|
||||||
const CreateFuncByName: any = {
|
const CreateFuncByName: any = {
|
||||||
[ViewName.ACCOUNT_BY_EMAIL]: createPlatformAccountEmailView,
|
[ViewName.ACCOUNT_BY_EMAIL]: createPlatformAccountEmailView,
|
||||||
[ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView,
|
[ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView,
|
||||||
|
@ -209,7 +208,9 @@ export const queryPlatformView = async <T extends Document>(
|
||||||
|
|
||||||
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => {
|
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => {
|
||||||
const createFn = CreateFuncByName[viewName]
|
const createFn = CreateFuncByName[viewName]
|
||||||
return queryView(viewName, params, db, createFn, opts)
|
return queryView(viewName, params, db, createFn, {
|
||||||
|
arrayResponse: true,
|
||||||
|
}) as Promise<T[]>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,11 @@ export async function getUserDoc(emailOrId: string): Promise<PlatformUser> {
|
||||||
return db.get(emailOrId)
|
return db.get(emailOrId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateUserDoc(platformUser: PlatformUserById) {
|
||||||
|
const db = getPlatformDB()
|
||||||
|
await db.put(platformUser)
|
||||||
|
}
|
||||||
|
|
||||||
// CREATE
|
// CREATE
|
||||||
|
|
||||||
function newUserIdDoc(id: string, tenantId: string): PlatformUserById {
|
function newUserIdDoc(id: string, tenantId: string): PlatformUserById {
|
||||||
|
@ -113,15 +118,12 @@ export async function addUser(
|
||||||
export async function removeUser(user: User) {
|
export async function removeUser(user: User) {
|
||||||
const db = getPlatformDB()
|
const db = getPlatformDB()
|
||||||
const keys = [user._id!, user.email]
|
const keys = [user._id!, user.email]
|
||||||
const userDocs = await db.allDocs({
|
const userDocs = await db.allDocs<User>({
|
||||||
keys,
|
keys,
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
const toDelete = userDocs.rows.map((row: any) => {
|
await db.bulkRemove(
|
||||||
return {
|
userDocs.rows.map(row => row.doc!),
|
||||||
...row.doc,
|
{ silenceErrors: true }
|
||||||
_deleted: true,
|
)
|
||||||
}
|
|
||||||
})
|
|
||||||
await db.bulkDocs(toDelete)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,9 @@ import {
|
||||||
User,
|
User,
|
||||||
UserStatus,
|
UserStatus,
|
||||||
UserGroup,
|
UserGroup,
|
||||||
|
PlatformUserBySsoId,
|
||||||
|
PlatformUserById,
|
||||||
|
AnyDocument,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
getAccountHolderFromUserIds,
|
getAccountHolderFromUserIds,
|
||||||
|
@ -25,7 +28,11 @@ import {
|
||||||
isCreator,
|
isCreator,
|
||||||
validateUniqueUser,
|
validateUniqueUser,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
import { searchExistingEmails } from "./lookup"
|
import {
|
||||||
|
getFirstPlatformUser,
|
||||||
|
getPlatformUsers,
|
||||||
|
searchExistingEmails,
|
||||||
|
} from "./lookup"
|
||||||
import { hash } from "../utils"
|
import { hash } from "../utils"
|
||||||
import { validatePassword } from "../security"
|
import { validatePassword } from "../security"
|
||||||
|
|
||||||
|
@ -446,9 +453,32 @@ export class UserDB {
|
||||||
creator => !!creator
|
creator => !!creator
|
||||||
).length
|
).length
|
||||||
|
|
||||||
|
const ssoUsersToDelete: AnyDocument[] = []
|
||||||
for (let user of usersToDelete) {
|
for (let user of usersToDelete) {
|
||||||
|
const platformUser = (await getFirstPlatformUser(
|
||||||
|
user._id!
|
||||||
|
)) as PlatformUserById
|
||||||
|
const ssoId = platformUser.ssoId
|
||||||
|
if (ssoId) {
|
||||||
|
// Need to get the _rev of the SSO user doc to delete it. The view also returns docs that have the ssoId property, so we need to ignore those.
|
||||||
|
const ssoUsers = (await getPlatformUsers(
|
||||||
|
ssoId
|
||||||
|
)) as PlatformUserBySsoId[]
|
||||||
|
ssoUsers
|
||||||
|
.filter(user => user.ssoId == null)
|
||||||
|
.forEach(user => {
|
||||||
|
ssoUsersToDelete.push({
|
||||||
|
...user,
|
||||||
|
_deleted: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
await bulkDeleteProcessing(user)
|
await bulkDeleteProcessing(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete any associated SSO user docs
|
||||||
|
await platform.getPlatformDB().bulkDocs(ssoUsersToDelete)
|
||||||
|
|
||||||
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount)
|
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount)
|
||||||
|
|
||||||
// Build Response
|
// Build Response
|
||||||
|
|
|
@ -34,15 +34,22 @@ export async function searchExistingEmails(emails: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup, could be email or userId, either will return a doc
|
// lookup, could be email or userId, either will return a doc
|
||||||
export async function getPlatformUser(
|
export async function getPlatformUsers(
|
||||||
identifier: string
|
identifier: string
|
||||||
): Promise<PlatformUser | null> {
|
): Promise<PlatformUser[]> {
|
||||||
// use the view here and allow to find anyone regardless of casing
|
// use the view here and allow to find anyone regardless of casing
|
||||||
// Use lowercase to ensure email login is case insensitive
|
// Use lowercase to ensure email login is case insensitive
|
||||||
return (await dbUtils.queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, {
|
return await dbUtils.queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, {
|
||||||
keys: [identifier.toLowerCase()],
|
keys: [identifier.toLowerCase()],
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})) as PlatformUser
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFirstPlatformUser(
|
||||||
|
identifier: string
|
||||||
|
): Promise<PlatformUser | null> {
|
||||||
|
const platformUserDocs = await getPlatformUsers(identifier)
|
||||||
|
return platformUserDocs[0] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getExistingTenantUsers(
|
export async function getExistingTenantUsers(
|
||||||
|
@ -74,15 +81,10 @@ export async function getExistingPlatformUsers(
|
||||||
keys: lcEmails,
|
keys: lcEmails,
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
}
|
}
|
||||||
|
return await dbUtils.queryPlatformView(
|
||||||
const opts = {
|
|
||||||
arrayResponse: true,
|
|
||||||
}
|
|
||||||
return (await dbUtils.queryPlatformView(
|
|
||||||
ViewName.PLATFORM_USERS_LOWERCASE,
|
ViewName.PLATFORM_USERS_LOWERCASE,
|
||||||
params,
|
params
|
||||||
opts
|
)
|
||||||
)) as PlatformUserByEmail[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getExistingAccounts(
|
export async function getExistingAccounts(
|
||||||
|
@ -93,14 +95,5 @@ export async function getExistingAccounts(
|
||||||
keys: lcEmails,
|
keys: lcEmails,
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
}
|
}
|
||||||
|
return await dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, params)
|
||||||
const opts = {
|
|
||||||
arrayResponse: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await dbUtils.queryPlatformView(
|
|
||||||
ViewName.ACCOUNT_BY_EMAIL,
|
|
||||||
params,
|
|
||||||
opts
|
|
||||||
)) as AccountMetadata[]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types"
|
import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types"
|
||||||
import * as accountSdk from "../accounts"
|
import * as accountSdk from "../accounts"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { getPlatformUser } from "./lookup"
|
import { getFirstPlatformUser } from "./lookup"
|
||||||
import { EmailUnavailableError } from "../errors"
|
import { EmailUnavailableError } from "../errors"
|
||||||
import { getTenantId } from "../context"
|
import { getTenantId } from "../context"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
@ -51,7 +51,7 @@ async function isCreatorByGroupMembership(user?: User | ContextUser) {
|
||||||
export async function validateUniqueUser(email: string, tenantId: string) {
|
export async function validateUniqueUser(email: string, tenantId: string) {
|
||||||
// check budibase users in other tenants
|
// check budibase users in other tenants
|
||||||
if (env.MULTI_TENANCY) {
|
if (env.MULTI_TENANCY) {
|
||||||
const tenantUser = await getPlatformUser(email)
|
const tenantUser = await getFirstPlatformUser(email)
|
||||||
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
|
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
|
||||||
throw new EmailUnavailableError(email)
|
throw new EmailUnavailableError(email)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {
|
import {
|
||||||
CONSTANT_EXTERNAL_ROW_COLS,
|
PROTECTED_EXTERNAL_COLUMNS,
|
||||||
CONSTANT_INTERNAL_ROW_COLS,
|
PROTECTED_INTERNAL_COLUMNS,
|
||||||
} from "@budibase/shared-core"
|
} from "@budibase/shared-core"
|
||||||
|
|
||||||
export function expectFunctionWasCalledTimesWith(
|
export function expectFunctionWasCalledTimesWith(
|
||||||
|
@ -14,7 +14,7 @@ export function expectFunctionWasCalledTimesWith(
|
||||||
}
|
}
|
||||||
|
|
||||||
export const expectAnyInternalColsAttributes: {
|
export const expectAnyInternalColsAttributes: {
|
||||||
[K in (typeof CONSTANT_INTERNAL_ROW_COLS)[number]]: any
|
[K in (typeof PROTECTED_INTERNAL_COLUMNS)[number]]: any
|
||||||
} = {
|
} = {
|
||||||
tableId: expect.anything(),
|
tableId: expect.anything(),
|
||||||
type: expect.anything(),
|
type: expect.anything(),
|
||||||
|
@ -25,7 +25,7 @@ export const expectAnyInternalColsAttributes: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const expectAnyExternalColsAttributes: {
|
export const expectAnyExternalColsAttributes: {
|
||||||
[K in (typeof CONSTANT_EXTERNAL_ROW_COLS)[number]]: any
|
[K in (typeof PROTECTED_EXTERNAL_COLUMNS)[number]]: any
|
||||||
} = {
|
} = {
|
||||||
tableId: expect.anything(),
|
tableId: expect.anything(),
|
||||||
_id: expect.anything(),
|
_id: expect.anything(),
|
||||||
|
|
|
@ -36,9 +36,11 @@
|
||||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="spectrum-InLineAlert-header">{header}</div>
|
<div class="spectrum-InLineAlert-header">{header}</div>
|
||||||
{#each split as splitMsg}
|
<slot>
|
||||||
<div class="spectrum-InLineAlert-content">{splitMsg}</div>
|
{#each split as splitMsg}
|
||||||
{/each}
|
<div class="spectrum-InLineAlert-content">{splitMsg}</div>
|
||||||
|
{/each}
|
||||||
|
</slot>
|
||||||
{#if onConfirm}
|
{#if onConfirm}
|
||||||
<div class="spectrum-InLineAlert-footer button">
|
<div class="spectrum-InLineAlert-footer button">
|
||||||
<Button {cta} secondary={cta ? false : true} on:click={onConfirm}
|
<Button {cta} secondary={cta ? false : true} on:click={onConfirm}
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
class:custom={!!color}
|
class:custom={!!color}
|
||||||
class:square
|
class:square
|
||||||
class:hoverable
|
class:hoverable
|
||||||
style={`--color: ${color};`}
|
style={`--color: ${color ?? "var(--spectrum-global-color-gray-400)"};`}
|
||||||
class:spectrum-StatusLight--celery={celery}
|
class:spectrum-StatusLight--celery={celery}
|
||||||
class:spectrum-StatusLight--yellow={yellow}
|
class:spectrum-StatusLight--yellow={yellow}
|
||||||
class:spectrum-StatusLight--fuchsia={fuchsia}
|
class:spectrum-StatusLight--fuchsia={fuchsia}
|
||||||
|
@ -61,13 +61,17 @@
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
transition: color ease-out 130ms;
|
|
||||||
}
|
}
|
||||||
.spectrum-StatusLight.withText::before {
|
.spectrum-StatusLight.withText::before {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spectrum-StatusLight::before {
|
||||||
|
transition: background-color ease-out 160ms;
|
||||||
|
}
|
||||||
|
|
||||||
.custom::before {
|
.custom::before {
|
||||||
background: var(--color) !important;
|
background-color: var(--color) !important;
|
||||||
}
|
}
|
||||||
.square::before {
|
.square::before {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
|
@ -79,4 +83,14 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--spectrum-global-color-gray-900);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spectrum-StatusLight--sizeXS::before {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-StatusLight--disabled::before {
|
||||||
|
background-color: var(--spectrum-global-color-gray-400) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
automationStore,
|
automationStore,
|
||||||
selectedAutomation,
|
selectedAutomation,
|
||||||
permissions,
|
permissions,
|
||||||
|
selectedAutomationDisplayData,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
Label,
|
Label,
|
||||||
AbsTooltip,
|
AbsTooltip,
|
||||||
|
InlineAlert,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
|
@ -49,6 +51,8 @@
|
||||||
$: isAppAction && setPermissions(role)
|
$: isAppAction && setPermissions(role)
|
||||||
$: isAppAction && getPermissions(automationId)
|
$: isAppAction && getPermissions(automationId)
|
||||||
|
|
||||||
|
$: triggerInfo = $selectedAutomationDisplayData?.triggerInfo
|
||||||
|
|
||||||
async function setPermissions(role) {
|
async function setPermissions(role) {
|
||||||
if (!role || !automationId) {
|
if (!role || !automationId) {
|
||||||
return
|
return
|
||||||
|
@ -183,6 +187,12 @@
|
||||||
{block}
|
{block}
|
||||||
{webhookModal}
|
{webhookModal}
|
||||||
/>
|
/>
|
||||||
|
{#if isTrigger && triggerInfo}
|
||||||
|
<InlineAlert
|
||||||
|
header={triggerInfo.type}
|
||||||
|
message={`This trigger is tied to the row action ${triggerInfo.rowAction.name} on your ${triggerInfo.table.name} table`}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{#if lastStep}
|
{#if lastStep}
|
||||||
<Button on:click={() => testDataModal.show()} cta>
|
<Button on:click={() => testDataModal.show()} cta>
|
||||||
Finish and test automation
|
Finish and test automation
|
||||||
|
|
|
@ -81,7 +81,7 @@
|
||||||
// Check the schema to see if required fields have been entered
|
// Check the schema to see if required fields have been entered
|
||||||
$: isError =
|
$: isError =
|
||||||
!isTriggerValid(trigger) ||
|
!isTriggerValid(trigger) ||
|
||||||
!trigger.schema.outputs.required?.every(
|
!(trigger.schema.outputs.required || []).every(
|
||||||
required => $memoTestData?.[required] || required !== "row"
|
required => $memoTestData?.[required] || required !== "row"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
contextMenuStore,
|
contextMenuStore,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import { notifications, Icon } from "@budibase/bbui"
|
import { notifications, Icon } from "@budibase/bbui"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import UpdateAutomationModal from "components/automation/AutomationPanel/UpdateAutomationModal.svelte"
|
import UpdateAutomationModal from "components/automation/AutomationPanel/UpdateAutomationModal.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
@ -35,45 +36,53 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const getContextMenuItems = () => {
|
const getContextMenuItems = () => {
|
||||||
return [
|
const isRowAction = sdk.automations.isRowAction(automation)
|
||||||
{
|
const result = []
|
||||||
icon: "Delete",
|
if (!isRowAction) {
|
||||||
name: "Delete",
|
result.push(
|
||||||
keyBind: null,
|
...[
|
||||||
visible: true,
|
{
|
||||||
disabled: false,
|
icon: "Delete",
|
||||||
callback: confirmDeleteDialog.show,
|
name: "Delete",
|
||||||
|
keyBind: null,
|
||||||
|
visible: true,
|
||||||
|
disabled: false,
|
||||||
|
callback: confirmDeleteDialog.show,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "Edit",
|
||||||
|
name: "Edit",
|
||||||
|
keyBind: null,
|
||||||
|
visible: true,
|
||||||
|
disabled: false,
|
||||||
|
callback: updateAutomationDialog.show,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "Duplicate",
|
||||||
|
name: "Duplicate",
|
||||||
|
keyBind: null,
|
||||||
|
visible: true,
|
||||||
|
disabled: automation.definition.trigger.name === "Webhook",
|
||||||
|
callback: duplicateAutomation,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
icon: automation.disabled ? "CheckmarkCircle" : "Cancel",
|
||||||
|
name: automation.disabled ? "Activate" : "Pause",
|
||||||
|
keyBind: null,
|
||||||
|
visible: true,
|
||||||
|
disabled: false,
|
||||||
|
callback: () => {
|
||||||
|
automationStore.actions.toggleDisabled(
|
||||||
|
automation._id,
|
||||||
|
automation.disabled
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
})
|
||||||
icon: "Edit",
|
return result
|
||||||
name: "Edit",
|
|
||||||
keyBind: null,
|
|
||||||
visible: true,
|
|
||||||
disabled: false,
|
|
||||||
callback: updateAutomationDialog.show,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: "Duplicate",
|
|
||||||
name: "Duplicate",
|
|
||||||
keyBind: null,
|
|
||||||
visible: true,
|
|
||||||
disabled: automation.definition.trigger.name === "Webhook",
|
|
||||||
callback: duplicateAutomation,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: automation.disabled ? "CheckmarkCircle" : "Cancel",
|
|
||||||
name: automation.disabled ? "Activate" : "Pause",
|
|
||||||
keyBind: null,
|
|
||||||
visible: true,
|
|
||||||
disabled: false,
|
|
||||||
callback: () => {
|
|
||||||
automationStore.actions.toggleDisabled(
|
|
||||||
automation._id,
|
|
||||||
automation.disabled
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const openContextMenu = e => {
|
const openContextMenu = e => {
|
||||||
|
@ -89,7 +98,7 @@
|
||||||
on:contextmenu={openContextMenu}
|
on:contextmenu={openContextMenu}
|
||||||
{icon}
|
{icon}
|
||||||
iconColor={"var(--spectrum-global-color-gray-900)"}
|
iconColor={"var(--spectrum-global-color-gray-900)"}
|
||||||
text={automation.name}
|
text={automation.displayName}
|
||||||
selected={automation._id === $selectedAutomation?._id}
|
selected={automation._id === $selectedAutomation?._id}
|
||||||
hovering={automation._id === $contextMenuStore.id}
|
hovering={automation._id === $contextMenuStore.id}
|
||||||
on:click={() => automationStore.actions.select(automation._id)}
|
on:click={() => automationStore.actions.select(automation._id)}
|
||||||
|
|
|
@ -17,9 +17,15 @@
|
||||||
automation.name.toLowerCase().includes(searchString.toLowerCase())
|
automation.name.toLowerCase().includes(searchString.toLowerCase())
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.map(automation => ({
|
||||||
|
...automation,
|
||||||
|
displayName:
|
||||||
|
$automationStore.automationDisplayData[automation._id].displayName ||
|
||||||
|
automation.name,
|
||||||
|
}))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const lowerA = a.name.toLowerCase()
|
const lowerA = a.displayName.toLowerCase()
|
||||||
const lowerB = b.name.toLowerCase()
|
const lowerB = b.displayName.toLowerCase()
|
||||||
return lowerA > lowerB ? 1 : -1
|
return lowerA > lowerB ? 1 : -1
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -876,6 +876,7 @@
|
||||||
options={value.enum}
|
options={value.enum}
|
||||||
getOptionLabel={(x, idx) =>
|
getOptionLabel={(x, idx) =>
|
||||||
value.pretty ? value.pretty[idx] : x}
|
value.pretty ? value.pretty[idx] : x}
|
||||||
|
disabled={value.readonly}
|
||||||
/>
|
/>
|
||||||
{:else if value.type === "json"}
|
{:else if value.type === "json"}
|
||||||
<Editor
|
<Editor
|
||||||
|
@ -884,6 +885,7 @@
|
||||||
mode="json"
|
mode="json"
|
||||||
value={inputData[key]?.value}
|
value={inputData[key]?.value}
|
||||||
on:change={e => onChange({ [key]: e.detail })}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
|
readOnly={value.readonly}
|
||||||
/>
|
/>
|
||||||
{:else if value.type === "boolean"}
|
{:else if value.type === "boolean"}
|
||||||
<div style="margin-top: 10px">
|
<div style="margin-top: 10px">
|
||||||
|
@ -891,6 +893,7 @@
|
||||||
text={value.title}
|
text={value.title}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange({ [key]: e.detail })}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
|
disabled={value.readonly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if value.type === "date"}
|
{:else if value.type === "date"}
|
||||||
|
@ -904,6 +907,7 @@
|
||||||
allowJS={true}
|
allowJS={true}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
drawerLeft="260px"
|
drawerLeft="260px"
|
||||||
|
disabled={value.readonly}
|
||||||
>
|
>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
|
@ -915,6 +919,7 @@
|
||||||
on:change={e => onChange({ [key]: e.detail })}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
options={Object.keys(table?.schema || {})}
|
options={Object.keys(table?.schema || {})}
|
||||||
|
disabled={value.readonly}
|
||||||
/>
|
/>
|
||||||
{:else if value.type === "attachment" || value.type === "signature_single"}
|
{:else if value.type === "attachment" || value.type === "signature_single"}
|
||||||
<div class="attachment-field-wrapper">
|
<div class="attachment-field-wrapper">
|
||||||
|
@ -1028,6 +1033,7 @@
|
||||||
{isTrigger}
|
{isTrigger}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange({ [key]: e.detail })}
|
on:change={e => onChange({ [key]: e.detail })}
|
||||||
|
disabled={value.readonly}
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "webhookUrl"}
|
{:else if value.customType === "webhookUrl"}
|
||||||
<WebhookDisplay value={inputData[key]} />
|
<WebhookDisplay value={inputData[key]} />
|
||||||
|
|
|
@ -17,8 +17,8 @@
|
||||||
SWITCHABLE_TYPES,
|
SWITCHABLE_TYPES,
|
||||||
ValidColumnNameRegex,
|
ValidColumnNameRegex,
|
||||||
helpers,
|
helpers,
|
||||||
CONSTANT_INTERNAL_ROW_COLS,
|
PROTECTED_INTERNAL_COLUMNS,
|
||||||
CONSTANT_EXTERNAL_ROW_COLS,
|
PROTECTED_EXTERNAL_COLUMNS,
|
||||||
} from "@budibase/shared-core"
|
} from "@budibase/shared-core"
|
||||||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
@ -489,8 +489,8 @@
|
||||||
}
|
}
|
||||||
const newError = {}
|
const newError = {}
|
||||||
const prohibited = externalTable
|
const prohibited = externalTable
|
||||||
? CONSTANT_EXTERNAL_ROW_COLS
|
? PROTECTED_EXTERNAL_COLUMNS
|
||||||
: CONSTANT_INTERNAL_ROW_COLS
|
: PROTECTED_INTERNAL_COLUMNS
|
||||||
if (!externalTable && fieldInfo.name?.startsWith("_")) {
|
if (!externalTable && fieldInfo.name?.startsWith("_")) {
|
||||||
newError.name = `Column name cannot start with an underscore.`
|
newError.name = `Column name cannot start with an underscore.`
|
||||||
} else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) {
|
} else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) {
|
||||||
|
|
|
@ -33,6 +33,5 @@
|
||||||
title="Confirm Deletion"
|
title="Confirm Deletion"
|
||||||
>
|
>
|
||||||
Are you sure you wish to delete the datasource
|
Are you sure you wish to delete the datasource
|
||||||
<i>{datasource.name}?</i>
|
<i>{datasource.name}</i>? This action cannot be undone.
|
||||||
This action cannot be undone.
|
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { tables, datasources, screenStore } from "stores/builder"
|
import { appStore, tables, datasources, screenStore } from "stores/builder"
|
||||||
import { Input, notifications } from "@budibase/bbui"
|
import { InlineAlert, Link, Input, notifications } from "@budibase/bbui"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { DB_TYPE_EXTERNAL } from "constants/backend"
|
import { DB_TYPE_EXTERNAL } from "constants/backend"
|
||||||
|
|
||||||
|
@ -9,28 +9,41 @@
|
||||||
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
|
|
||||||
export const show = () => {
|
let screensPossiblyAffected = []
|
||||||
templateScreens = $screenStore.screens.filter(
|
let viewsMessage = ""
|
||||||
screen => screen.autoTableId === table._id
|
let deleteTableName
|
||||||
)
|
|
||||||
willBeDeleted = ["All table data"].concat(
|
const getViewsMessage = () => {
|
||||||
templateScreens.map(screen => `Screen ${screen.routing?.route || ""}`)
|
const views = Object.values(table?.views ?? [])
|
||||||
)
|
if (views.length < 1) {
|
||||||
confirmDeleteDialog.show()
|
return ""
|
||||||
|
}
|
||||||
|
if (views.length === 1) {
|
||||||
|
return ", including 1 view"
|
||||||
|
}
|
||||||
|
|
||||||
|
return `, including ${views.length} views`
|
||||||
}
|
}
|
||||||
|
|
||||||
let templateScreens
|
export const show = () => {
|
||||||
let willBeDeleted
|
viewsMessage = getViewsMessage()
|
||||||
let deleteTableName
|
screensPossiblyAffected = $screenStore.screens
|
||||||
|
.filter(
|
||||||
|
screen => screen.autoTableId === table._id && screen.routing?.route
|
||||||
|
)
|
||||||
|
.map(screen => ({
|
||||||
|
text: screen.routing.route,
|
||||||
|
url: `/builder/app/${$appStore.appId}/design/${screen._id}`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
confirmDeleteDialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteTable() {
|
async function deleteTable() {
|
||||||
const isSelected = $params.tableId === table._id
|
const isSelected = $params.tableId === table._id
|
||||||
try {
|
try {
|
||||||
await tables.delete(table)
|
await tables.delete(table)
|
||||||
// Screens need deleted one at a time because of undo/redo
|
|
||||||
for (let screen of templateScreens) {
|
|
||||||
await screenStore.delete(screen)
|
|
||||||
}
|
|
||||||
if (table.sourceType === DB_TYPE_EXTERNAL) {
|
if (table.sourceType === DB_TYPE_EXTERNAL) {
|
||||||
await datasources.fetch()
|
await datasources.fetch()
|
||||||
}
|
}
|
||||||
|
@ -46,6 +59,10 @@
|
||||||
function hideDeleteDialog() {
|
function hideDeleteDialog() {
|
||||||
deleteTableName = ""
|
deleteTableName = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const autofillTableName = () => {
|
||||||
|
deleteTableName = table.name
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
@ -56,34 +73,103 @@
|
||||||
title="Confirm Deletion"
|
title="Confirm Deletion"
|
||||||
disabled={deleteTableName !== table.name}
|
disabled={deleteTableName !== table.name}
|
||||||
>
|
>
|
||||||
<p>
|
<div class="content">
|
||||||
Are you sure you wish to delete the table
|
<p class="firstWarning">
|
||||||
<b>{table.name}?</b>
|
Are you sure you wish to delete the table
|
||||||
The following will also be deleted:
|
<span class="tableNameLine">
|
||||||
</p>
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<b>
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="delete-items">
|
<b on:click={autofillTableName} class="tableName">{table.name}</b>
|
||||||
{#each willBeDeleted as item}
|
<span>?</span>
|
||||||
<div>{item}</div>
|
</span>
|
||||||
{/each}
|
</p>
|
||||||
</div>
|
|
||||||
</b>
|
<p class="secondWarning">All table data will be deleted{viewsMessage}.</p>
|
||||||
<p>
|
<p class="thirdWarning">This action <b>cannot be undone</b>.</p>
|
||||||
This action cannot be undone - to continue please enter the table name below
|
|
||||||
to confirm.
|
{#if screensPossiblyAffected.length > 0}
|
||||||
</p>
|
<div class="affectedScreens">
|
||||||
<Input bind:value={deleteTableName} placeholder={table.name} />
|
<InlineAlert
|
||||||
|
header="The following screens were originally generated from this table and may no longer function as expected"
|
||||||
|
>
|
||||||
|
<ul class="affectedScreensList">
|
||||||
|
{#each screensPossiblyAffected as item}
|
||||||
|
<li>
|
||||||
|
<Link quiet overBackground target="_blank" href={item.url}
|
||||||
|
>{item.text}</Link
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</InlineAlert>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<p class="fourthWarning">Please enter the app name below to confirm.</p>
|
||||||
|
<Input bind:value={deleteTableName} placeholder={table.name} />
|
||||||
|
</div>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div.delete-items {
|
.content {
|
||||||
margin-top: 10px;
|
margin-top: 0;
|
||||||
margin-bottom: 10px;
|
max-width: 320px;
|
||||||
margin-left: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.delete-items div {
|
.firstWarning {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableNameLine {
|
||||||
|
display: inline-flex;
|
||||||
|
max-width: 100%;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableName {
|
||||||
|
flex-grow: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondWarning {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thirdWarning {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.affectedScreens {
|
||||||
|
margin: 18px 0;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.affectedScreens :global(.spectrum-InLineAlert) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.affectedScreensList {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.affectedScreensList li {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
font-weight: 600;
|
}
|
||||||
|
|
||||||
|
.fourthWarning {
|
||||||
|
margin: 12px 0 6px;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<script>
|
||||||
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
import { StatusLight } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let id
|
||||||
|
export let size = "M"
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
|
$: color = RoleUtils.getRoleColour(id)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<StatusLight square {disabled} {size} {color} />
|
|
@ -1,20 +1,32 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout, Input } from "@budibase/bbui"
|
import { FancyForm, FancyInput } from "@budibase/bbui"
|
||||||
import { createValidationStore, requiredValidator } from "helpers/validation"
|
import { createValidationStore, requiredValidator } from "helpers/validation"
|
||||||
|
|
||||||
export let password
|
export let password
|
||||||
|
export let passwordForm
|
||||||
export let error
|
export let error
|
||||||
|
|
||||||
|
const validatePassword = value => {
|
||||||
|
if (!value || value.length < 12) {
|
||||||
|
return "Please enter at least 12 characters. We recommend using machine generated or random passwords."
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const [firstPassword, passwordError, firstTouched] = createValidationStore(
|
const [firstPassword, passwordError, firstTouched] = createValidationStore(
|
||||||
"",
|
"",
|
||||||
requiredValidator
|
requiredValidator
|
||||||
)
|
)
|
||||||
const [repeatPassword, _, repeatTouched] = createValidationStore(
|
const [repeatPassword, _, repeatTouched] = createValidationStore(
|
||||||
"",
|
"",
|
||||||
requiredValidator
|
requiredValidator,
|
||||||
|
validatePassword
|
||||||
)
|
)
|
||||||
|
|
||||||
$: password = $firstPassword
|
$: password = $firstPassword
|
||||||
|
$: firstPasswordError =
|
||||||
|
($firstTouched && $passwordError) ||
|
||||||
|
($repeatTouched && validatePassword(password))
|
||||||
$: error =
|
$: error =
|
||||||
!$firstPassword ||
|
!$firstPassword ||
|
||||||
!$firstTouched ||
|
!$firstTouched ||
|
||||||
|
@ -22,19 +34,19 @@
|
||||||
$firstPassword !== $repeatPassword
|
$firstPassword !== $repeatPassword
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout gap="XS" noPadding>
|
<FancyForm bind:this={passwordForm}>
|
||||||
<Input
|
<FancyInput
|
||||||
label="Password"
|
label="Password"
|
||||||
type="password"
|
type="password"
|
||||||
error={$firstTouched && $passwordError}
|
error={firstPasswordError}
|
||||||
bind:value={$firstPassword}
|
bind:value={$firstPassword}
|
||||||
/>
|
/>
|
||||||
<Input
|
<FancyInput
|
||||||
label="Repeat Password"
|
label="Repeat password"
|
||||||
type="password"
|
type="password"
|
||||||
error={$repeatTouched &&
|
error={$repeatTouched &&
|
||||||
$firstPassword !== $repeatPassword &&
|
$firstPassword !== $repeatPassword &&
|
||||||
"Passwords must match"}
|
"Passwords must match"}
|
||||||
bind:value={$repeatPassword}
|
bind:value={$repeatPassword}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</FancyForm>
|
||||||
|
|
|
@ -115,6 +115,7 @@
|
||||||
})
|
})
|
||||||
$: fields = bindings
|
$: fields = bindings
|
||||||
.filter(x => arrayTypes.includes(x.fieldSchema?.type))
|
.filter(x => arrayTypes.includes(x.fieldSchema?.type))
|
||||||
|
.filter(x => x.fieldSchema?.tableId != null)
|
||||||
.map(binding => {
|
.map(binding => {
|
||||||
const { providerId, readableBinding, runtimeBinding } = binding
|
const { providerId, readableBinding, runtimeBinding } = binding
|
||||||
const { name, type, tableId } = binding.fieldSchema
|
const { name, type, tableId } = binding.fieldSchema
|
||||||
|
|
|
@ -1,108 +1,88 @@
|
||||||
<script>
|
<script>
|
||||||
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
|
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
|
||||||
import DatasourceModal from "./DatasourceModal.svelte"
|
import DatasourceModal from "./DatasourceModal.svelte"
|
||||||
import ScreenRoleModal from "./ScreenRoleModal.svelte"
|
|
||||||
import sanitizeUrl from "helpers/sanitizeUrl"
|
import sanitizeUrl from "helpers/sanitizeUrl"
|
||||||
import FormTypeModal from "./FormTypeModal.svelte"
|
import FormTypeModal from "./FormTypeModal.svelte"
|
||||||
import { Modal, notifications } from "@budibase/bbui"
|
import { Modal, notifications } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
screenStore,
|
screenStore,
|
||||||
navigationStore,
|
navigationStore,
|
||||||
tables,
|
permissions as permissionsStore,
|
||||||
builderStore,
|
builderStore,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import getTemplates from "templates"
|
|
||||||
import { Roles } from "constants/backend"
|
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
|
import blankScreen from "templates/blankScreen"
|
||||||
import formScreen from "templates/formScreen"
|
import formScreen from "templates/formScreen"
|
||||||
import gridListScreen from "templates/gridListScreen"
|
import gridScreen from "templates/gridScreen"
|
||||||
import gridDetailsScreen from "templates/gridDetailsScreen"
|
import gridDetailsScreen from "templates/gridDetailsScreen"
|
||||||
|
import { Roles } from "constants/backend"
|
||||||
|
|
||||||
let mode
|
let mode
|
||||||
let pendingScreen
|
|
||||||
|
|
||||||
// Modal refs
|
|
||||||
let screenDetailsModal
|
let screenDetailsModal
|
||||||
let datasourceModal
|
let datasourceModal
|
||||||
let screenAccessRoleModal
|
|
||||||
let formTypeModal
|
let formTypeModal
|
||||||
|
|
||||||
// Cache variables for workflow
|
let selectedTablesAndViews = []
|
||||||
let screenAccessRole = Roles.BASIC
|
let permissions = {}
|
||||||
|
|
||||||
let templates = null
|
export const show = newMode => {
|
||||||
let screens = null
|
mode = newMode
|
||||||
|
selectedTablesAndViews = []
|
||||||
|
permissions = {}
|
||||||
|
|
||||||
let selectedDatasources = null
|
if (mode === "grid" || mode === "gridDetails" || mode === "form") {
|
||||||
let blankScreenUrl = null
|
datasourceModal.show()
|
||||||
let screenMode = null
|
} else if (mode === "blank") {
|
||||||
let formType = null
|
screenDetailsModal.show()
|
||||||
|
} else {
|
||||||
// Creates an array of screens, checking and sanitising their URLs
|
throw new Error("Invalid mode provided")
|
||||||
const createScreens = async ({ screens, screenAccessRole }) => {
|
|
||||||
if (!screens?.length) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createScreen = async screen => {
|
||||||
try {
|
try {
|
||||||
let createdScreens = []
|
// Check we aren't clashing with an existing URL
|
||||||
|
if (hasExistingUrl(screen.routing.route, screen.routing.roleId)) {
|
||||||
for (let screen of screens) {
|
let suffix = 2
|
||||||
// Check we aren't clashing with an existing URL
|
let candidateUrl = makeCandidateUrl(screen, suffix)
|
||||||
if (hasExistingUrl(screen.routing.route)) {
|
while (hasExistingUrl(candidateUrl, screen.routing.roleId)) {
|
||||||
let suffix = 2
|
candidateUrl = makeCandidateUrl(screen, ++suffix)
|
||||||
let candidateUrl = makeCandidateUrl(screen, suffix)
|
|
||||||
while (hasExistingUrl(candidateUrl)) {
|
|
||||||
candidateUrl = makeCandidateUrl(screen, ++suffix)
|
|
||||||
}
|
|
||||||
screen.routing.route = candidateUrl
|
|
||||||
}
|
}
|
||||||
|
screen.routing.route = candidateUrl
|
||||||
// Sanitise URL
|
|
||||||
screen.routing.route = sanitizeUrl(screen.routing.route)
|
|
||||||
|
|
||||||
// Use the currently selected role
|
|
||||||
if (!screenAccessRole) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
screen.routing.roleId = screenAccessRole
|
|
||||||
|
|
||||||
// Create the screen
|
|
||||||
const response = await screenStore.save(screen)
|
|
||||||
createdScreens.push(response)
|
|
||||||
|
|
||||||
// Add link in layout. We only ever actually create 1 screen now, even
|
|
||||||
// for autoscreens, so it's always safe to do this.
|
|
||||||
await navigationStore.saveLink(
|
|
||||||
screen.routing.route,
|
|
||||||
capitalise(screen.routing.route.split("/")[1]),
|
|
||||||
screenAccessRole
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdScreens
|
screen.routing.route = sanitizeUrl(screen.routing.route)
|
||||||
|
|
||||||
|
return await screenStore.save(screen)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
notifications.error("Error creating screens")
|
notifications.error("Error creating screens")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addNavigationLink = async screen =>
|
||||||
|
await navigationStore.saveLink(
|
||||||
|
screen.routing.route,
|
||||||
|
capitalise(screen.routing.route.split("/")[1]),
|
||||||
|
screen.routing.roleId
|
||||||
|
)
|
||||||
|
|
||||||
// Checks if any screens exist in the store with the given route and
|
// Checks if any screens exist in the store with the given route and
|
||||||
// currently selected role
|
// currently selected role
|
||||||
const hasExistingUrl = url => {
|
const hasExistingUrl = (url, screenAccessRole) => {
|
||||||
const roleId = screenAccessRole
|
|
||||||
const screens = get(screenStore).screens.filter(
|
const screens = get(screenStore).screens.filter(
|
||||||
s => s.routing.roleId === roleId
|
s => s.routing.roleId === screenAccessRole
|
||||||
)
|
)
|
||||||
return !!screens.find(s => s.routing?.route === url)
|
return !!screens.find(s => s.routing?.route === url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructs a candidate URL for a new screen, suffixing the base of the
|
// Constructs a candidate URL for a new screen, appending a given suffix to the
|
||||||
// screen's URL with a given suffix.
|
// screen's URL
|
||||||
// e.g. "/sales/:id" => "/sales-1/:id"
|
// e.g. "/sales/:id" => "/sales-1/:id"
|
||||||
const makeCandidateUrl = (screen, suffix) => {
|
const makeCandidateUrl = (screen, suffix) => {
|
||||||
let url = screen.routing?.route || ""
|
let url = screen.routing?.route || ""
|
||||||
|
@ -117,105 +97,79 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler for NewScreenModal
|
const onSelectDatasources = async () => {
|
||||||
export const show = newMode => {
|
if (mode === "form") {
|
||||||
mode = newMode
|
|
||||||
templates = null
|
|
||||||
screens = null
|
|
||||||
selectedDatasources = null
|
|
||||||
blankScreenUrl = null
|
|
||||||
screenMode = mode
|
|
||||||
pendingScreen = null
|
|
||||||
screenAccessRole = Roles.BASIC
|
|
||||||
formType = null
|
|
||||||
|
|
||||||
if (mode === "grid" || mode === "gridDetails" || mode === "form") {
|
|
||||||
datasourceModal.show()
|
|
||||||
} else if (mode === "blank") {
|
|
||||||
let templates = getTemplates($tables.list)
|
|
||||||
const blankScreenTemplate = templates.find(
|
|
||||||
t => t.id === "createFromScratch"
|
|
||||||
)
|
|
||||||
pendingScreen = blankScreenTemplate.create()
|
|
||||||
screenDetailsModal.show()
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid mode provided")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler for DatasourceModal confirmation, move to screen access select
|
|
||||||
const confirmScreenDatasources = async ({ datasources }) => {
|
|
||||||
selectedDatasources = datasources
|
|
||||||
if (screenMode === "form") {
|
|
||||||
formTypeModal.show()
|
formTypeModal.show()
|
||||||
} else {
|
} else if (mode === "grid") {
|
||||||
screenAccessRoleModal.show()
|
await createGridScreen()
|
||||||
|
} else if (mode === "gridDetails") {
|
||||||
|
await createGridDetailsScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler for Datasource Screen Creation
|
const createBlankScreen = async ({ screenUrl }) => {
|
||||||
const completeDatasourceScreenCreation = async () => {
|
const screenTemplate = blankScreen(screenUrl)
|
||||||
templates =
|
const screen = await createScreen(screenTemplate)
|
||||||
mode === "grid"
|
await addNavigationLink(screenTemplate)
|
||||||
? gridListScreen(selectedDatasources)
|
|
||||||
: gridDetailsScreen(selectedDatasources)
|
|
||||||
|
|
||||||
const screens = templates.map(template => {
|
loadNewScreen(screen)
|
||||||
let screenTemplate = template.create()
|
|
||||||
screenTemplate.autoTableId = template.resourceId
|
|
||||||
return screenTemplate
|
|
||||||
})
|
|
||||||
const createdScreens = await createScreens({ screens, screenAccessRole })
|
|
||||||
loadNewScreen(createdScreens)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmScreenBlank = async ({ screenUrl }) => {
|
const createGridScreen = async () => {
|
||||||
blankScreenUrl = screenUrl
|
let firstScreen = null
|
||||||
screenAccessRoleModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit request for a blank screen
|
for (let tableOrView of selectedTablesAndViews) {
|
||||||
const confirmBlankScreenCreation = async ({
|
const screenTemplate = gridScreen(
|
||||||
screenUrl,
|
tableOrView,
|
||||||
screenAccessRole,
|
permissions[tableOrView.id]
|
||||||
}) => {
|
)
|
||||||
if (!pendingScreen) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pendingScreen.routing.route = screenUrl
|
|
||||||
const createdScreens = await createScreens({
|
|
||||||
screens: [pendingScreen],
|
|
||||||
screenAccessRole,
|
|
||||||
})
|
|
||||||
loadNewScreen(createdScreens)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onConfirmFormType = () => {
|
const screen = await createScreen(screenTemplate)
|
||||||
screenAccessRoleModal.show()
|
await addNavigationLink(screen)
|
||||||
}
|
|
||||||
|
|
||||||
const loadNewScreen = createdScreens => {
|
firstScreen ??= screen
|
||||||
const lastScreen = createdScreens.slice(-1)[0]
|
|
||||||
|
|
||||||
// Go to new screen
|
|
||||||
if (lastScreen?.props?._children.length) {
|
|
||||||
// Focus on the main component for the streen type
|
|
||||||
const mainComponent = lastScreen?.props?._children?.[0]._id
|
|
||||||
$goto(`./${lastScreen._id}/${mainComponent}`)
|
|
||||||
} else {
|
|
||||||
$goto(`./${lastScreen._id}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
screenStore.select(lastScreen._id)
|
loadNewScreen(firstScreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmFormScreenCreation = async () => {
|
const createGridDetailsScreen = async () => {
|
||||||
templates = formScreen(selectedDatasources, { actionType: formType })
|
let firstScreen = null
|
||||||
screens = templates.map(template => {
|
|
||||||
let screenTemplate = template.create()
|
for (let tableOrView of selectedTablesAndViews) {
|
||||||
return screenTemplate
|
const screenTemplate = gridDetailsScreen(
|
||||||
})
|
tableOrView,
|
||||||
const createdScreens = await createScreens({ screens, screenAccessRole })
|
permissions[tableOrView.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const screen = await createScreen(screenTemplate)
|
||||||
|
await addNavigationLink(screen)
|
||||||
|
|
||||||
|
firstScreen ??= screen
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNewScreen(firstScreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createFormScreen = async formType => {
|
||||||
|
let firstScreen = null
|
||||||
|
|
||||||
|
for (let tableOrView of selectedTablesAndViews) {
|
||||||
|
const screenTemplate = formScreen(
|
||||||
|
tableOrView,
|
||||||
|
formType,
|
||||||
|
permissions[tableOrView.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const screen = await createScreen(screenTemplate)
|
||||||
|
// Only add a navigation link for `Create`, as both `Update` and `View`
|
||||||
|
// require an `id` in their URL in order to function.
|
||||||
|
if (formType === "Create") {
|
||||||
|
await addNavigationLink(screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
firstScreen ??= screen
|
||||||
|
}
|
||||||
|
|
||||||
if (formType === "Update" || formType === "Create") {
|
if (formType === "Update" || formType === "Create") {
|
||||||
const associatedTour =
|
const associatedTour =
|
||||||
|
@ -229,66 +183,89 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go to new screen
|
loadNewScreen(firstScreen)
|
||||||
loadNewScreen(createdScreens)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit screen config for creation.
|
const loadNewScreen = screen => {
|
||||||
const confirmScreenCreation = async () => {
|
if (screen?.props?._children.length) {
|
||||||
if (screenMode === "blank") {
|
// Focus on the main component for the screen type
|
||||||
confirmBlankScreenCreation({
|
const mainComponent = screen?.props?._children?.[0]._id
|
||||||
screenUrl: blankScreenUrl,
|
$goto(`./${screen._id}/${mainComponent}`)
|
||||||
screenAccessRole,
|
|
||||||
})
|
|
||||||
} else if (screenMode === "form") {
|
|
||||||
confirmFormScreenCreation()
|
|
||||||
} else {
|
} else {
|
||||||
completeDatasourceScreenCreation()
|
$goto(`./${screen._id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
screenStore.select(screen._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleSelectBack = () => {
|
const fetchPermission = resourceId => {
|
||||||
if (screenMode === "blank") {
|
permissions[resourceId] = { loading: true, read: null, write: null }
|
||||||
screenDetailsModal.show()
|
|
||||||
|
permissionsStore
|
||||||
|
.forResource(resourceId)
|
||||||
|
.then(permission => {
|
||||||
|
if (permissions[resourceId]?.loading) {
|
||||||
|
permissions[resourceId] = {
|
||||||
|
loading: false,
|
||||||
|
read: permission?.read?.role,
|
||||||
|
write: permission?.write?.role,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error("Error fetching permission data: ", e)
|
||||||
|
|
||||||
|
if (permissions[resourceId]?.loading) {
|
||||||
|
permissions[resourceId] = {
|
||||||
|
loading: false,
|
||||||
|
read: Roles.PUBLIC,
|
||||||
|
write: Roles.PUBLIC,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletePermission = resourceId => {
|
||||||
|
delete permissions[resourceId]
|
||||||
|
permissions = permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTableOrViewToggle = ({ detail: tableOrView }) => {
|
||||||
|
const alreadySelected = selectedTablesAndViews.some(
|
||||||
|
selected => selected.id === tableOrView.id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!alreadySelected) {
|
||||||
|
fetchPermission(tableOrView.id)
|
||||||
|
selectedTablesAndViews = [...selectedTablesAndViews, tableOrView]
|
||||||
} else {
|
} else {
|
||||||
datasourceModal.show()
|
deletePermission(tableOrView.id)
|
||||||
|
selectedTablesAndViews = selectedTablesAndViews.filter(
|
||||||
|
selected => selected.id !== tableOrView.id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={datasourceModal} autoFocus={false}>
|
<Modal bind:this={datasourceModal} autoFocus={false}>
|
||||||
<DatasourceModal {mode} onConfirm={confirmScreenDatasources} />
|
<DatasourceModal
|
||||||
</Modal>
|
{selectedTablesAndViews}
|
||||||
|
{permissions}
|
||||||
<Modal bind:this={screenAccessRoleModal}>
|
onConfirm={onSelectDatasources}
|
||||||
<ScreenRoleModal
|
on:toggle={handleTableOrViewToggle}
|
||||||
onConfirm={() => {
|
|
||||||
confirmScreenCreation()
|
|
||||||
}}
|
|
||||||
bind:screenAccessRole
|
|
||||||
onCancel={roleSelectBack}
|
|
||||||
screenUrl={blankScreenUrl}
|
|
||||||
confirmText={screenMode === "form" ? "Confirm" : "Done"}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={screenDetailsModal}>
|
<Modal bind:this={screenDetailsModal}>
|
||||||
<ScreenDetailsModal
|
<ScreenDetailsModal onConfirm={createBlankScreen} />
|
||||||
onConfirm={confirmScreenBlank}
|
|
||||||
initialUrl={blankScreenUrl}
|
|
||||||
/>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={formTypeModal}>
|
<Modal bind:this={formTypeModal}>
|
||||||
<FormTypeModal
|
<FormTypeModal
|
||||||
onConfirm={onConfirmFormType}
|
onConfirm={createFormScreen}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
formTypeModal.hide()
|
formTypeModal.hide()
|
||||||
datasourceModal.show()
|
datasourceModal.show()
|
||||||
}}
|
}}
|
||||||
on:select={e => {
|
|
||||||
formType = e.detail
|
|
||||||
}}
|
|
||||||
type={formType}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,42 +1,95 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Layout, notifications, Body } from "@budibase/bbui"
|
import { ModalContent, Layout, notifications, Body } from "@budibase/bbui"
|
||||||
import { datasources } from "stores/builder"
|
import { datasources as datasourcesStore } from "stores/builder"
|
||||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||||
import { IntegrationNames } from "constants"
|
import { IntegrationNames } from "constants"
|
||||||
import { onMount } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
import DatasourceTemplateRow from "./DatasourceTemplateRow.svelte"
|
import TableOrViewOption from "./TableOrViewOption.svelte"
|
||||||
|
|
||||||
export let onCancel
|
|
||||||
export let onConfirm
|
export let onConfirm
|
||||||
|
export let selectedTablesAndViews
|
||||||
|
export let permissions
|
||||||
|
|
||||||
let selectedSources = []
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: filteredSources = $datasources.list?.filter(datasource => {
|
const getViews = table => {
|
||||||
return datasource.source !== IntegrationNames.REST && datasource["entities"]
|
const views = Object.values(table.views || {}).filter(
|
||||||
})
|
view => view.version === 2
|
||||||
|
|
||||||
const toggleSelection = datasource => {
|
|
||||||
const exists = selectedSources.find(
|
|
||||||
d => d.resourceId === datasource.resourceId
|
|
||||||
)
|
)
|
||||||
if (exists) {
|
|
||||||
selectedSources = selectedSources.filter(
|
return views.map(view => ({
|
||||||
d => d.resourceId === datasource.resourceId
|
icon: "Remove",
|
||||||
)
|
name: view.name,
|
||||||
} else {
|
id: view.id,
|
||||||
selectedSources = [...selectedSources, datasource]
|
clientData: {
|
||||||
}
|
...view,
|
||||||
|
type: "viewV2",
|
||||||
|
label: view.name,
|
||||||
|
},
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmDatasourceSelection = async () => {
|
const getTablesAndViews = datasource => {
|
||||||
await onConfirm({
|
let tablesAndViews = []
|
||||||
datasources: selectedSources,
|
const rawTables = Array.isArray(datasource.entities)
|
||||||
})
|
? datasource.entities
|
||||||
|
: Object.values(datasource.entities ?? {})
|
||||||
|
|
||||||
|
for (const rawTable of rawTables) {
|
||||||
|
if (rawTable._id === "ta_users") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = {
|
||||||
|
icon: "Table",
|
||||||
|
name: rawTable.name,
|
||||||
|
id: rawTable._id,
|
||||||
|
clientData: {
|
||||||
|
...rawTable,
|
||||||
|
label: rawTable.name,
|
||||||
|
tableId: rawTable._id,
|
||||||
|
type: "table",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tablesAndViews = tablesAndViews.concat([table, ...getViews(rawTable)])
|
||||||
|
}
|
||||||
|
|
||||||
|
return tablesAndViews
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDatasources = rawDatasources => {
|
||||||
|
const datasources = []
|
||||||
|
|
||||||
|
for (const rawDatasource of rawDatasources) {
|
||||||
|
if (
|
||||||
|
rawDatasource.source === IntegrationNames.REST ||
|
||||||
|
!rawDatasource["entities"]
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const datasource = {
|
||||||
|
name: rawDatasource.name,
|
||||||
|
iconComponent: ICONS[rawDatasource.source],
|
||||||
|
tablesAndViews: getTablesAndViews(rawDatasource),
|
||||||
|
}
|
||||||
|
|
||||||
|
datasources.push(datasource)
|
||||||
|
}
|
||||||
|
|
||||||
|
return datasources
|
||||||
|
}
|
||||||
|
|
||||||
|
$: datasources = getDatasources($datasourcesStore.list)
|
||||||
|
|
||||||
|
const toggleSelection = tableOrView => {
|
||||||
|
dispatch("toggle", tableOrView)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await datasources.fetch()
|
await datasourcesStore.fetch()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error fetching datasources")
|
notifications.error("Error fetching datasources")
|
||||||
}
|
}
|
||||||
|
@ -48,66 +101,35 @@
|
||||||
title="Autogenerated screens"
|
title="Autogenerated screens"
|
||||||
confirmText="Confirm"
|
confirmText="Confirm"
|
||||||
cancelText="Back"
|
cancelText="Back"
|
||||||
onConfirm={confirmDatasourceSelection}
|
{onConfirm}
|
||||||
{onCancel}
|
disabled={!selectedTablesAndViews.length}
|
||||||
disabled={!selectedSources.length}
|
|
||||||
size="L"
|
size="L"
|
||||||
>
|
>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Select which datasources you would like to use to create your screens
|
Select which datasources you would like to use to create your screens
|
||||||
</Body>
|
</Body>
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
{#each filteredSources as datasource}
|
{#each datasources as datasource}
|
||||||
{@const entities = Array.isArray(datasource.entities)
|
|
||||||
? datasource.entities
|
|
||||||
: Object.values(datasource.entities || {})}
|
|
||||||
<div class="data-source-wrap">
|
<div class="data-source-wrap">
|
||||||
<div class="data-source-header">
|
<div class="data-source-header">
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={ICONS[datasource.source]}
|
this={datasource.iconComponent}
|
||||||
height="24"
|
height="24"
|
||||||
width="24"
|
width="24"
|
||||||
/>
|
/>
|
||||||
<div class="data-source-name">{datasource.name}</div>
|
<div class="data-source-name">{datasource.name}</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- List all tables -->
|
<!-- List all tables -->
|
||||||
{#each entities.filter(table => table._id !== "ta_users") as table}
|
{#each datasource.tablesAndViews as tableOrView}
|
||||||
{@const views = Object.values(table.views || {}).filter(
|
{@const selected = selectedTablesAndViews.some(
|
||||||
view => view.version === 2
|
selected => selected.id === tableOrView.id
|
||||||
)}
|
)}
|
||||||
{@const tableDS = {
|
<TableOrViewOption
|
||||||
tableId: table._id,
|
roles={permissions[tableOrView.id]}
|
||||||
label: table.name,
|
on:click={() => toggleSelection(tableOrView)}
|
||||||
resourceId: table._id,
|
|
||||||
type: "table",
|
|
||||||
}}
|
|
||||||
{@const selected = selectedSources.find(
|
|
||||||
datasource => datasource.resourceId === tableDS.resourceId
|
|
||||||
)}
|
|
||||||
<DatasourceTemplateRow
|
|
||||||
on:click={() => toggleSelection(tableDS)}
|
|
||||||
{selected}
|
{selected}
|
||||||
datasource={tableDS}
|
{tableOrView}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- List all views inside this table -->
|
|
||||||
{#each views as view}
|
|
||||||
{@const viewDS = {
|
|
||||||
label: view.name,
|
|
||||||
id: view.id,
|
|
||||||
resourceId: view.id,
|
|
||||||
tableId: view.tableId,
|
|
||||||
type: "viewV2",
|
|
||||||
}}
|
|
||||||
{@const selected = selectedSources.find(
|
|
||||||
x => x.resourceId === viewDS.resourceId
|
|
||||||
)}
|
|
||||||
<DatasourceTemplateRow
|
|
||||||
on:click={() => toggleSelection(viewDS)}
|
|
||||||
{selected}
|
|
||||||
datasource={viewDS}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -118,8 +140,11 @@
|
||||||
<style>
|
<style>
|
||||||
.data-source-wrap {
|
.data-source-wrap {
|
||||||
padding-bottom: var(--spectrum-alias-item-padding-s);
|
padding-bottom: var(--spectrum-alias-item-padding-s);
|
||||||
display: grid;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
grid-gap: var(--spacing-s);
|
grid-gap: var(--spacing-s);
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.data-source-header {
|
.data-source-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Icon } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let datasource
|
|
||||||
export let selected = false
|
|
||||||
|
|
||||||
$: icon = datasource.type === "viewV2" ? "Remove" : "Table"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<div class="data-source-entry" class:selected on:click>
|
|
||||||
<Icon name={icon} color="var(--spectrum-global-color-gray-600)" />
|
|
||||||
{datasource.label}
|
|
||||||
{#if selected}
|
|
||||||
<span class="data-source-check">
|
|
||||||
<Icon size="S" name="CheckmarkCircle" />
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.data-source-entry {
|
|
||||||
cursor: pointer;
|
|
||||||
grid-gap: var(--spacing-m);
|
|
||||||
padding: var(--spectrum-alias-item-padding-s);
|
|
||||||
background: var(--spectrum-alias-background-color-secondary);
|
|
||||||
transition: 0.3s all;
|
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.data-source-entry:hover,
|
|
||||||
.selected {
|
|
||||||
background: var(--spectrum-alias-background-color-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-source-check {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
.data-source-check :global(.spectrum-Icon) {
|
|
||||||
color: var(--spectrum-global-color-green-600);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,12 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Layout, Body, Icon } from "@budibase/bbui"
|
import { ModalContent, Layout, Body, Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
|
let type = null
|
||||||
|
|
||||||
export let onCancel = () => {}
|
export let onCancel = () => {}
|
||||||
export let onConfirm = () => {}
|
export let onConfirm = () => {}
|
||||||
export let type
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
|
@ -14,7 +12,7 @@
|
||||||
title="Select form type"
|
title="Select form type"
|
||||||
confirmText="Done"
|
confirmText="Done"
|
||||||
cancelText="Back"
|
cancelText="Back"
|
||||||
{onConfirm}
|
onConfirm={() => onConfirm(type)}
|
||||||
{onCancel}
|
{onCancel}
|
||||||
disabled={!type}
|
disabled={!type}
|
||||||
size="L"
|
size="L"
|
||||||
|
@ -25,9 +23,7 @@
|
||||||
<div
|
<div
|
||||||
class="form-type"
|
class="form-type"
|
||||||
class:selected={type === "Create"}
|
class:selected={type === "Create"}
|
||||||
on:click={() => {
|
on:click={() => (type = "Create")}
|
||||||
dispatch("select", "Create")
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div class="form-type-wrap">
|
<div class="form-type-wrap">
|
||||||
<div class="form-type-content">
|
<div class="form-type-content">
|
||||||
|
@ -46,9 +42,7 @@
|
||||||
<div
|
<div
|
||||||
class="form-type"
|
class="form-type"
|
||||||
class:selected={type === "Update"}
|
class:selected={type === "Update"}
|
||||||
on:click={() => {
|
on:click={() => (type = "Update")}
|
||||||
dispatch("select", "Update")
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div class="form-type-wrap">
|
<div class="form-type-wrap">
|
||||||
<div class="form-type-content">
|
<div class="form-type-content">
|
||||||
|
@ -65,9 +59,7 @@
|
||||||
<div
|
<div
|
||||||
class="form-type"
|
class="form-type"
|
||||||
class:selected={type === "View"}
|
class:selected={type === "View"}
|
||||||
on:click={() => {
|
on:click={() => (type = "View")}
|
||||||
dispatch("select", "View")
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div class="form-type-wrap">
|
<div class="form-type-wrap">
|
||||||
<div class="form-type-content">
|
<div class="form-type-content">
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Select, ModalContent } from "@budibase/bbui"
|
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
|
||||||
import { roles, screenStore } from "stores/builder"
|
|
||||||
import { get } from "svelte/store"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
export let onConfirm
|
|
||||||
export let onCancel
|
|
||||||
export let screenUrl
|
|
||||||
export let screenAccessRole
|
|
||||||
export let confirmText = "Done"
|
|
||||||
|
|
||||||
let error
|
|
||||||
|
|
||||||
const onChangeRole = e => {
|
|
||||||
const roleId = e.detail
|
|
||||||
if (routeExists(screenUrl, roleId)) {
|
|
||||||
error = "This URL is already taken for this access role"
|
|
||||||
} else {
|
|
||||||
error = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const routeExists = (url, role) => {
|
|
||||||
if (!url || !role) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return get(screenStore).screens.some(
|
|
||||||
screen =>
|
|
||||||
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
|
||||||
screen.routing.roleId === role
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// Validate the initial role
|
|
||||||
onChangeRole({ detail: screenAccessRole })
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent
|
|
||||||
title="Access"
|
|
||||||
{confirmText}
|
|
||||||
cancelText="Back"
|
|
||||||
{onConfirm}
|
|
||||||
{onCancel}
|
|
||||||
disabled={!!error}
|
|
||||||
>
|
|
||||||
Select the level of access required to see these screens
|
|
||||||
<Select
|
|
||||||
bind:value={screenAccessRole}
|
|
||||||
on:change={onChangeRole}
|
|
||||||
label="Access"
|
|
||||||
{error}
|
|
||||||
getOptionLabel={role => role.name}
|
|
||||||
getOptionValue={role => role._id}
|
|
||||||
getOptionColour={role => RoleUtils.getRoleColour(role._id)}
|
|
||||||
options={$roles}
|
|
||||||
placeholder={null}
|
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
<script>
|
||||||
|
import { Icon, AbsTooltip } from "@budibase/bbui"
|
||||||
|
import RoleIcon from "components/common/RoleIcon.svelte"
|
||||||
|
|
||||||
|
export let tableOrView
|
||||||
|
export let roles
|
||||||
|
export let selected = false
|
||||||
|
|
||||||
|
$: hideRoles = roles == undefined || roles?.loading
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div role="button" tabindex="0" class="datasource" class:selected on:click>
|
||||||
|
<div class="content">
|
||||||
|
<Icon name={tableOrView.icon} />
|
||||||
|
<span>{tableOrView.name}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class:hideRoles class="roles">
|
||||||
|
<AbsTooltip
|
||||||
|
type="info"
|
||||||
|
text={`Screens that only read data will be generated with access "${roles?.read?.toLowerCase()}"`}
|
||||||
|
>
|
||||||
|
<div class="role">
|
||||||
|
<span>read</span>
|
||||||
|
<RoleIcon
|
||||||
|
size="XS"
|
||||||
|
id={roles?.read}
|
||||||
|
disabled={roles?.loading !== false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AbsTooltip>
|
||||||
|
<AbsTooltip
|
||||||
|
type="info"
|
||||||
|
text={`Screens that write data will be generated with access "${roles?.write?.toLowerCase()}"`}
|
||||||
|
>
|
||||||
|
<div class="role">
|
||||||
|
<span>write</span>
|
||||||
|
<RoleIcon
|
||||||
|
size="XS"
|
||||||
|
id={roles?.write}
|
||||||
|
disabled={roles?.loading !== false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AbsTooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.datasource {
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
transition: 160ms all;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
background-color: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.datasource :global(svg) {
|
||||||
|
transition: 160ms all;
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: var(--spectrum-alias-item-padding-s);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
grid-gap: var(--spacing-m);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content span {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datasource:hover {
|
||||||
|
border: 1px solid var(--grey-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
border: 1px solid var(--blue) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roles {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: end;
|
||||||
|
padding-right: var(--spectrum-alias-item-padding-s);
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 160ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hideRoles {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role span {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -187,7 +187,9 @@
|
||||||
<Divider />
|
<Divider />
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="XS">History</Heading>
|
<Heading size="XS">History</Heading>
|
||||||
<Body size="S">Free plan stores up to 1 day of automation history</Body>
|
{#if licensePlan?.type === Constants.PlanType.FREE}
|
||||||
|
<Body size="S">Free plan stores up to 1 day of automation history</Body>
|
||||||
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="search">
|
<div class="search">
|
||||||
|
|
|
@ -4,47 +4,45 @@
|
||||||
Button,
|
Button,
|
||||||
Heading,
|
Heading,
|
||||||
Layout,
|
Layout,
|
||||||
|
ProgressCircle,
|
||||||
notifications,
|
notifications,
|
||||||
FancyForm,
|
|
||||||
FancyInput,
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { auth, organisation } from "stores/portal"
|
import { auth, organisation } from "stores/portal"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { handleError, passwordsMatch } from "./_components/utils"
|
import PasswordRepeatInput from "../../../components/common/users/PasswordRepeatInput.svelte"
|
||||||
|
|
||||||
const resetCode = $params["?code"]
|
const resetCode = $params["?code"]
|
||||||
let form
|
let form
|
||||||
let formData = {}
|
|
||||||
let errors = {}
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
let loading = false
|
||||||
|
let password
|
||||||
|
let passwordError
|
||||||
|
|
||||||
$: submitted = false
|
|
||||||
$: forceResetPassword = $auth?.user?.forceResetPassword
|
$: forceResetPassword = $auth?.user?.forceResetPassword
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
form.validate()
|
if (!form.validate() || passwordError) {
|
||||||
if (Object.keys(errors).length > 0) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
submitted = true
|
|
||||||
try {
|
try {
|
||||||
|
loading = true
|
||||||
if (forceResetPassword) {
|
if (forceResetPassword) {
|
||||||
await auth.updateSelf({
|
await auth.updateSelf({
|
||||||
password: formData.password,
|
password,
|
||||||
forceResetPassword: false,
|
forceResetPassword: false,
|
||||||
})
|
})
|
||||||
$goto("../portal/")
|
$goto("../portal/")
|
||||||
} else {
|
} else {
|
||||||
await auth.resetPassword(formData.password, resetCode)
|
await auth.resetPassword(password, resetCode)
|
||||||
notifications.success("Password reset successfully")
|
notifications.success("Password reset successfully")
|
||||||
// send them to login if reset successful
|
// send them to login if reset successful
|
||||||
$goto("./login")
|
$goto("./login")
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
submitted = false
|
loading = false
|
||||||
notifications.error(err.message || "Unable to reset password")
|
notifications.error(err.message || "Unable to reset password")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,86 +56,37 @@
|
||||||
}
|
}
|
||||||
loaded = true
|
loaded = true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleKeydown = evt => {
|
||||||
|
if (evt.key === "Enter") {
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
<TestimonialPage enabled={$organisation.testimonialsEnabled}>
|
<TestimonialPage enabled={$organisation.testimonialsEnabled}>
|
||||||
<Layout gap="S" noPadding>
|
<Layout gap="S" noPadding>
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
{/if}
|
{/if}
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<Heading size="M">Reset your password</Heading>
|
|
||||||
<Body size="M">Please enter the new password you'd like to use.</Body>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<Layout gap="S" noPadding>
|
<Layout gap="S" noPadding>
|
||||||
<FancyForm bind:this={form}>
|
<Heading size="M">Reset your password</Heading>
|
||||||
<FancyInput
|
<Body size="M">Must contain at least 12 characters</Body>
|
||||||
label="Password"
|
<PasswordRepeatInput
|
||||||
value={formData.password}
|
bind:passwordForm={form}
|
||||||
type="password"
|
bind:password
|
||||||
on:change={e => {
|
bind:error={passwordError}
|
||||||
formData = {
|
/>
|
||||||
...formData,
|
<Button secondary cta on:click={reset}>
|
||||||
password: e.detail,
|
{#if loading}
|
||||||
}
|
<ProgressCircle overBackground={true} size="S" />
|
||||||
}}
|
{:else}
|
||||||
validate={() => {
|
Reset
|
||||||
let fieldError = {}
|
{/if}
|
||||||
|
</Button>
|
||||||
fieldError["password"] = !formData.password
|
|
||||||
? "Please enter a password"
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
fieldError["confirmationPassword"] =
|
|
||||||
!passwordsMatch(
|
|
||||||
formData.password,
|
|
||||||
formData.confirmationPassword
|
|
||||||
) && formData.confirmationPassword
|
|
||||||
? "Passwords must match"
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
errors = handleError({ ...errors, ...fieldError })
|
|
||||||
}}
|
|
||||||
error={errors.password}
|
|
||||||
disabled={submitted}
|
|
||||||
/>
|
|
||||||
<FancyInput
|
|
||||||
label="Repeat Password"
|
|
||||||
value={formData.confirmationPassword}
|
|
||||||
type="password"
|
|
||||||
on:change={e => {
|
|
||||||
formData = {
|
|
||||||
...formData,
|
|
||||||
confirmationPassword: e.detail,
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
validate={() => {
|
|
||||||
const isValid =
|
|
||||||
!passwordsMatch(
|
|
||||||
formData.password,
|
|
||||||
formData.confirmationPassword
|
|
||||||
) && formData.password
|
|
||||||
|
|
||||||
let fieldError = {
|
|
||||||
confirmationPassword: isValid ? "Passwords must match" : null,
|
|
||||||
}
|
|
||||||
|
|
||||||
errors = handleError({ ...errors, ...fieldError })
|
|
||||||
}}
|
|
||||||
error={errors.confirmationPassword}
|
|
||||||
disabled={submitted}
|
|
||||||
/>
|
|
||||||
</FancyForm>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
<div>
|
<div />
|
||||||
<Button
|
|
||||||
disabled={Object.keys(errors).length > 0 ||
|
|
||||||
(forceResetPassword ? false : !resetCode)}
|
|
||||||
cta
|
|
||||||
on:click={reset}>Reset your password</Button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</TestimonialPage>
|
</TestimonialPage>
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ const initialAutomationState = {
|
||||||
ACTION: [],
|
ACTION: [],
|
||||||
},
|
},
|
||||||
selectedAutomationId: null,
|
selectedAutomationId: null,
|
||||||
|
automationDisplayData: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this functions, remove the actions elements
|
// If this functions, remove the actions elements
|
||||||
|
@ -58,18 +59,19 @@ const automationActions = store => ({
|
||||||
return response
|
return response
|
||||||
},
|
},
|
||||||
fetch: async () => {
|
fetch: async () => {
|
||||||
const responses = await Promise.all([
|
const [automationResponse, definitions] = await Promise.all([
|
||||||
API.getAutomations(),
|
API.getAutomations({ enrich: true }),
|
||||||
API.getAutomationDefinitions(),
|
API.getAutomationDefinitions(),
|
||||||
])
|
])
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.automations = responses[0]
|
state.automations = automationResponse.automations
|
||||||
state.automations.sort((a, b) => {
|
state.automations.sort((a, b) => {
|
||||||
return a.name < b.name ? -1 : 1
|
return a.name < b.name ? -1 : 1
|
||||||
})
|
})
|
||||||
|
state.automationDisplayData = automationResponse.builderData
|
||||||
state.blockDefinitions = {
|
state.blockDefinitions = {
|
||||||
TRIGGER: responses[1].trigger,
|
TRIGGER: definitions.trigger,
|
||||||
ACTION: responses[1].action,
|
ACTION: definitions.action,
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
@ -102,19 +104,8 @@ const automationActions = store => ({
|
||||||
},
|
},
|
||||||
save: async automation => {
|
save: async automation => {
|
||||||
const response = await API.updateAutomation(automation)
|
const response = await API.updateAutomation(automation)
|
||||||
store.update(state => {
|
|
||||||
const updatedAutomation = response.automation
|
await store.actions.fetch()
|
||||||
const existingIdx = state.automations.findIndex(
|
|
||||||
existing => existing._id === automation._id
|
|
||||||
)
|
|
||||||
if (existingIdx !== -1) {
|
|
||||||
state.automations.splice(existingIdx, 1, updatedAutomation)
|
|
||||||
return state
|
|
||||||
} else {
|
|
||||||
state.automations = [...state.automations, updatedAutomation]
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
return response.automation
|
return response.automation
|
||||||
},
|
},
|
||||||
delete: async automation => {
|
delete: async automation => {
|
||||||
|
@ -308,7 +299,9 @@ const automationActions = store => ({
|
||||||
if (!automation) {
|
if (!automation) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
delete newAutomation.definition.stepNames[blockId]
|
if (newAutomation.definition.stepNames) {
|
||||||
|
delete newAutomation.definition.stepNames[blockId]
|
||||||
|
}
|
||||||
|
|
||||||
await store.actions.save(newAutomation)
|
await store.actions.save(newAutomation)
|
||||||
},
|
},
|
||||||
|
@ -384,3 +377,13 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
|
||||||
x => x._id === $automationStore.selectedAutomationId
|
x => x._id === $automationStore.selectedAutomationId
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const selectedAutomationDisplayData = derived(
|
||||||
|
[automationStore, selectedAutomation],
|
||||||
|
([$automationStore, $selectedAutomation]) => {
|
||||||
|
if (!$selectedAutomation._id) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return $automationStore.automationDisplayData[$selectedAutomation._id]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
automationStore,
|
automationStore,
|
||||||
selectedAutomation,
|
selectedAutomation,
|
||||||
automationHistoryStore,
|
automationHistoryStore,
|
||||||
|
selectedAutomationDisplayData,
|
||||||
} from "./automations.js"
|
} from "./automations.js"
|
||||||
import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js"
|
import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js"
|
||||||
import { deploymentStore } from "./deployments.js"
|
import { deploymentStore } from "./deployments.js"
|
||||||
|
@ -44,6 +45,7 @@ export {
|
||||||
previewStore,
|
previewStore,
|
||||||
automationStore,
|
automationStore,
|
||||||
selectedAutomation,
|
selectedAutomation,
|
||||||
|
selectedAutomationDisplayData,
|
||||||
automationHistoryStore,
|
automationHistoryStore,
|
||||||
sortedScreens,
|
sortedScreens,
|
||||||
userStore,
|
userStore,
|
||||||
|
|
|
@ -63,6 +63,11 @@ export class Screen extends BaseStructure {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autoTableId(autoTableId) {
|
||||||
|
this._json.autoTableId = autoTableId
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
instanceName(name) {
|
instanceName(name) {
|
||||||
this._json.props._instanceName = name
|
this._json.props._instanceName = name
|
||||||
return this
|
return this
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Screen } from "./Screen"
|
||||||
|
|
||||||
|
const blankScreen = route => {
|
||||||
|
return new Screen().instanceName("New Screen").route(route).json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default blankScreen
|
|
@ -1,12 +0,0 @@
|
||||||
import { Screen } from "./Screen"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: `Create from scratch`,
|
|
||||||
id: `createFromScratch`,
|
|
||||||
create: () => createScreen(),
|
|
||||||
table: `Create from scratch`,
|
|
||||||
}
|
|
||||||
|
|
||||||
const createScreen = () => {
|
|
||||||
return new Screen().instanceName("New Screen").json()
|
|
||||||
}
|
|
|
@ -3,41 +3,47 @@ import { Component } from "./Component"
|
||||||
import sanitizeUrl from "helpers/sanitizeUrl"
|
import sanitizeUrl from "helpers/sanitizeUrl"
|
||||||
|
|
||||||
export const FORM_TEMPLATE = "FORM_TEMPLATE"
|
export const FORM_TEMPLATE = "FORM_TEMPLATE"
|
||||||
export const formUrl = datasource => sanitizeUrl(`/${datasource.label}-form`)
|
export const formUrl = (tableOrView, actionType) => {
|
||||||
|
if (actionType === "Create") {
|
||||||
// Mode not really necessary
|
return sanitizeUrl(`/${tableOrView.name}/new`)
|
||||||
export default function (datasources, config) {
|
} else if (actionType === "Update") {
|
||||||
if (!Array.isArray(datasources)) {
|
return sanitizeUrl(`/${tableOrView.name}/edit/:id`)
|
||||||
return []
|
} else if (actionType === "View") {
|
||||||
|
return sanitizeUrl(`/${tableOrView.name}/view/:id`)
|
||||||
}
|
}
|
||||||
return datasources.map(datasource => {
|
|
||||||
return {
|
|
||||||
name: `${datasource.label} - Form`,
|
|
||||||
create: () => createScreen(datasource, config),
|
|
||||||
id: FORM_TEMPLATE,
|
|
||||||
resourceId: datasource.resourceId,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateMultistepFormBlock = (dataSource, { actionType } = {}) => {
|
export const getRole = (permissions, actionType) => {
|
||||||
|
if (actionType === "View") {
|
||||||
|
return permissions.read
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions.write
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateMultistepFormBlock = (tableOrView, actionType) => {
|
||||||
const multistepFormBlock = new Component(
|
const multistepFormBlock = new Component(
|
||||||
"@budibase/standard-components/multistepformblock"
|
"@budibase/standard-components/multistepformblock"
|
||||||
)
|
)
|
||||||
multistepFormBlock
|
multistepFormBlock
|
||||||
.customProps({
|
.customProps({
|
||||||
actionType,
|
actionType,
|
||||||
dataSource,
|
dataSource: tableOrView.clientData,
|
||||||
steps: [{}],
|
steps: [{}],
|
||||||
|
rowId: actionType === "new" ? undefined : `{{ url.id }}`,
|
||||||
})
|
})
|
||||||
.instanceName(`${dataSource.label} - Multistep Form block`)
|
.instanceName(`${tableOrView.name} - Multistep Form block`)
|
||||||
return multistepFormBlock
|
return multistepFormBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
const createScreen = (datasource, config) => {
|
const createScreen = (tableOrView, actionType, permissions) => {
|
||||||
return new Screen()
|
return new Screen()
|
||||||
.route(formUrl(datasource))
|
.route(formUrl(tableOrView, actionType))
|
||||||
.instanceName(`${datasource.label} - Form`)
|
.instanceName(`${tableOrView.name} - Form`)
|
||||||
.addChild(generateMultistepFormBlock(datasource, config))
|
.role(getRole(permissions, actionType))
|
||||||
|
.autoTableId(tableOrView.id)
|
||||||
|
.addChild(generateMultistepFormBlock(tableOrView, actionType))
|
||||||
.json()
|
.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default createScreen
|
||||||
|
|
|
@ -5,24 +5,9 @@ import { generate } from "shortid"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export default function (datasources) {
|
const gridDetailsUrl = tableOrView => sanitizeUrl(`/${tableOrView.name}`)
|
||||||
if (!Array.isArray(datasources)) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return datasources.map(datasource => {
|
|
||||||
return {
|
|
||||||
name: `${datasource.label} - List with panel`,
|
|
||||||
create: () => createScreen(datasource),
|
|
||||||
id: GRID_DETAILS_TEMPLATE,
|
|
||||||
resourceId: datasource.resourceId,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GRID_DETAILS_TEMPLATE = "GRID_DETAILS_TEMPLATE"
|
const createScreen = (tableOrView, permissions) => {
|
||||||
export const gridDetailsUrl = datasource => sanitizeUrl(`/${datasource.label}`)
|
|
||||||
|
|
||||||
const createScreen = datasource => {
|
|
||||||
/*
|
/*
|
||||||
Create Row
|
Create Row
|
||||||
*/
|
*/
|
||||||
|
@ -47,7 +32,7 @@ const createScreen = datasource => {
|
||||||
type: "cta",
|
type: "cta",
|
||||||
})
|
})
|
||||||
|
|
||||||
buttonGroup.instanceName(`${datasource.label} - Create`).customProps({
|
buttonGroup.instanceName(`${tableOrView.name} - Create`).customProps({
|
||||||
hAlign: "right",
|
hAlign: "right",
|
||||||
buttons: [createButton.json()],
|
buttons: [createButton.json()],
|
||||||
})
|
})
|
||||||
|
@ -62,7 +47,7 @@ const createScreen = datasource => {
|
||||||
const heading = new Component("@budibase/standard-components/heading")
|
const heading = new Component("@budibase/standard-components/heading")
|
||||||
.instanceName("Table heading")
|
.instanceName("Table heading")
|
||||||
.customProps({
|
.customProps({
|
||||||
text: datasource?.label,
|
text: tableOrView.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
gridHeader.addChild(heading)
|
gridHeader.addChild(heading)
|
||||||
|
@ -72,7 +57,7 @@ const createScreen = datasource => {
|
||||||
"@budibase/standard-components/formblock"
|
"@budibase/standard-components/formblock"
|
||||||
)
|
)
|
||||||
createFormBlock.instanceName("Create row form block").customProps({
|
createFormBlock.instanceName("Create row form block").customProps({
|
||||||
dataSource: datasource,
|
dataSource: tableOrView.clientData,
|
||||||
labelPosition: "left",
|
labelPosition: "left",
|
||||||
buttonPosition: "top",
|
buttonPosition: "top",
|
||||||
actionType: "Create",
|
actionType: "Create",
|
||||||
|
@ -83,7 +68,7 @@ const createScreen = datasource => {
|
||||||
showSaveButton: true,
|
showSaveButton: true,
|
||||||
saveButtonLabel: "Save",
|
saveButtonLabel: "Save",
|
||||||
actionType: "Create",
|
actionType: "Create",
|
||||||
dataSource: datasource,
|
dataSource: tableOrView.clientData,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -99,7 +84,7 @@ const createScreen = datasource => {
|
||||||
|
|
||||||
const editFormBlock = new Component("@budibase/standard-components/formblock")
|
const editFormBlock = new Component("@budibase/standard-components/formblock")
|
||||||
editFormBlock.instanceName("Edit row form block").customProps({
|
editFormBlock.instanceName("Edit row form block").customProps({
|
||||||
dataSource: datasource,
|
dataSource: tableOrView.clientData,
|
||||||
labelPosition: "left",
|
labelPosition: "left",
|
||||||
buttonPosition: "top",
|
buttonPosition: "top",
|
||||||
actionType: "Update",
|
actionType: "Update",
|
||||||
|
@ -112,7 +97,7 @@ const createScreen = datasource => {
|
||||||
saveButtonLabel: "Save",
|
saveButtonLabel: "Save",
|
||||||
deleteButtonLabel: "Delete",
|
deleteButtonLabel: "Delete",
|
||||||
actionType: "Update",
|
actionType: "Update",
|
||||||
dataSource: datasource,
|
dataSource: tableOrView.clientData,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -121,7 +106,7 @@ const createScreen = datasource => {
|
||||||
const gridBlock = new Component("@budibase/standard-components/gridblock")
|
const gridBlock = new Component("@budibase/standard-components/gridblock")
|
||||||
gridBlock
|
gridBlock
|
||||||
.customProps({
|
.customProps({
|
||||||
table: datasource,
|
table: tableOrView.clientData,
|
||||||
allowAddRows: false,
|
allowAddRows: false,
|
||||||
allowEditRows: false,
|
allowEditRows: false,
|
||||||
allowDeleteRows: false,
|
allowDeleteRows: false,
|
||||||
|
@ -145,14 +130,18 @@ const createScreen = datasource => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.instanceName(`${datasource.label} - Table`)
|
.instanceName(`${tableOrView.name} - Table`)
|
||||||
|
|
||||||
return new Screen()
|
return new Screen()
|
||||||
.route(gridDetailsUrl(datasource))
|
.route(gridDetailsUrl(tableOrView))
|
||||||
.instanceName(`${datasource.label} - List and details`)
|
.instanceName(`${tableOrView.name} - List and details`)
|
||||||
|
.role(permissions.write)
|
||||||
|
.autoTableId(tableOrView.resourceId)
|
||||||
.addChild(gridHeader)
|
.addChild(gridHeader)
|
||||||
.addChild(gridBlock)
|
.addChild(gridBlock)
|
||||||
.addChild(createRowSidePanel)
|
.addChild(createRowSidePanel)
|
||||||
.addChild(detailsSidePanel)
|
.addChild(detailsSidePanel)
|
||||||
.json()
|
.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default createScreen
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
import sanitizeUrl from "helpers/sanitizeUrl"
|
|
||||||
import { Screen } from "./Screen"
|
|
||||||
import { Component } from "./Component"
|
|
||||||
|
|
||||||
export default function (datasources) {
|
|
||||||
if (!Array.isArray(datasources)) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return datasources.map(datasource => {
|
|
||||||
return {
|
|
||||||
name: `${datasource.label} - List`,
|
|
||||||
create: () => createScreen(datasource),
|
|
||||||
id: GRID_LIST_TEMPLATE,
|
|
||||||
resourceId: datasource.resourceId,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GRID_LIST_TEMPLATE = "GRID_LIST_TEMPLATE"
|
|
||||||
export const gridListUrl = datasource => sanitizeUrl(`/${datasource.label}`)
|
|
||||||
|
|
||||||
const createScreen = datasource => {
|
|
||||||
const heading = new Component("@budibase/standard-components/heading")
|
|
||||||
.instanceName("Table heading")
|
|
||||||
.customProps({
|
|
||||||
text: datasource?.label,
|
|
||||||
})
|
|
||||||
|
|
||||||
const gridBlock = new Component("@budibase/standard-components/gridblock")
|
|
||||||
.instanceName(`${datasource.label} - Table`)
|
|
||||||
.customProps({
|
|
||||||
table: datasource,
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Screen()
|
|
||||||
.route(gridListUrl(datasource))
|
|
||||||
.instanceName(`${datasource.label} - List`)
|
|
||||||
.addChild(heading)
|
|
||||||
.addChild(gridBlock)
|
|
||||||
.json()
|
|
||||||
}
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import sanitizeUrl from "helpers/sanitizeUrl"
|
||||||
|
import { Screen } from "./Screen"
|
||||||
|
import { Component } from "./Component"
|
||||||
|
|
||||||
|
const gridUrl = tableOrView => sanitizeUrl(`/${tableOrView.name}`)
|
||||||
|
|
||||||
|
const createScreen = (tableOrView, permissions) => {
|
||||||
|
const heading = new Component("@budibase/standard-components/heading")
|
||||||
|
.instanceName("Table heading")
|
||||||
|
.customProps({
|
||||||
|
text: tableOrView.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
const gridBlock = new Component("@budibase/standard-components/gridblock")
|
||||||
|
.instanceName(`${tableOrView.name} - Table`)
|
||||||
|
.customProps({
|
||||||
|
table: tableOrView.clientData,
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Screen()
|
||||||
|
.route(gridUrl(tableOrView))
|
||||||
|
.instanceName(`${tableOrView.name} - List`)
|
||||||
|
.role(permissions.write)
|
||||||
|
.autoTableId(tableOrView.id)
|
||||||
|
.addChild(heading)
|
||||||
|
.addChild(gridBlock)
|
||||||
|
.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createScreen
|
|
@ -1,35 +0,0 @@
|
||||||
import gridListScreen from "./gridListScreen"
|
|
||||||
import gridDetailsScreen from "./gridDetailsScreen"
|
|
||||||
import createFromScratchScreen from "./createFromScratchScreen"
|
|
||||||
import formScreen from "./formScreen"
|
|
||||||
|
|
||||||
const allTemplates = datasources => [
|
|
||||||
...gridListScreen(datasources),
|
|
||||||
...gridDetailsScreen(datasources),
|
|
||||||
...formScreen(datasources),
|
|
||||||
]
|
|
||||||
|
|
||||||
// Allows us to apply common behaviour to all create() functions
|
|
||||||
const createTemplateOverride = template => () => {
|
|
||||||
const screen = template.create()
|
|
||||||
screen.name = screen.props._id
|
|
||||||
screen.routing.route = screen.routing.route.toLowerCase()
|
|
||||||
screen.template = template.id
|
|
||||||
return screen
|
|
||||||
}
|
|
||||||
|
|
||||||
export default datasources => {
|
|
||||||
const enrichTemplate = template => ({
|
|
||||||
...template,
|
|
||||||
create: createTemplateOverride(template),
|
|
||||||
})
|
|
||||||
const fromScratch = enrichTemplate(createFromScratchScreen)
|
|
||||||
const tableTemplates = allTemplates(datasources).map(enrichTemplate)
|
|
||||||
|
|
||||||
return [
|
|
||||||
fromScratch,
|
|
||||||
...tableTemplates.sort((templateA, templateB) => {
|
|
||||||
return templateA.name > templateB.name ? 1 : -1
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -26,9 +26,14 @@ export const buildAutomationEndpoints = API => ({
|
||||||
/**
|
/**
|
||||||
* Gets a list of all automations.
|
* Gets a list of all automations.
|
||||||
*/
|
*/
|
||||||
getAutomations: async () => {
|
getAutomations: async ({ enrich }) => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (enrich) {
|
||||||
|
params.set("enrich", true)
|
||||||
|
}
|
||||||
|
|
||||||
return await API.get({
|
return await API.get({
|
||||||
url: "/api/automations",
|
url: `/api/automations?${params.toString()}`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as triggers from "../../automations/triggers"
|
import * as triggers from "../../automations/triggers"
|
||||||
|
import { sdk as coreSdk } from "@budibase/shared-core"
|
||||||
import { DocumentType } from "../../db/utils"
|
import { DocumentType } from "../../db/utils"
|
||||||
import { updateTestHistory, removeDeprecated } from "../../automations/utils"
|
import { updateTestHistory, removeDeprecated } from "../../automations/utils"
|
||||||
import { setTestFlag, clearTestFlag } from "../../utilities/redis"
|
import { setTestFlag, clearTestFlag } from "../../utilities/redis"
|
||||||
|
@ -11,6 +12,7 @@ import {
|
||||||
AutomationResults,
|
AutomationResults,
|
||||||
UserCtx,
|
UserCtx,
|
||||||
DeleteAutomationResponse,
|
DeleteAutomationResponse,
|
||||||
|
FetchAutomationResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { getActionDefinitions as actionDefs } from "../../automations/actions"
|
import { getActionDefinitions as actionDefs } from "../../automations/actions"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
|
@ -73,8 +75,17 @@ export async function update(ctx: UserCtx) {
|
||||||
builderSocket?.emitAutomationUpdate(ctx, automation)
|
builderSocket?.emitAutomationUpdate(ctx, automation)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch(ctx: UserCtx) {
|
export async function fetch(ctx: UserCtx<void, FetchAutomationResponse>) {
|
||||||
ctx.body = await sdk.automations.fetch()
|
const query: { enrich?: string } = ctx.request.query || {}
|
||||||
|
const enrich = query.enrich === "true"
|
||||||
|
|
||||||
|
const automations = await sdk.automations.fetch()
|
||||||
|
ctx.body = { automations }
|
||||||
|
if (enrich) {
|
||||||
|
ctx.body.builderData = await sdk.automations.utils.getBuilderData(
|
||||||
|
automations
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function find(ctx: UserCtx) {
|
export async function find(ctx: UserCtx) {
|
||||||
|
@ -84,6 +95,11 @@ export async function find(ctx: UserCtx) {
|
||||||
export async function destroy(ctx: UserCtx<void, DeleteAutomationResponse>) {
|
export async function destroy(ctx: UserCtx<void, DeleteAutomationResponse>) {
|
||||||
const automationId = ctx.params.id
|
const automationId = ctx.params.id
|
||||||
|
|
||||||
|
const automation = await sdk.automations.get(ctx.params.id)
|
||||||
|
if (coreSdk.automations.isRowAction(automation)) {
|
||||||
|
ctx.throw("Row actions automations cannot be deleted", 422)
|
||||||
|
}
|
||||||
|
|
||||||
ctx.body = await sdk.automations.remove(automationId, ctx.params.rev)
|
ctx.body = await sdk.automations.remove(automationId, ctx.params.rev)
|
||||||
builderSocket?.emitAutomationDeletion(ctx, automationId)
|
builderSocket?.emitAutomationDeletion(ctx, automationId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
// need to handle table name + field or just field, depending on if relationships used
|
// need to handle table name + field or just field, depending on if relationships used
|
||||||
import { FieldType, Row, Table } from "@budibase/types"
|
import { FieldType, Row, Table } from "@budibase/types"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers, PROTECTED_INTERNAL_COLUMNS } from "@budibase/shared-core"
|
||||||
import { generateRowIdField } from "../../../../integrations/utils"
|
import { generateRowIdField } from "../../../../integrations/utils"
|
||||||
import { CONSTANT_INTERNAL_ROW_COLS } from "../../../../db/utils"
|
|
||||||
|
|
||||||
function extractFieldValue({
|
function extractFieldValue({
|
||||||
row,
|
row,
|
||||||
|
@ -94,7 +93,7 @@ export function basicProcessing({
|
||||||
thisRow._rev = "rev"
|
thisRow._rev = "rev"
|
||||||
} else {
|
} else {
|
||||||
const columns = Object.keys(table.schema)
|
const columns = Object.keys(table.schema)
|
||||||
for (let internalColumn of [...CONSTANT_INTERNAL_ROW_COLS, ...columns]) {
|
for (let internalColumn of [...PROTECTED_INTERNAL_COLUMNS, ...columns]) {
|
||||||
thisRow[internalColumn] = extractFieldValue({
|
thisRow[internalColumn] = extractFieldValue({
|
||||||
row,
|
row,
|
||||||
tableName: table._id!,
|
tableName: table._id!,
|
||||||
|
|
|
@ -31,7 +31,12 @@ export async function find(ctx: Ctx<void, RowActionsResponse>) {
|
||||||
actions: Object.entries(actions).reduce<Record<string, RowActionResponse>>(
|
actions: Object.entries(actions).reduce<Record<string, RowActionResponse>>(
|
||||||
(acc, [key, action]) => ({
|
(acc, [key, action]) => ({
|
||||||
...acc,
|
...acc,
|
||||||
[key]: { id: key, tableId: table._id!, ...action },
|
[key]: {
|
||||||
|
id: key,
|
||||||
|
tableId: table._id!,
|
||||||
|
name: action.name,
|
||||||
|
automationId: action.automationId,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
),
|
),
|
||||||
|
@ -50,7 +55,9 @@ export async function create(
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
...createdAction,
|
id: createdAction.id,
|
||||||
|
name: createdAction.name,
|
||||||
|
automationId: createdAction.automationId,
|
||||||
}
|
}
|
||||||
ctx.status = 201
|
ctx.status = 201
|
||||||
}
|
}
|
||||||
|
@ -61,13 +68,15 @@ export async function update(
|
||||||
const table = await getTable(ctx)
|
const table = await getTable(ctx)
|
||||||
const { actionId } = ctx.params
|
const { actionId } = ctx.params
|
||||||
|
|
||||||
const actions = await sdk.rowActions.update(table._id!, actionId, {
|
const action = await sdk.rowActions.update(table._id!, actionId, {
|
||||||
name: ctx.request.body.name,
|
name: ctx.request.body.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
...actions,
|
id: action.id,
|
||||||
|
name: action.name,
|
||||||
|
automationId: action.automationId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
export function run() {
|
import { RowActionTriggerRequest, Ctx } from "@budibase/types"
|
||||||
throw new Error("Function not implemented.")
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
|
export async function run(ctx: Ctx<RowActionTriggerRequest, void>) {
|
||||||
|
const { tableId, actionId } = ctx.params
|
||||||
|
const { rowId } = ctx.request.body
|
||||||
|
|
||||||
|
await sdk.rowActions.run(tableId, actionId, rowId)
|
||||||
|
ctx.status = 200
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@ export async function save(
|
||||||
sourceType: rest.sourceType || TableSourceType.INTERNAL,
|
sourceType: rest.sourceType || TableSourceType.INTERNAL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isImport = !!rows
|
||||||
|
|
||||||
if (!tableToSave.views) {
|
if (!tableToSave.views) {
|
||||||
tableToSave.views = {}
|
tableToSave.views = {}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +37,7 @@ export async function save(
|
||||||
rowsToImport: rows,
|
rowsToImport: rows,
|
||||||
tableId: ctx.request.body._id,
|
tableId: ctx.request.body._id,
|
||||||
renaming,
|
renaming,
|
||||||
|
isImport,
|
||||||
})
|
})
|
||||||
|
|
||||||
return table
|
return table
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import Router from "@koa/router"
|
import Router from "@koa/router"
|
||||||
|
import Joi from "joi"
|
||||||
|
import { middleware, permissions } from "@budibase/backend-core"
|
||||||
import * as rowActionController from "../controllers/rowAction"
|
import * as rowActionController from "../controllers/rowAction"
|
||||||
import { authorizedResource } from "../../middleware/authorized"
|
import { authorizedResource } from "../../middleware/authorized"
|
||||||
|
|
||||||
import { middleware, permissions } from "@budibase/backend-core"
|
|
||||||
import Joi from "joi"
|
|
||||||
|
|
||||||
const { PermissionLevel, PermissionType } = permissions
|
const { PermissionLevel, PermissionType } = permissions
|
||||||
|
|
||||||
export function rowActionValidator() {
|
function rowActionValidator() {
|
||||||
return middleware.joiValidator.body(
|
return middleware.joiValidator.body(
|
||||||
Joi.object({
|
Joi.object({
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
|
@ -16,6 +15,15 @@ export function rowActionValidator() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rowTriggerValidator() {
|
||||||
|
return middleware.joiValidator.body(
|
||||||
|
Joi.object({
|
||||||
|
rowId: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
{ allowUnknown: false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const router: Router = new Router()
|
const router: Router = new Router()
|
||||||
|
|
||||||
// CRUD endpoints
|
// CRUD endpoints
|
||||||
|
@ -45,7 +53,8 @@ router
|
||||||
|
|
||||||
// Other endpoints
|
// Other endpoints
|
||||||
.post(
|
.post(
|
||||||
"/api/tables/:tableId/actions/:actionId/run",
|
"/api/tables/:tableId/actions/:actionId/trigger",
|
||||||
|
rowTriggerValidator(),
|
||||||
authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"),
|
authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"),
|
||||||
rowActionController.run
|
rowActionController.run
|
||||||
)
|
)
|
||||||
|
|
|
@ -398,7 +398,9 @@ describe("/automations", () => {
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body[0]).toEqual(expect.objectContaining(autoConfig))
|
expect(res.body.automations[0]).toEqual(
|
||||||
|
expect.objectContaining(autoConfig)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should apply authorization to endpoint", async () => {
|
it("should apply authorization to endpoint", async () => {
|
||||||
|
@ -423,6 +425,22 @@ describe("/automations", () => {
|
||||||
expect(events.automation.deleted).toHaveBeenCalledTimes(1)
|
expect(events.automation.deleted).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("cannot delete a row action automation", async () => {
|
||||||
|
const automation = await config.createAutomation(
|
||||||
|
setup.structures.rowActionAutomation()
|
||||||
|
)
|
||||||
|
await request
|
||||||
|
.delete(`/api/automations/${automation._id}/${automation._rev}`)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(422, {
|
||||||
|
message: "Row actions automations cannot be deleted",
|
||||||
|
status: 422,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(events.automation.deleted).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it("should apply authorization to endpoint", async () => {
|
it("should apply authorization to endpoint", async () => {
|
||||||
const automation = await config.createAutomation()
|
const automation = await config.createAutomation()
|
||||||
await checkBuilderEndpoint({
|
await checkBuilderEndpoint({
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import * as setup from "./utilities"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DatabaseName,
|
DatabaseName,
|
||||||
getDatasource,
|
getDatasource,
|
||||||
|
@ -7,7 +9,6 @@ import {
|
||||||
import tk from "timekeeper"
|
import tk from "timekeeper"
|
||||||
import emitter from "../../../../src/events"
|
import emitter from "../../../../src/events"
|
||||||
import { outputProcessing } from "../../../utilities/rowProcessor"
|
import { outputProcessing } from "../../../utilities/rowProcessor"
|
||||||
import * as setup from "./utilities"
|
|
||||||
import { context, InternalTable, tenancy } from "@budibase/backend-core"
|
import { context, InternalTable, tenancy } from "@budibase/backend-core"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
import tk from "timekeeper"
|
import tk from "timekeeper"
|
||||||
|
|
||||||
import { CreateRowActionRequest, RowActionResponse } from "@budibase/types"
|
import {
|
||||||
|
CreateRowActionRequest,
|
||||||
|
DocumentType,
|
||||||
|
RowActionResponse,
|
||||||
|
} from "@budibase/types"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
|
||||||
|
const expectAutomationId = () =>
|
||||||
|
expect.stringMatching(`^${DocumentType.AUTOMATION}_.+`)
|
||||||
|
|
||||||
describe("/rowsActions", () => {
|
describe("/rowsActions", () => {
|
||||||
const config = setup.getConfig()
|
const config = setup.getConfig()
|
||||||
|
|
||||||
|
@ -79,17 +86,19 @@ describe("/rowsActions", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(res).toEqual({
|
expect(res).toEqual({
|
||||||
|
name: rowAction.name,
|
||||||
id: expect.stringMatching(/^row_action_\w+/),
|
id: expect.stringMatching(/^row_action_\w+/),
|
||||||
tableId: tableId,
|
tableId: tableId,
|
||||||
...rowAction,
|
automationId: expectAutomationId(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(await config.api.rowAction.find(tableId)).toEqual({
|
expect(await config.api.rowAction.find(tableId)).toEqual({
|
||||||
actions: {
|
actions: {
|
||||||
[res.id]: {
|
[res.id]: {
|
||||||
...rowAction,
|
name: rowAction.name,
|
||||||
id: res.id,
|
id: res.id,
|
||||||
tableId: tableId,
|
tableId: tableId,
|
||||||
|
automationId: expectAutomationId(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -97,19 +106,13 @@ describe("/rowsActions", () => {
|
||||||
|
|
||||||
it("trims row action names", async () => {
|
it("trims row action names", async () => {
|
||||||
const name = " action name "
|
const name = " action name "
|
||||||
const res = await createRowAction(
|
const res = await createRowAction(tableId, { name }, { status: 201 })
|
||||||
tableId,
|
|
||||||
{ name },
|
|
||||||
{
|
|
||||||
status: 201,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(res).toEqual({
|
expect(res).toEqual(
|
||||||
id: expect.stringMatching(/^row_action_\w+/),
|
expect.objectContaining({
|
||||||
tableId: tableId,
|
name: "action name",
|
||||||
name: "action name",
|
})
|
||||||
})
|
)
|
||||||
|
|
||||||
expect(await config.api.rowAction.find(tableId)).toEqual({
|
expect(await config.api.rowAction.find(tableId)).toEqual({
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -129,9 +132,24 @@ describe("/rowsActions", () => {
|
||||||
|
|
||||||
expect(await config.api.rowAction.find(tableId)).toEqual({
|
expect(await config.api.rowAction.find(tableId)).toEqual({
|
||||||
actions: {
|
actions: {
|
||||||
[responses[0].id]: { ...rowActions[0], id: responses[0].id, tableId },
|
[responses[0].id]: {
|
||||||
[responses[1].id]: { ...rowActions[1], id: responses[1].id, tableId },
|
name: rowActions[0].name,
|
||||||
[responses[2].id]: { ...rowActions[2], id: responses[2].id, tableId },
|
id: responses[0].id,
|
||||||
|
tableId,
|
||||||
|
automationId: expectAutomationId(),
|
||||||
|
},
|
||||||
|
[responses[1].id]: {
|
||||||
|
name: rowActions[1].name,
|
||||||
|
id: responses[1].id,
|
||||||
|
tableId,
|
||||||
|
automationId: expectAutomationId(),
|
||||||
|
},
|
||||||
|
[responses[2].id]: {
|
||||||
|
name: rowActions[2].name,
|
||||||
|
id: responses[2].id,
|
||||||
|
tableId,
|
||||||
|
automationId: expectAutomationId(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -152,7 +170,7 @@ describe("/rowsActions", () => {
|
||||||
it("ignores not valid row action data", async () => {
|
it("ignores not valid row action data", async () => {
|
||||||
const rowAction = createRowActionRequest()
|
const rowAction = createRowActionRequest()
|
||||||
const dirtyRowAction = {
|
const dirtyRowAction = {
|
||||||
...rowAction,
|
name: rowAction.name,
|
||||||
id: generator.guid(),
|
id: generator.guid(),
|
||||||
valueToIgnore: generator.string(),
|
valueToIgnore: generator.string(),
|
||||||
}
|
}
|
||||||
|
@ -161,17 +179,19 @@ describe("/rowsActions", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(res).toEqual({
|
expect(res).toEqual({
|
||||||
|
name: rowAction.name,
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
tableId,
|
tableId,
|
||||||
...rowAction,
|
automationId: expectAutomationId(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(await config.api.rowAction.find(tableId)).toEqual({
|
expect(await config.api.rowAction.find(tableId)).toEqual({
|
||||||
actions: {
|
actions: {
|
||||||
[res.id]: {
|
[res.id]: {
|
||||||
|
name: rowAction.name,
|
||||||
id: res.id,
|
id: res.id,
|
||||||
tableId: tableId,
|
tableId: tableId,
|
||||||
...rowAction,
|
automationId: expectAutomationId(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -213,6 +233,17 @@ describe("/rowsActions", () => {
|
||||||
|
|
||||||
await createRowAction(otherTable._id!, { name: action.name })
|
await createRowAction(otherTable._id!, { name: action.name })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("an automation is created when creating a new row action", async () => {
|
||||||
|
const action1 = await createRowAction(tableId, createRowActionRequest())
|
||||||
|
const action2 = await createRowAction(tableId, createRowActionRequest())
|
||||||
|
|
||||||
|
for (const automationId of [action1.automationId, action2.automationId]) {
|
||||||
|
expect(
|
||||||
|
await config.api.automation.get(automationId, { status: 200 })
|
||||||
|
).toEqual(expect.objectContaining({ _id: automationId }))
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("find", () => {
|
describe("find", () => {
|
||||||
|
@ -264,7 +295,6 @@ describe("/rowsActions", () => {
|
||||||
const updatedName = generator.string()
|
const updatedName = generator.string()
|
||||||
|
|
||||||
const res = await config.api.rowAction.update(tableId, actionId, {
|
const res = await config.api.rowAction.update(tableId, actionId, {
|
||||||
...actionData,
|
|
||||||
name: updatedName,
|
name: updatedName,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -272,14 +302,17 @@ describe("/rowsActions", () => {
|
||||||
id: actionId,
|
id: actionId,
|
||||||
tableId,
|
tableId,
|
||||||
name: updatedName,
|
name: updatedName,
|
||||||
|
automationId: actionData.automationId,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(await config.api.rowAction.find(tableId)).toEqual(
|
expect(await config.api.rowAction.find(tableId)).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
actions: expect.objectContaining({
|
actions: expect.objectContaining({
|
||||||
[actionId]: {
|
[actionId]: {
|
||||||
...actionData,
|
|
||||||
name: updatedName,
|
name: updatedName,
|
||||||
|
id: actionData.id,
|
||||||
|
tableId: actionData.tableId,
|
||||||
|
automationId: actionData.automationId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
@ -296,7 +329,6 @@ describe("/rowsActions", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const res = await config.api.rowAction.update(tableId, rowAction.id, {
|
const res = await config.api.rowAction.update(tableId, rowAction.id, {
|
||||||
...rowAction,
|
|
||||||
name: " action name ",
|
name: " action name ",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -408,5 +440,26 @@ describe("/rowsActions", () => {
|
||||||
status: 400,
|
status: 400,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("deletes the linked automation", async () => {
|
||||||
|
const actions: RowActionResponse[] = []
|
||||||
|
for (const rowAction of createRowActionRequests(3)) {
|
||||||
|
actions.push(await createRowAction(tableId, rowAction))
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionToDelete = _.sample(actions)!
|
||||||
|
await config.api.rowAction.delete(tableId, actionToDelete.id, {
|
||||||
|
status: 204,
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.api.automation.get(actionToDelete.automationId, {
|
||||||
|
status: 404,
|
||||||
|
})
|
||||||
|
for (const action of actions.filter(a => a.id !== actionToDelete.id)) {
|
||||||
|
await config.api.automation.get(action.automationId, {
|
||||||
|
status: 200,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -54,7 +54,7 @@ export const clearAllApps = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clearAllAutomations = async (config: TestConfiguration) => {
|
export const clearAllAutomations = async (config: TestConfiguration) => {
|
||||||
const automations = await config.getAllAutomations()
|
const { automations } = await config.getAllAutomations()
|
||||||
for (let auto of automations) {
|
for (let auto of automations) {
|
||||||
await context.doInAppContext(config.getAppId(), async () => {
|
await context.doInAppContext(config.getAppId(), async () => {
|
||||||
await config.deleteAutomation(auto)
|
await config.deleteAutomation(auto)
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
|
import {
|
||||||
|
AutomationTriggerSchema,
|
||||||
|
AutomationTriggerStepId,
|
||||||
|
} from "@budibase/types"
|
||||||
import * as app from "./app"
|
import * as app from "./app"
|
||||||
import * as cron from "./cron"
|
import * as cron from "./cron"
|
||||||
import * as rowDeleted from "./rowDeleted"
|
import * as rowDeleted from "./rowDeleted"
|
||||||
import * as rowSaved from "./rowSaved"
|
import * as rowSaved from "./rowSaved"
|
||||||
import * as rowUpdated from "./rowUpdated"
|
import * as rowUpdated from "./rowUpdated"
|
||||||
import * as webhook from "./webhook"
|
import * as webhook from "./webhook"
|
||||||
|
import * as rowAction from "./rowAction"
|
||||||
|
|
||||||
export const definitions = {
|
export const definitions: Record<
|
||||||
|
keyof typeof AutomationTriggerStepId,
|
||||||
|
AutomationTriggerSchema
|
||||||
|
> = {
|
||||||
ROW_SAVED: rowSaved.definition,
|
ROW_SAVED: rowSaved.definition,
|
||||||
ROW_UPDATED: rowUpdated.definition,
|
ROW_UPDATED: rowUpdated.definition,
|
||||||
ROW_DELETED: rowDeleted.definition,
|
ROW_DELETED: rowDeleted.definition,
|
||||||
WEBHOOK: webhook.definition,
|
WEBHOOK: webhook.definition,
|
||||||
APP: app.definition,
|
APP: app.definition,
|
||||||
CRON: cron.definition,
|
CRON: cron.definition,
|
||||||
|
ROW_ACTION: rowAction.definition,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import {
|
||||||
|
AutomationCustomIOType,
|
||||||
|
AutomationIOType,
|
||||||
|
AutomationStepType,
|
||||||
|
AutomationTriggerSchema,
|
||||||
|
AutomationTriggerStepId,
|
||||||
|
AutomationEventType,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
export const definition: AutomationTriggerSchema = {
|
||||||
|
type: AutomationStepType.TRIGGER,
|
||||||
|
name: "Row Action",
|
||||||
|
event: AutomationEventType.ROW_ACTION, // TODO
|
||||||
|
icon: "Workflow", // TODO
|
||||||
|
tagline:
|
||||||
|
"Row action triggered in {{inputs.enriched.table.name}} by {{inputs.enriched.row._id}}",
|
||||||
|
description: "TODO description", // TODO
|
||||||
|
stepId: AutomationTriggerStepId.ROW_ACTION,
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
tableId: {
|
||||||
|
type: AutomationIOType.STRING,
|
||||||
|
customType: AutomationCustomIOType.TABLE,
|
||||||
|
title: "Table",
|
||||||
|
readonly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["tableId"],
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: AutomationIOType.STRING,
|
||||||
|
description: "Row ID - can be used for updating",
|
||||||
|
},
|
||||||
|
revision: {
|
||||||
|
type: AutomationIOType.STRING,
|
||||||
|
description: "Revision of row",
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
type: AutomationIOType.OBJECT,
|
||||||
|
customType: AutomationCustomIOType.TABLE,
|
||||||
|
title: "The table linked to the row action",
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
type: AutomationIOType.OBJECT,
|
||||||
|
customType: AutomationCustomIOType.ROW,
|
||||||
|
description: "The row linked to the row action",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ import {
|
||||||
AutomationStatus,
|
AutomationStatus,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { executeInThread } from "../threads/automation"
|
import { executeInThread } from "../threads/automation"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
import { dataFilters, sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export const TRIGGER_DEFINITIONS = definitions
|
export const TRIGGER_DEFINITIONS = definitions
|
||||||
const JOB_OPTS = {
|
const JOB_OPTS = {
|
||||||
|
@ -121,17 +121,15 @@ function rowPassesFilters(row: Row, filters: SearchFilters) {
|
||||||
|
|
||||||
export async function externalTrigger(
|
export async function externalTrigger(
|
||||||
automation: Automation,
|
automation: Automation,
|
||||||
params: { fields: Record<string, any>; timeout?: number },
|
params: { fields: Record<string, any>; timeout?: number; appId?: string },
|
||||||
{ getResponses }: { getResponses?: boolean } = {}
|
{ getResponses }: { getResponses?: boolean } = {}
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
if (automation.disabled) {
|
if (automation.disabled) {
|
||||||
throw new Error("Automation is disabled")
|
throw new Error("Automation is disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
automation.definition != null &&
|
sdk.automations.isAppAction(automation) &&
|
||||||
automation.definition.trigger != null &&
|
|
||||||
automation.definition.trigger.stepId === definitions.APP.stepId &&
|
|
||||||
automation.definition.trigger.stepId === "APP" &&
|
|
||||||
!(await checkTestFlag(automation._id!))
|
!(await checkTestFlag(automation._id!))
|
||||||
) {
|
) {
|
||||||
// values are likely to be submitted as strings, so we shall convert to correct type
|
// values are likely to be submitted as strings, so we shall convert to correct type
|
||||||
|
@ -141,6 +139,13 @@ export async function externalTrigger(
|
||||||
coercedFields[key] = coerce(params.fields[key], fields[key])
|
coercedFields[key] = coerce(params.fields[key], fields[key])
|
||||||
}
|
}
|
||||||
params.fields = coercedFields
|
params.fields = coercedFields
|
||||||
|
} else if (sdk.automations.isRowAction(automation)) {
|
||||||
|
params = {
|
||||||
|
...params,
|
||||||
|
// Until we don't refactor all the types, we want to flatten the nested "fields" object
|
||||||
|
...params.fields,
|
||||||
|
fields: {},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const data: AutomationData = { automation, event: params }
|
const data: AutomationData = { automation, event: params }
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
Database,
|
Database,
|
||||||
FieldSchema,
|
FieldSchema,
|
||||||
FieldType,
|
FieldType,
|
||||||
LinkDocumentValue,
|
|
||||||
RelationshipFieldMetadata,
|
RelationshipFieldMetadata,
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
Row,
|
Row,
|
||||||
|
@ -213,11 +212,10 @@ class LinkController {
|
||||||
linkedSchema?.relationshipType === RelationshipType.ONE_TO_MANY
|
linkedSchema?.relationshipType === RelationshipType.ONE_TO_MANY
|
||||||
) {
|
) {
|
||||||
let links = (
|
let links = (
|
||||||
(await getLinkDocuments({
|
await getLinkDocuments({
|
||||||
tableId: field.tableId,
|
tableId: field.tableId,
|
||||||
rowId: linkId,
|
rowId: linkId,
|
||||||
includeDocs: IncludeDocs.EXCLUDE,
|
})
|
||||||
})) as LinkDocumentValue[]
|
|
||||||
).filter(
|
).filter(
|
||||||
link =>
|
link =>
|
||||||
link.id !== row._id && link.fieldName === linkedSchema.name
|
link.id !== row._id && link.fieldName === linkedSchema.name
|
||||||
|
@ -295,13 +293,7 @@ class LinkController {
|
||||||
if (linkDocs.length === 0) {
|
if (linkDocs.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const toDelete = linkDocs.map(doc => {
|
await this._db.bulkRemove(linkDocs, { silenceErrors: true })
|
||||||
return {
|
|
||||||
...doc,
|
|
||||||
_deleted: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await this._db.bulkDocs(toDelete)
|
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,14 +313,8 @@ class LinkController {
|
||||||
: linkDoc.doc2.fieldName
|
: linkDoc.doc2.fieldName
|
||||||
return correctFieldName === fieldName
|
return correctFieldName === fieldName
|
||||||
})
|
})
|
||||||
await this._db.bulkDocs(
|
await this._db.bulkRemove(toDelete, { silenceErrors: true })
|
||||||
toDelete.map(doc => {
|
|
||||||
return {
|
|
||||||
...doc,
|
|
||||||
_deleted: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
try {
|
try {
|
||||||
// remove schema from other table, if it exists
|
// remove schema from other table, if it exists
|
||||||
let linkedTable = await this._db.get<Table>(field.tableId)
|
let linkedTable = await this._db.get<Table>(field.tableId)
|
||||||
|
@ -453,13 +439,7 @@ class LinkController {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
// get link docs for this table and configure for deletion
|
// get link docs for this table and configure for deletion
|
||||||
const toDelete = linkDocs.map(doc => {
|
await this._db.bulkRemove(linkDocs, { silenceErrors: true })
|
||||||
return {
|
|
||||||
...doc,
|
|
||||||
_deleted: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await this._db.bulkDocs(toDelete)
|
|
||||||
return table
|
return table
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import LinkController from "./LinkController"
|
import LinkController from "./LinkController"
|
||||||
import {
|
import {
|
||||||
IncludeDocs,
|
|
||||||
getLinkDocuments,
|
getLinkDocuments,
|
||||||
getUniqueByProp,
|
getUniqueByProp,
|
||||||
getRelatedTableForField,
|
getRelatedTableForField,
|
||||||
|
@ -56,12 +55,9 @@ async function getLinksForRows(rows: Row[]): Promise<LinkDocumentValue[]> {
|
||||||
const promises = tableIds.map(tableId =>
|
const promises = tableIds.map(tableId =>
|
||||||
getLinkDocuments({
|
getLinkDocuments({
|
||||||
tableId: tableId,
|
tableId: tableId,
|
||||||
includeDocs: IncludeDocs.EXCLUDE,
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const responses = flatten(
|
const responses = flatten(await Promise.all(promises))
|
||||||
(await Promise.all(promises)) as LinkDocumentValue[][]
|
|
||||||
)
|
|
||||||
// have to get unique as the previous table query can
|
// have to get unique as the previous table query can
|
||||||
// return duplicates, could be querying for both tables in a relation
|
// return duplicates, could be querying for both tables in a relation
|
||||||
return getUniqueByProp(
|
return getUniqueByProp(
|
||||||
|
|
|
@ -34,6 +34,17 @@ export const IncludeDocs = {
|
||||||
* @returns This will return an array of the linking documents that were found
|
* @returns This will return an array of the linking documents that were found
|
||||||
* (if any).
|
* (if any).
|
||||||
*/
|
*/
|
||||||
|
export function getLinkDocuments(args: {
|
||||||
|
tableId?: string
|
||||||
|
rowId?: string
|
||||||
|
fieldName?: string
|
||||||
|
includeDocs: boolean
|
||||||
|
}): Promise<LinkDocument[]>
|
||||||
|
export function getLinkDocuments(args: {
|
||||||
|
tableId?: string
|
||||||
|
rowId?: string
|
||||||
|
fieldName?: string
|
||||||
|
}): Promise<LinkDocumentValue[]>
|
||||||
export async function getLinkDocuments(args: {
|
export async function getLinkDocuments(args: {
|
||||||
tableId?: string
|
tableId?: string
|
||||||
rowId?: string
|
rowId?: string
|
||||||
|
|
|
@ -57,14 +57,6 @@ export const getUserMetadataParams = dbCore.getUserMetadataParams
|
||||||
export const generateUserMetadataID = dbCore.generateUserMetadataID
|
export const generateUserMetadataID = dbCore.generateUserMetadataID
|
||||||
export const getGlobalIDFromUserMetadataID =
|
export const getGlobalIDFromUserMetadataID =
|
||||||
dbCore.getGlobalIDFromUserMetadataID
|
dbCore.getGlobalIDFromUserMetadataID
|
||||||
export const CONSTANT_INTERNAL_ROW_COLS = [
|
|
||||||
"_id",
|
|
||||||
"_rev",
|
|
||||||
"type",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt",
|
|
||||||
"tableId",
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets parameters for retrieving tables, this is a utility function for the getDocParams function.
|
* Gets parameters for retrieving tables, this is a utility function for the getDocParams function.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
import {
|
import {
|
||||||
Automation,
|
Automation,
|
||||||
RequiredKeys,
|
RequiredKeys,
|
||||||
|
@ -16,6 +17,11 @@ import {
|
||||||
import { definitions } from "../../../automations/triggerInfo"
|
import { definitions } from "../../../automations/triggerInfo"
|
||||||
import automations from "."
|
import automations from "."
|
||||||
|
|
||||||
|
export interface PersistedAutomation extends Automation {
|
||||||
|
_id: string
|
||||||
|
_rev: string
|
||||||
|
}
|
||||||
|
|
||||||
function getDb() {
|
function getDb() {
|
||||||
return context.getAppDB()
|
return context.getAppDB()
|
||||||
}
|
}
|
||||||
|
@ -76,7 +82,7 @@ async function handleStepEvents(
|
||||||
|
|
||||||
export async function fetch() {
|
export async function fetch() {
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
const response = await db.allDocs<Automation>(
|
const response = await db.allDocs<PersistedAutomation>(
|
||||||
getAutomationParams(null, {
|
getAutomationParams(null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
|
@ -89,7 +95,7 @@ export async function fetch() {
|
||||||
|
|
||||||
export async function get(automationId: string) {
|
export async function get(automationId: string) {
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
const result = await db.get<Automation>(automationId)
|
const result = await db.get<PersistedAutomation>(automationId)
|
||||||
return trimUnexpectedObjectFields(result)
|
return trimUnexpectedObjectFields(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +133,9 @@ export async function update(automation: Automation) {
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
|
|
||||||
const oldAutomation = await db.get<Automation>(automation._id)
|
const oldAutomation = await db.get<Automation>(automation._id)
|
||||||
|
|
||||||
|
guardInvalidUpdatesAndThrow(automation, oldAutomation)
|
||||||
|
|
||||||
automation = cleanAutomationInputs(automation)
|
automation = cleanAutomationInputs(automation)
|
||||||
automation = await checkForWebhooks({
|
automation = await checkForWebhooks({
|
||||||
oldAuto: oldAutomation,
|
oldAuto: oldAutomation,
|
||||||
|
@ -254,6 +263,41 @@ async function checkForWebhooks({ oldAuto, newAuto }: any) {
|
||||||
return newAuto
|
return newAuto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function guardInvalidUpdatesAndThrow(
|
||||||
|
automation: Automation,
|
||||||
|
oldAutomation: Automation
|
||||||
|
) {
|
||||||
|
const stepDefinitions = [
|
||||||
|
automation.definition.trigger,
|
||||||
|
...automation.definition.steps,
|
||||||
|
]
|
||||||
|
const oldStepDefinitions = [
|
||||||
|
oldAutomation.definition.trigger,
|
||||||
|
...oldAutomation.definition.steps,
|
||||||
|
]
|
||||||
|
for (const step of stepDefinitions) {
|
||||||
|
const readonlyFields = Object.keys(
|
||||||
|
step.schema.inputs.properties || {}
|
||||||
|
).filter(k => step.schema.inputs.properties[k].readonly)
|
||||||
|
readonlyFields.forEach(readonlyField => {
|
||||||
|
const oldStep = oldStepDefinitions.find(i => i.id === step.id)
|
||||||
|
if (step.inputs[readonlyField] !== oldStep?.inputs[readonlyField]) {
|
||||||
|
throw new HTTPError(
|
||||||
|
`Field ${readonlyField} is readonly and it cannot be modified`,
|
||||||
|
400
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
sdk.automations.isRowAction(automation) &&
|
||||||
|
automation.name !== oldAutomation.name
|
||||||
|
) {
|
||||||
|
throw new Error("Row actions cannot be renamed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function trimUnexpectedObjectFields<T extends Automation>(automation: T): T {
|
function trimUnexpectedObjectFields<T extends Automation>(automation: T): T {
|
||||||
// This will ensure all the automation fields (and nothing else) is mapped to the result
|
// This will ensure all the automation fields (and nothing else) is mapped to the result
|
||||||
const allRequired: RequiredKeys<Automation> = {
|
const allRequired: RequiredKeys<Automation> = {
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { sample } from "lodash/fp"
|
||||||
|
import { Automation, AutomationTriggerStepId } from "@budibase/types"
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
|
||||||
|
import automationSdk from "../"
|
||||||
|
import { structures } from "../../../../api/routes/tests/utilities"
|
||||||
|
|
||||||
|
describe("automation sdk", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("update", () => {
|
||||||
|
it("can rename existing automations", async () => {
|
||||||
|
await config.doInContext(config.getAppId(), async () => {
|
||||||
|
const automation = structures.newAutomation()
|
||||||
|
|
||||||
|
const response = await automationSdk.create(automation)
|
||||||
|
|
||||||
|
const newName = generator.guid()
|
||||||
|
const update = { ...response, name: newName }
|
||||||
|
const result = await automationSdk.update(update)
|
||||||
|
expect(result.name).toEqual(newName)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("cannot rename row action automations", async () => {
|
||||||
|
await config.doInContext(config.getAppId(), async () => {
|
||||||
|
const automation = structures.newAutomation({
|
||||||
|
trigger: {
|
||||||
|
...structures.automationTrigger(),
|
||||||
|
stepId: AutomationTriggerStepId.ROW_ACTION,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await automationSdk.create(automation)
|
||||||
|
|
||||||
|
const newName = generator.guid()
|
||||||
|
const update = { ...response, name: newName }
|
||||||
|
await expect(automationSdk.update(update)).rejects.toThrow(
|
||||||
|
"Row actions cannot be renamed"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["trigger", (a: Automation) => a.definition.trigger],
|
||||||
|
["step", (a: Automation) => a.definition.steps[0]],
|
||||||
|
])("can update input fields (for a %s)", async (_, getStep) => {
|
||||||
|
await config.doInContext(config.getAppId(), async () => {
|
||||||
|
const automation = structures.newAutomation()
|
||||||
|
|
||||||
|
const keyToUse = sample(Object.keys(getStep(automation).inputs))!
|
||||||
|
getStep(automation).inputs[keyToUse] = "anyValue"
|
||||||
|
|
||||||
|
const response = await automationSdk.create(automation)
|
||||||
|
|
||||||
|
const update = { ...response }
|
||||||
|
getStep(update).inputs[keyToUse] = "anyUpdatedValue"
|
||||||
|
const result = await automationSdk.update(update)
|
||||||
|
expect(getStep(result).inputs[keyToUse]).toEqual("anyUpdatedValue")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["trigger", (a: Automation) => a.definition.trigger],
|
||||||
|
["step", (a: Automation) => a.definition.steps[0]],
|
||||||
|
])("cannot update readonly fields (for a %s)", async (_, getStep) => {
|
||||||
|
await config.doInContext(config.getAppId(), async () => {
|
||||||
|
const automation = structures.newAutomation()
|
||||||
|
getStep(automation).schema.inputs.properties["readonlyProperty"] = {
|
||||||
|
readonly: true,
|
||||||
|
}
|
||||||
|
getStep(automation).inputs["readonlyProperty"] = "anyValue"
|
||||||
|
|
||||||
|
const response = await automationSdk.create(automation)
|
||||||
|
|
||||||
|
const update = { ...response }
|
||||||
|
getStep(update).inputs["readonlyProperty"] = "anyUpdatedValue"
|
||||||
|
await expect(automationSdk.update(update)).rejects.toThrow(
|
||||||
|
"Field readonlyProperty is readonly and it cannot be modified"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,7 +1,66 @@
|
||||||
import { Automation, AutomationActionStepId } from "@budibase/types"
|
import {
|
||||||
|
Automation,
|
||||||
|
AutomationActionStepId,
|
||||||
|
AutomationBuilderData,
|
||||||
|
TableRowActions,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { sdk as coreSdk } from "@budibase/shared-core"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
export function checkForCollectStep(automation: Automation) {
|
export function checkForCollectStep(automation: Automation) {
|
||||||
return automation.definition.steps.some(
|
return automation.definition.steps.some(
|
||||||
(step: any) => step.stepId === AutomationActionStepId.COLLECT
|
(step: any) => step.stepId === AutomationActionStepId.COLLECT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getBuilderData(
|
||||||
|
automations: Automation[]
|
||||||
|
): Promise<Record<string, AutomationBuilderData>> {
|
||||||
|
const tableNameCache: Record<string, string> = {}
|
||||||
|
async function getTableName(tableId: string) {
|
||||||
|
if (!tableNameCache[tableId]) {
|
||||||
|
const table = await sdk.tables.getTable(tableId)
|
||||||
|
tableNameCache[tableId] = table.name
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableNameCache[tableId]
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowActionNameCache: Record<string, TableRowActions> = {}
|
||||||
|
async function getRowActionName(tableId: string, rowActionId: string) {
|
||||||
|
if (!rowActionNameCache[tableId]) {
|
||||||
|
const rowActions = await sdk.rowActions.get(tableId)
|
||||||
|
rowActionNameCache[tableId] = rowActions
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowActionNameCache[tableId].actions[rowActionId]?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Record<string, AutomationBuilderData> = {}
|
||||||
|
for (const automation of automations) {
|
||||||
|
const isRowAction = coreSdk.automations.isRowAction(automation)
|
||||||
|
if (!isRowAction) {
|
||||||
|
result[automation._id!] = { displayName: automation.name }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tableId, rowActionId } = automation.definition.trigger.inputs
|
||||||
|
|
||||||
|
const tableName = await getTableName(tableId)
|
||||||
|
|
||||||
|
const rowActionName = await getRowActionName(tableId, rowActionId)
|
||||||
|
|
||||||
|
result[automation._id!] = {
|
||||||
|
displayName: `${tableName}: ${automation.name}`,
|
||||||
|
triggerInfo: {
|
||||||
|
type: "Automation trigger",
|
||||||
|
table: { id: tableId, name: tableName },
|
||||||
|
rowAction: {
|
||||||
|
id: rowActionId,
|
||||||
|
name: rowActionName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { context, HTTPError, utils } from "@budibase/backend-core"
|
import { context, HTTPError, utils } from "@budibase/backend-core"
|
||||||
|
|
||||||
import { generateRowActionsID } from "../../db/utils"
|
|
||||||
import {
|
import {
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
TableRowActions,
|
TableRowActions,
|
||||||
VirtualDocumentType,
|
VirtualDocumentType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import { generateRowActionsID } from "../../db/utils"
|
||||||
|
import automations from "./automations"
|
||||||
|
import { definitions as TRIGGER_DEFINITIONS } from "../../automations/triggerInfo"
|
||||||
|
import * as triggers from "../../automations/triggers"
|
||||||
|
import sdk from ".."
|
||||||
|
|
||||||
function ensureUniqueAndThrow(
|
function ensureUniqueAndThrow(
|
||||||
doc: TableRowActions,
|
doc: TableRowActions,
|
||||||
|
@ -41,13 +45,40 @@ export async function create(tableId: string, rowAction: { name: string }) {
|
||||||
|
|
||||||
ensureUniqueAndThrow(doc, action.name)
|
ensureUniqueAndThrow(doc, action.name)
|
||||||
|
|
||||||
const newId = `${VirtualDocumentType.ROW_ACTION}${SEPARATOR}${utils.newid()}`
|
const appId = context.getAppId()
|
||||||
doc.actions[newId] = action
|
if (!appId) {
|
||||||
|
throw new Error("Could not get the current appId")
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRowActionId = `${
|
||||||
|
VirtualDocumentType.ROW_ACTION
|
||||||
|
}${SEPARATOR}${utils.newid()}`
|
||||||
|
|
||||||
|
const automation = await automations.create({
|
||||||
|
name: action.name,
|
||||||
|
appId,
|
||||||
|
definition: {
|
||||||
|
trigger: {
|
||||||
|
id: "trigger",
|
||||||
|
...TRIGGER_DEFINITIONS.ROW_ACTION,
|
||||||
|
inputs: {
|
||||||
|
tableId,
|
||||||
|
rowActionId: newRowActionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
steps: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
doc.actions[newRowActionId] = {
|
||||||
|
name: action.name,
|
||||||
|
automationId: automation._id!,
|
||||||
|
}
|
||||||
await db.put(doc)
|
await db.put(doc)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: newId,
|
id: newRowActionId,
|
||||||
...action,
|
...doc.actions[newRowActionId],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,29 +112,61 @@ export async function update(
|
||||||
|
|
||||||
ensureUniqueAndThrow(actionsDoc, action.name, rowActionId)
|
ensureUniqueAndThrow(actionsDoc, action.name, rowActionId)
|
||||||
|
|
||||||
actionsDoc.actions[rowActionId] = action
|
actionsDoc.actions[rowActionId] = {
|
||||||
|
automationId: actionsDoc.actions[rowActionId].automationId,
|
||||||
|
...action,
|
||||||
|
}
|
||||||
|
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
await db.put(actionsDoc)
|
await db.put(actionsDoc)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: rowActionId,
|
id: rowActionId,
|
||||||
...action,
|
...actionsDoc.actions[rowActionId],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove(tableId: string, rowActionId: string) {
|
export async function remove(tableId: string, rowActionId: string) {
|
||||||
const actionsDoc = await get(tableId)
|
const actionsDoc = await get(tableId)
|
||||||
|
|
||||||
if (!actionsDoc.actions[rowActionId]) {
|
const rowAction = actionsDoc.actions[rowActionId]
|
||||||
|
if (!rowAction) {
|
||||||
throw new HTTPError(
|
throw new HTTPError(
|
||||||
`Row action '${rowActionId}' not found in '${tableId}'`,
|
`Row action '${rowActionId}' not found in '${tableId}'`,
|
||||||
400
|
400
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { automationId } = rowAction
|
||||||
|
const automation = await automations.get(automationId)
|
||||||
|
await automations.remove(automation._id, automation._rev)
|
||||||
delete actionsDoc.actions[rowActionId]
|
delete actionsDoc.actions[rowActionId]
|
||||||
|
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
await db.put(actionsDoc)
|
await db.put(actionsDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function run(tableId: any, rowActionId: any, rowId: string) {
|
||||||
|
const table = await sdk.tables.getTable(tableId)
|
||||||
|
if (!table) {
|
||||||
|
throw new HTTPError("Table not found", 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { actions } = await get(tableId)
|
||||||
|
|
||||||
|
const rowAction = actions[rowActionId]
|
||||||
|
if (!rowAction) {
|
||||||
|
throw new HTTPError("Row action not found", 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
const automation = await sdk.automations.get(rowAction.automationId)
|
||||||
|
|
||||||
|
const row = await sdk.rows.find(tableId, rowId)
|
||||||
|
await triggers.externalTrigger(automation, {
|
||||||
|
fields: {
|
||||||
|
row,
|
||||||
|
table,
|
||||||
|
},
|
||||||
|
appId: context.getAppId(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
breakExternalTableId,
|
breakExternalTableId,
|
||||||
breakRowIdField,
|
breakRowIdField,
|
||||||
} from "../../../../integrations/utils"
|
} from "../../../../integrations/utils"
|
||||||
import { utils, CONSTANT_EXTERNAL_ROW_COLS } from "@budibase/shared-core"
|
import { utils, PROTECTED_EXTERNAL_COLUMNS } from "@budibase/shared-core"
|
||||||
import { ExportRowsParams, ExportRowsResult } from "./types"
|
import { ExportRowsParams, ExportRowsResult } from "./types"
|
||||||
import { HTTPError } from "@budibase/backend-core"
|
import { HTTPError } from "@budibase/backend-core"
|
||||||
import pick from "lodash/pick"
|
import pick from "lodash/pick"
|
||||||
|
@ -99,7 +99,7 @@ export async function search(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.fields) {
|
if (options.fields) {
|
||||||
const fields = [...options.fields, ...CONSTANT_EXTERNAL_ROW_COLS]
|
const fields = [...options.fields, ...PROTECTED_EXTERNAL_COLUMNS]
|
||||||
rows = rows.map((r: any) => pick(r, fields))
|
rows = rows.map((r: any) => pick(r, fields))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { context, HTTPError } from "@budibase/backend-core"
|
import { context, HTTPError } from "@budibase/backend-core"
|
||||||
import { CONSTANT_INTERNAL_ROW_COLS } from "@budibase/shared-core"
|
import { PROTECTED_INTERNAL_COLUMNS } from "@budibase/shared-core"
|
||||||
import env from "../../../../environment"
|
import env from "../../../../environment"
|
||||||
import { fullSearch, paginatedSearch } from "./utils"
|
import { fullSearch, paginatedSearch } from "./utils"
|
||||||
import { getRowParams, InternalTables } from "../../../../db/utils"
|
import { getRowParams, InternalTables } from "../../../../db/utils"
|
||||||
|
@ -75,7 +75,7 @@ export async function search(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.fields) {
|
if (options.fields) {
|
||||||
const fields = [...options.fields, ...CONSTANT_INTERNAL_ROW_COLS]
|
const fields = [...options.fields, ...PROTECTED_INTERNAL_COLUMNS]
|
||||||
response.rows = response.rows.map((r: any) => pick(r, fields))
|
response.rows = response.rows.map((r: any) => pick(r, fields))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,10 +27,7 @@ import {
|
||||||
SQLITE_DESIGN_DOC_ID,
|
SQLITE_DESIGN_DOC_ID,
|
||||||
SQS_DATASOURCE_INTERNAL,
|
SQS_DATASOURCE_INTERNAL,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import {
|
import { generateJunctionTableID } from "../../../../db/utils"
|
||||||
CONSTANT_INTERNAL_ROW_COLS,
|
|
||||||
generateJunctionTableID,
|
|
||||||
} from "../../../../db/utils"
|
|
||||||
import AliasTables from "../sqlAlias"
|
import AliasTables from "../sqlAlias"
|
||||||
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
||||||
import pick from "lodash/pick"
|
import pick from "lodash/pick"
|
||||||
|
@ -40,7 +37,11 @@ import {
|
||||||
getRelationshipColumns,
|
getRelationshipColumns,
|
||||||
getTableIDList,
|
getTableIDList,
|
||||||
} from "./filters"
|
} from "./filters"
|
||||||
import { dataFilters, helpers } from "@budibase/shared-core"
|
import {
|
||||||
|
dataFilters,
|
||||||
|
helpers,
|
||||||
|
PROTECTED_INTERNAL_COLUMNS,
|
||||||
|
} from "@budibase/shared-core"
|
||||||
|
|
||||||
const builder = new sql.Sql(SqlClient.SQL_LITE)
|
const builder = new sql.Sql(SqlClient.SQL_LITE)
|
||||||
const MISSING_COLUMN_REGEX = new RegExp(`no such column: .+`)
|
const MISSING_COLUMN_REGEX = new RegExp(`no such column: .+`)
|
||||||
|
@ -61,7 +62,7 @@ function buildInternalFieldList(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fieldList = fieldList.concat(
|
fieldList = fieldList.concat(
|
||||||
CONSTANT_INTERNAL_ROW_COLS.map(col => `${table._id}.${col}`)
|
PROTECTED_INTERNAL_COLUMNS.map(col => `${table._id}.${col}`)
|
||||||
)
|
)
|
||||||
for (let col of Object.values(table.schema)) {
|
for (let col of Object.values(table.schema)) {
|
||||||
const isRelationship = col.type === FieldType.LINK
|
const isRelationship = col.type === FieldType.LINK
|
||||||
|
@ -351,7 +352,7 @@ export async function search(
|
||||||
|
|
||||||
// check if we need to pick specific rows out
|
// check if we need to pick specific rows out
|
||||||
if (options.fields) {
|
if (options.fields) {
|
||||||
const fields = [...options.fields, ...CONSTANT_INTERNAL_ROW_COLS]
|
const fields = [...options.fields, ...PROTECTED_INTERNAL_COLUMNS]
|
||||||
finalRows = finalRows.map((r: any) => pick(r, fields))
|
finalRows = finalRows.map((r: any) => pick(r, fields))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ export async function save(
|
||||||
tableId?: string
|
tableId?: string
|
||||||
rowsToImport?: Row[]
|
rowsToImport?: Row[]
|
||||||
renaming?: RenameColumn
|
renaming?: RenameColumn
|
||||||
|
isImport?: boolean
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
@ -47,7 +48,9 @@ export async function save(
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for case sensitivity - we don't want to allow duplicated columns
|
// check for case sensitivity - we don't want to allow duplicated columns
|
||||||
const duplicateColumn = findDuplicateInternalColumns(table)
|
const duplicateColumn = findDuplicateInternalColumns(table, {
|
||||||
|
ignoreProtectedColumnNames: !oldTable && !!opts?.isImport,
|
||||||
|
})
|
||||||
if (duplicateColumn.length) {
|
if (duplicateColumn.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Column(s) "${duplicateColumn.join(
|
`Column(s) "${duplicateColumn.join(
|
||||||
|
|
|
@ -10,13 +10,10 @@ import {
|
||||||
Table,
|
Table,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import tablesSdk from "../"
|
import tablesSdk from "../"
|
||||||
import {
|
import { generateJunctionTableID } from "../../../../db/utils"
|
||||||
CONSTANT_INTERNAL_ROW_COLS,
|
|
||||||
generateJunctionTableID,
|
|
||||||
} from "../../../../db/utils"
|
|
||||||
import { isEqual } from "lodash"
|
import { isEqual } from "lodash"
|
||||||
import { DEFAULT_TABLES } from "../../../../db/defaultData/datasource_bb_default"
|
import { DEFAULT_TABLES } from "../../../../db/defaultData/datasource_bb_default"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers, PROTECTED_INTERNAL_COLUMNS } from "@budibase/shared-core"
|
||||||
|
|
||||||
const FieldTypeMap: Record<FieldType, SQLiteType> = {
|
const FieldTypeMap: Record<FieldType, SQLiteType> = {
|
||||||
[FieldType.BOOLEAN]: SQLiteType.NUMERIC,
|
[FieldType.BOOLEAN]: SQLiteType.NUMERIC,
|
||||||
|
@ -104,7 +101,7 @@ function mapTable(table: Table): SQLiteTables {
|
||||||
}
|
}
|
||||||
// there are some extra columns to map - add these in
|
// there are some extra columns to map - add these in
|
||||||
const constantMap: Record<string, SQLiteType> = {}
|
const constantMap: Record<string, SQLiteType> = {}
|
||||||
CONSTANT_INTERNAL_ROW_COLS.forEach(col => {
|
PROTECTED_INTERNAL_COLUMNS.forEach(col => {
|
||||||
constantMap[col] = SQLiteType.TEXT
|
constantMap[col] = SQLiteType.TEXT
|
||||||
})
|
})
|
||||||
const thisTable: SQLiteTable = {
|
const thisTable: SQLiteTable = {
|
||||||
|
|
|
@ -10,8 +10,8 @@ import { HTTPError } from "@budibase/backend-core"
|
||||||
import { features } from "@budibase/pro"
|
import { features } from "@budibase/pro"
|
||||||
import {
|
import {
|
||||||
helpers,
|
helpers,
|
||||||
CONSTANT_EXTERNAL_ROW_COLS,
|
PROTECTED_EXTERNAL_COLUMNS,
|
||||||
CONSTANT_INTERNAL_ROW_COLS,
|
PROTECTED_INTERNAL_COLUMNS,
|
||||||
} from "@budibase/shared-core"
|
} from "@budibase/shared-core"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
@ -148,8 +148,8 @@ export function allowedFields(view: View | ViewV2) {
|
||||||
const fieldSchema = view.schema![key]
|
const fieldSchema = view.schema![key]
|
||||||
return fieldSchema.visible && !fieldSchema.readonly
|
return fieldSchema.visible && !fieldSchema.readonly
|
||||||
}),
|
}),
|
||||||
...CONSTANT_EXTERNAL_ROW_COLS,
|
...PROTECTED_EXTERNAL_COLUMNS,
|
||||||
...CONSTANT_INTERNAL_ROW_COLS,
|
...PROTECTED_INTERNAL_COLUMNS,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Automation } from "@budibase/types"
|
||||||
|
import { Expectations, TestAPI } from "./base"
|
||||||
|
|
||||||
|
export class AutomationAPI extends TestAPI {
|
||||||
|
get = async (
|
||||||
|
automationId: string,
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<Automation> => {
|
||||||
|
const result = await this._get<Automation>(
|
||||||
|
`/api/automations/${automationId}`,
|
||||||
|
{
|
||||||
|
expectations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import { QueryAPI } from "./query"
|
||||||
import { RoleAPI } from "./role"
|
import { RoleAPI } from "./role"
|
||||||
import { TemplateAPI } from "./template"
|
import { TemplateAPI } from "./template"
|
||||||
import { RowActionAPI } from "./rowAction"
|
import { RowActionAPI } from "./rowAction"
|
||||||
|
import { AutomationAPI } from "./automation"
|
||||||
|
|
||||||
export default class API {
|
export default class API {
|
||||||
table: TableAPI
|
table: TableAPI
|
||||||
|
@ -31,6 +32,7 @@ export default class API {
|
||||||
roles: RoleAPI
|
roles: RoleAPI
|
||||||
templates: TemplateAPI
|
templates: TemplateAPI
|
||||||
rowAction: RowActionAPI
|
rowAction: RowActionAPI
|
||||||
|
automation: AutomationAPI
|
||||||
|
|
||||||
constructor(config: TestConfiguration) {
|
constructor(config: TestConfiguration) {
|
||||||
this.table = new TableAPI(config)
|
this.table = new TableAPI(config)
|
||||||
|
@ -48,5 +50,6 @@ export default class API {
|
||||||
this.roles = new RoleAPI(config)
|
this.roles = new RoleAPI(config)
|
||||||
this.templates = new TemplateAPI(config)
|
this.templates = new TemplateAPI(config)
|
||||||
this.rowAction = new RowActionAPI(config)
|
this.rowAction = new RowActionAPI(config)
|
||||||
|
this.automation = new AutomationAPI(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,7 +158,10 @@ export function automationTrigger(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function newAutomation({ steps, trigger }: any = {}) {
|
export function newAutomation({
|
||||||
|
steps,
|
||||||
|
trigger,
|
||||||
|
}: { steps?: AutomationStep[]; trigger?: AutomationTrigger } = {}) {
|
||||||
const automation = basicAutomation()
|
const automation = basicAutomation()
|
||||||
|
|
||||||
if (trigger) {
|
if (trigger) {
|
||||||
|
@ -176,6 +179,16 @@ export function newAutomation({ steps, trigger }: any = {}) {
|
||||||
return automation
|
return automation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function rowActionAutomation() {
|
||||||
|
const automation = newAutomation({
|
||||||
|
trigger: {
|
||||||
|
...automationTrigger(),
|
||||||
|
stepId: AutomationTriggerStepId.ROW_ACTION,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return automation
|
||||||
|
}
|
||||||
|
|
||||||
export function basicAutomation(appId?: string): Automation {
|
export function basicAutomation(appId?: string): Automation {
|
||||||
return {
|
return {
|
||||||
name: "My Automation",
|
name: "My Automation",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const CONSTANT_INTERNAL_ROW_COLS = [
|
export const PROTECTED_INTERNAL_COLUMNS = [
|
||||||
"_id",
|
"_id",
|
||||||
"_rev",
|
"_rev",
|
||||||
"type",
|
"type",
|
||||||
|
@ -7,8 +7,8 @@ export const CONSTANT_INTERNAL_ROW_COLS = [
|
||||||
"tableId",
|
"tableId",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const
|
export const PROTECTED_EXTERNAL_COLUMNS = ["_id", "_rev", "tableId"] as const
|
||||||
|
|
||||||
export function isInternalColumnName(name: string): boolean {
|
export function isInternalColumnName(name: string): boolean {
|
||||||
return (CONSTANT_INTERNAL_ROW_COLS as readonly string[]).includes(name)
|
return (PROTECTED_INTERNAL_COLUMNS as readonly string[]).includes(name)
|
||||||
}
|
}
|
||||||
|
|
|
@ -594,7 +594,7 @@ export const runQuery = (docs: Record<string, any>[], query: SearchFilters) => {
|
||||||
if (Array.isArray(docValue)) {
|
if (Array.isArray(docValue)) {
|
||||||
return docValue.length === 0
|
return docValue.length === 0
|
||||||
}
|
}
|
||||||
if (typeof docValue === "object") {
|
if (docValue && typeof docValue === "object") {
|
||||||
return Object.keys(docValue).length === 0
|
return Object.keys(docValue).length === 0
|
||||||
}
|
}
|
||||||
return docValue == null
|
return docValue == null
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Automation, AutomationTriggerStepId } from "@budibase/types"
|
||||||
|
|
||||||
|
export function isRowAction(automation: Automation) {
|
||||||
|
const result =
|
||||||
|
automation.definition.trigger?.stepId === AutomationTriggerStepId.ROW_ACTION
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAppAction(automation: Automation) {
|
||||||
|
const result =
|
||||||
|
automation.definition.trigger?.stepId === AutomationTriggerStepId.APP
|
||||||
|
return result
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
export * as applications from "./applications"
|
export * as applications from "./applications"
|
||||||
|
export * as automations from "./automations"
|
||||||
export * as users from "./users"
|
export * as users from "./users"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { FieldType, Table } from "@budibase/types"
|
import { FieldType, Table } from "@budibase/types"
|
||||||
import { CONSTANT_INTERNAL_ROW_COLS } from "./constants"
|
import { PROTECTED_INTERNAL_COLUMNS } from "./constants"
|
||||||
|
|
||||||
const allowDisplayColumnByType: Record<FieldType, boolean> = {
|
const allowDisplayColumnByType: Record<FieldType, boolean> = {
|
||||||
[FieldType.STRING]: true,
|
[FieldType.STRING]: true,
|
||||||
|
@ -53,7 +53,10 @@ export function canBeSortColumn(type: FieldType): boolean {
|
||||||
return !!allowSortColumnByType[type]
|
return !!allowSortColumnByType[type]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findDuplicateInternalColumns(table: Table): string[] {
|
export function findDuplicateInternalColumns(
|
||||||
|
table: Table,
|
||||||
|
opts?: { ignoreProtectedColumnNames: boolean }
|
||||||
|
): string[] {
|
||||||
// maintains the case of keys
|
// maintains the case of keys
|
||||||
const casedKeys = Object.keys(table.schema)
|
const casedKeys = Object.keys(table.schema)
|
||||||
// get the column names
|
// get the column names
|
||||||
|
@ -69,9 +72,11 @@ export function findDuplicateInternalColumns(table: Table): string[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let internalColumn of CONSTANT_INTERNAL_ROW_COLS) {
|
if (!opts?.ignoreProtectedColumnNames) {
|
||||||
if (casedKeys.find(key => key === internalColumn)) {
|
for (let internalColumn of PROTECTED_INTERNAL_COLUMNS) {
|
||||||
duplicates.push(internalColumn)
|
if (casedKeys.find(key => key === internalColumn)) {
|
||||||
|
duplicates.push(internalColumn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return duplicates
|
return duplicates
|
||||||
|
|
|
@ -7,8 +7,13 @@ export interface UpdateRowActionRequest extends RowActionData {}
|
||||||
export interface RowActionResponse extends RowActionData {
|
export interface RowActionResponse extends RowActionData {
|
||||||
id: string
|
id: string
|
||||||
tableId: string
|
tableId: string
|
||||||
|
automationId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RowActionsResponse {
|
export interface RowActionsResponse {
|
||||||
actions: Record<string, RowActionResponse>
|
actions: Record<string, RowActionResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RowActionTriggerRequest {
|
||||||
|
rowId: string
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,24 @@
|
||||||
import { DocumentDestroyResponse } from "@budibase/nano"
|
import { DocumentDestroyResponse } from "@budibase/nano"
|
||||||
|
import { Automation } from "../../documents"
|
||||||
|
|
||||||
export interface DeleteAutomationResponse extends DocumentDestroyResponse {}
|
export interface DeleteAutomationResponse extends DocumentDestroyResponse {}
|
||||||
|
|
||||||
|
export interface AutomationBuilderData {
|
||||||
|
displayName: string
|
||||||
|
triggerInfo?: {
|
||||||
|
type: string
|
||||||
|
table: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
rowAction: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetchAutomationResponse {
|
||||||
|
automations: Automation[]
|
||||||
|
builderData?: Record<string, AutomationBuilderData> // The key will be the automationId
|
||||||
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ export enum AutomationTriggerStepId {
|
||||||
WEBHOOK = "WEBHOOK",
|
WEBHOOK = "WEBHOOK",
|
||||||
APP = "APP",
|
APP = "APP",
|
||||||
CRON = "CRON",
|
CRON = "CRON",
|
||||||
|
ROW_ACTION = "ROW_ACTION",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AutomationStepType {
|
export enum AutomationStepType {
|
||||||
|
@ -152,6 +153,7 @@ interface BaseIOStructure {
|
||||||
[key: string]: BaseIOStructure
|
[key: string]: BaseIOStructure
|
||||||
}
|
}
|
||||||
required?: string[]
|
required?: string[]
|
||||||
|
readonly?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InputOutputBlock {
|
export interface InputOutputBlock {
|
||||||
|
@ -192,6 +194,7 @@ export interface AutomationStep extends AutomationStepSchema {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutomationTriggerSchema extends AutomationStepSchema {
|
export interface AutomationTriggerSchema extends AutomationStepSchema {
|
||||||
|
type: AutomationStepType.TRIGGER
|
||||||
event?: string
|
event?: string
|
||||||
cronJobId?: string
|
cronJobId?: string
|
||||||
}
|
}
|
||||||
|
@ -276,6 +279,7 @@ export enum AutomationEventType {
|
||||||
APP_TRIGGER = "app:trigger",
|
APP_TRIGGER = "app:trigger",
|
||||||
CRON_TRIGGER = "cron:trigger",
|
CRON_TRIGGER = "cron:trigger",
|
||||||
WEBHOOK_TRIGGER = "web:trigger",
|
WEBHOOK_TRIGGER = "web:trigger",
|
||||||
|
ROW_ACTION = "row:action",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdatedRowEventEmitter = {
|
export type UpdatedRowEventEmitter = {
|
||||||
|
|
|
@ -6,6 +6,7 @@ export interface TableRowActions extends Document {
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
name: string
|
name: string
|
||||||
|
automationId: string
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@ export interface PlatformUserByEmail extends Document {
|
||||||
*/
|
*/
|
||||||
export interface PlatformUserById extends Document {
|
export interface PlatformUserById extends Document {
|
||||||
tenantId: string
|
tenantId: string
|
||||||
|
email?: string
|
||||||
|
ssoId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,6 +24,7 @@ export interface PlatformUserBySsoId extends Document {
|
||||||
tenantId: string
|
tenantId: string
|
||||||
userId: string
|
userId: string
|
||||||
email: string
|
email: string
|
||||||
|
ssoId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlatformUser =
|
export type PlatformUser =
|
||||||
|
|
|
@ -137,6 +137,10 @@ export interface Database {
|
||||||
): Promise<T[]>
|
): Promise<T[]>
|
||||||
remove(idOrDoc: Document): Promise<Nano.DocumentDestroyResponse>
|
remove(idOrDoc: Document): Promise<Nano.DocumentDestroyResponse>
|
||||||
remove(idOrDoc: string, rev?: string): Promise<Nano.DocumentDestroyResponse>
|
remove(idOrDoc: string, rev?: string): Promise<Nano.DocumentDestroyResponse>
|
||||||
|
bulkRemove(
|
||||||
|
documents: Document[],
|
||||||
|
opts?: { silenceErrors?: boolean }
|
||||||
|
): Promise<void>
|
||||||
put(
|
put(
|
||||||
document: AnyDocument,
|
document: AnyDocument,
|
||||||
opts?: DatabasePutOpts
|
opts?: DatabasePutOpts
|
||||||
|
|
|
@ -62,7 +62,7 @@ export const addSsoSupport = async (ctx: Ctx<AddSSoUserRequest>) => {
|
||||||
const { email, ssoId } = ctx.request.body
|
const { email, ssoId } = ctx.request.body
|
||||||
try {
|
try {
|
||||||
// Status is changed to 404 from getUserDoc if user is not found
|
// Status is changed to 404 from getUserDoc if user is not found
|
||||||
let userByEmail = (await platform.users.getUserDoc(
|
const userByEmail = (await platform.users.getUserDoc(
|
||||||
email
|
email
|
||||||
)) as PlatformUserByEmail
|
)) as PlatformUserByEmail
|
||||||
await platform.users.addSsoUser(
|
await platform.users.addSsoUser(
|
||||||
|
@ -71,6 +71,13 @@ export const addSsoSupport = async (ctx: Ctx<AddSSoUserRequest>) => {
|
||||||
userByEmail.userId,
|
userByEmail.userId,
|
||||||
userByEmail.tenantId
|
userByEmail.tenantId
|
||||||
)
|
)
|
||||||
|
// Need to get the _rev of the user doc to update
|
||||||
|
const userById = await platform.users.getUserDoc(userByEmail.userId)
|
||||||
|
await platform.users.updateUserDoc({
|
||||||
|
...userById,
|
||||||
|
email,
|
||||||
|
ssoId,
|
||||||
|
})
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ctx.throw(err.status || 400, err)
|
ctx.throw(err.status || 400, err)
|
||||||
|
@ -268,7 +275,7 @@ export const find = async (ctx: any) => {
|
||||||
|
|
||||||
export const tenantUserLookup = async (ctx: any) => {
|
export const tenantUserLookup = async (ctx: any) => {
|
||||||
const id = ctx.params.id
|
const id = ctx.params.id
|
||||||
const user = await userSdk.core.getPlatformUser(id)
|
const user = await userSdk.core.getFirstPlatformUser(id)
|
||||||
if (user) {
|
if (user) {
|
||||||
ctx.body = user
|
ctx.body = user
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
if [ -d "packages/pro/src" ]; then
|
|
||||||
cd packages/pro
|
|
||||||
|
|
||||||
yarn
|
|
||||||
lerna bootstrap
|
|
||||||
fi
|
|
|
@ -51,20 +51,6 @@ async function runBuild(entry, outfile) {
|
||||||
fs.readFileSync(tsconfig, "utf-8")
|
fs.readFileSync(tsconfig, "utf-8")
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
|
||||||
!fs.existsSync(path.join(__dirname, "../packages/pro/src")) &&
|
|
||||||
tsconfigPathPluginContent.compilerOptions?.paths
|
|
||||||
) {
|
|
||||||
// If we don't have pro, we cannot bundle backend-core.
|
|
||||||
// Otherwise, the main context will not be shared between libraries
|
|
||||||
delete tsconfigPathPluginContent?.compilerOptions?.paths?.[
|
|
||||||
"@budibase/backend-core"
|
|
||||||
]
|
|
||||||
delete tsconfigPathPluginContent?.compilerOptions?.paths?.[
|
|
||||||
"@budibase/backend-core/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const sharedConfig = {
|
const sharedConfig = {
|
||||||
entryPoints: [entry],
|
entryPoints: [entry],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
|
@ -75,7 +61,7 @@ async function runBuild(entry, outfile) {
|
||||||
svelteCompilePlugin,
|
svelteCompilePlugin,
|
||||||
TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }),
|
TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }),
|
||||||
nodeExternalsPlugin({
|
nodeExternalsPlugin({
|
||||||
allowList: ["@budibase/frontend-core", "svelte"],
|
allowList: ["@budibase/frontend-core", "@budibase/pro", "svelte"],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
preserveSymlinks: true,
|
preserveSymlinks: true,
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Check if the pro submodule is loaded
|
|
||||||
if [ ! -d "./packages/pro/src" ]; then
|
|
||||||
echo "[ERROR] Submodule is not loaded. This is only allowed with loaded submodules."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
yarn build:apps
|
yarn build:apps
|
||||||
docker compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0
|
docker compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0
|
||||||
|
|
||||||
|
|
28
yarn.lock
28
yarn.lock
|
@ -8005,7 +8005,20 @@ caseless@~0.12.0:
|
||||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||||
integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
|
integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
|
||||||
|
|
||||||
chai@^4.3.10, chai@^4.3.7:
|
chai@^4.3.10:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8"
|
||||||
|
integrity sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==
|
||||||
|
dependencies:
|
||||||
|
assertion-error "^1.1.0"
|
||||||
|
check-error "^1.0.3"
|
||||||
|
deep-eql "^4.1.3"
|
||||||
|
get-func-name "^2.0.2"
|
||||||
|
loupe "^2.3.6"
|
||||||
|
pathval "^1.1.1"
|
||||||
|
type-detect "^4.1.0"
|
||||||
|
|
||||||
|
chai@^4.3.7:
|
||||||
version "4.4.1"
|
version "4.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1"
|
resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1"
|
||||||
integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==
|
integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==
|
||||||
|
@ -10319,10 +10332,10 @@ es6-promise@^4.2.4:
|
||||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
|
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
|
||||||
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
|
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
|
||||||
|
|
||||||
esbuild-node-externals@^1.8.0:
|
esbuild-node-externals@^1.14.0:
|
||||||
version "1.8.0"
|
version "1.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/esbuild-node-externals/-/esbuild-node-externals-1.8.0.tgz#878fbe458d4e58337753c2eacfd7200dc1077bd1"
|
resolved "https://registry.yarnpkg.com/esbuild-node-externals/-/esbuild-node-externals-1.14.0.tgz#fc2950c67a068dc2b538fd1381ad7d8e20a6f54d"
|
||||||
integrity sha512-pYslmT8Bl383UnfxzHQQRpCgBNIOwAzDaYheuIeI4CODxelsN/eQroVn5STDow5QOpRalMgWUR+R8LfSgUROcw==
|
integrity sha512-jMWnTlCII3cLEjR5+u0JRSTJuP+MgbjEHKfwSIAI41NgLQ0ZjfzjchlbEn0r7v2u5gCBMSEYvYlkO7GDG8gG3A==
|
||||||
dependencies:
|
dependencies:
|
||||||
find-up "^5.0.0"
|
find-up "^5.0.0"
|
||||||
tslib "^2.4.1"
|
tslib "^2.4.1"
|
||||||
|
@ -21204,6 +21217,11 @@ type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.8:
|
||||||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
|
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
|
||||||
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
|
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
|
||||||
|
|
||||||
|
type-detect@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c"
|
||||||
|
integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==
|
||||||
|
|
||||||
type-fest@^0.13.1:
|
type-fest@^0.13.1:
|
||||||
version "0.13.1"
|
version "0.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"
|
||||||
|
|
Loading…
Reference in New Issue