Merge branch 'master' into feature/filter-bindings
This commit is contained in:
commit
46995a115a
16 changed files with 185 additions and 52 deletions
46
packages/builder/assets/FreeTrial.svelte
Normal file
46
packages/builder/assets/FreeTrial.svelte
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 804 KiB |
|
@ -0,0 +1,64 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, ModalContent } from "@budibase/bbui"
|
||||||
|
import FreeTrial from "../../../../assets/FreeTrial.svelte"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { auth, licensing } from "stores/portal"
|
||||||
|
import { API } from "api"
|
||||||
|
import { PlanType } from "@budibase/types"
|
||||||
|
|
||||||
|
let freeTrialModal
|
||||||
|
|
||||||
|
$: planType = $licensing?.license?.plan?.type
|
||||||
|
$: showFreeTrialModal(planType, freeTrialModal)
|
||||||
|
|
||||||
|
const showFreeTrialModal = (planType, freeTrialModal) => {
|
||||||
|
if (
|
||||||
|
planType === PlanType.ENTERPRISE_BASIC_TRIAL &&
|
||||||
|
!$auth.user?.freeTrialConfirmedAt
|
||||||
|
) {
|
||||||
|
freeTrialModal?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={freeTrialModal} disableCancel={true}>
|
||||||
|
<ModalContent
|
||||||
|
confirmText="Get started"
|
||||||
|
size="M"
|
||||||
|
showCancelButton={false}
|
||||||
|
showCloseIcon={false}
|
||||||
|
onConfirm={async () => {
|
||||||
|
if (get(auth).user) {
|
||||||
|
try {
|
||||||
|
await API.updateSelf({
|
||||||
|
freeTrialConfirmedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
// Update the cached user
|
||||||
|
await auth.getSelf()
|
||||||
|
} finally {
|
||||||
|
freeTrialModal.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1>Experience all of Budibase with a free 14-day trial</h1>
|
||||||
|
<div class="free-trial-text">
|
||||||
|
We've upgraded you to a free 14-day trial that allows you to try all our
|
||||||
|
features before deciding which plan is right for you.
|
||||||
|
<p>
|
||||||
|
At the end of your trial, we'll automatically downgrade you to the Free
|
||||||
|
plan unless you choose to upgrade.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<FreeTrial />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
.free-trial-text {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -20,6 +20,9 @@ export function getFormattedPlanName(userPlanType) {
|
||||||
case PlanType.ENTERPRISE:
|
case PlanType.ENTERPRISE:
|
||||||
planName = "Enterprise"
|
planName = "Enterprise"
|
||||||
break
|
break
|
||||||
|
case PlanType.ENTERPRISE_BASIC_TRIAL:
|
||||||
|
planName = "Trial"
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
planName = "Free" // Default to "Free" if the type is not explicitly handled
|
planName = "Free" // Default to "Free" if the type is not explicitly handled
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
import { UserAvatars } from "@budibase/frontend-core"
|
import { UserAvatars } from "@budibase/frontend-core"
|
||||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
||||||
|
import EnterpriseBasicTrialModal from "components/portal/onboarding/EnterpriseBasicTrialModal.svelte"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
|
||||||
|
@ -192,6 +193,8 @@
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<EnterpriseBasicTrialModal />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.back-to-apps {
|
.back-to-apps {
|
||||||
display: contents;
|
display: contents;
|
||||||
|
|
|
@ -103,6 +103,8 @@ export const createLicensingStore = () => {
|
||||||
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
|
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
|
||||||
const isFreePlan = planType === Constants.PlanType.FREE
|
const isFreePlan = planType === Constants.PlanType.FREE
|
||||||
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
|
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
|
||||||
|
const isEnterpriseTrial =
|
||||||
|
planType === Constants.PlanType.ENTERPRISE_BASIC_TRIAL
|
||||||
const groupsEnabled = license.features.includes(
|
const groupsEnabled = license.features.includes(
|
||||||
Constants.Features.USER_GROUPS
|
Constants.Features.USER_GROUPS
|
||||||
)
|
)
|
||||||
|
@ -143,6 +145,7 @@ export const createLicensingStore = () => {
|
||||||
isEnterprisePlan,
|
isEnterprisePlan,
|
||||||
isFreePlan,
|
isFreePlan,
|
||||||
isBusinessPlan,
|
isBusinessPlan,
|
||||||
|
isEnterpriseTrial,
|
||||||
groupsEnabled,
|
groupsEnabled,
|
||||||
backupsEnabled,
|
backupsEnabled,
|
||||||
brandingEnabled,
|
brandingEnabled,
|
||||||
|
|
|
@ -4220,8 +4220,8 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"attachmentfield": {
|
"attachmentfield": {
|
||||||
"name": "Attachment list",
|
"name": "Attachment List",
|
||||||
"icon": "Attach",
|
"icon": "DocumentFragmentGroup",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
"requiredAncestors": ["form"],
|
"requiredAncestors": ["form"],
|
||||||
"editable": true,
|
"editable": true,
|
||||||
|
@ -4318,7 +4318,7 @@
|
||||||
},
|
},
|
||||||
"attachmentsinglefield": {
|
"attachmentsinglefield": {
|
||||||
"name": "Single Attachment",
|
"name": "Single Attachment",
|
||||||
"icon": "Attach",
|
"icon": "DocumentFragment",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
"requiredAncestors": ["form"],
|
"requiredAncestors": ["form"],
|
||||||
"editable": true,
|
"editable": true,
|
||||||
|
|
|
@ -57,6 +57,7 @@ export const PlanType = {
|
||||||
PRO: "pro",
|
PRO: "pro",
|
||||||
BUSINESS: "business",
|
BUSINESS: "business",
|
||||||
ENTERPRISE: "enterprise",
|
ENTERPRISE: "enterprise",
|
||||||
|
ENTERPRISE_BASIC_TRIAL: "enterprise_basic_trial",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -124,8 +125,8 @@ export const TypeIconMap = {
|
||||||
[FieldType.ARRAY]: "Duplicate",
|
[FieldType.ARRAY]: "Duplicate",
|
||||||
[FieldType.NUMBER]: "123",
|
[FieldType.NUMBER]: "123",
|
||||||
[FieldType.BOOLEAN]: "Boolean",
|
[FieldType.BOOLEAN]: "Boolean",
|
||||||
[FieldType.ATTACHMENTS]: "Attach",
|
[FieldType.ATTACHMENTS]: "DocumentFragmentGroup",
|
||||||
[FieldType.ATTACHMENT_SINGLE]: "Attach",
|
[FieldType.ATTACHMENT_SINGLE]: "DocumentFragment",
|
||||||
[FieldType.LINK]: "DataCorrelated",
|
[FieldType.LINK]: "DataCorrelated",
|
||||||
[FieldType.FORMULA]: "Calculator",
|
[FieldType.FORMULA]: "Calculator",
|
||||||
[FieldType.JSON]: "Brackets",
|
[FieldType.JSON]: "Brackets",
|
||||||
|
|
|
@ -1,20 +1,11 @@
|
||||||
import { getRowParams } from "../../../db/utils"
|
import { getRowParams } from "../../../db/utils"
|
||||||
import {
|
import {
|
||||||
outputProcessing,
|
outputProcessing,
|
||||||
processAutoColumn,
|
|
||||||
processFormulas,
|
processFormulas,
|
||||||
} from "../../../utilities/rowProcessor"
|
} from "../../../utilities/rowProcessor"
|
||||||
import { context, locks } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import {
|
import { Table, Row, FormulaType, FieldType } from "@budibase/types"
|
||||||
Table,
|
|
||||||
Row,
|
|
||||||
LockType,
|
|
||||||
LockName,
|
|
||||||
FormulaType,
|
|
||||||
FieldType,
|
|
||||||
} from "@budibase/types"
|
|
||||||
import * as linkRows from "../../../db/linkedRows"
|
import * as linkRows from "../../../db/linkedRows"
|
||||||
import sdk from "../../../sdk"
|
|
||||||
import isEqual from "lodash/isEqual"
|
import isEqual from "lodash/isEqual"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
@ -151,30 +142,7 @@ export async function finaliseRow(
|
||||||
// if another row has been written since processing this will
|
// if another row has been written since processing this will
|
||||||
// handle the auto ID clash
|
// handle the auto ID clash
|
||||||
if (oldTable && !isEqual(oldTable, table)) {
|
if (oldTable && !isEqual(oldTable, table)) {
|
||||||
try {
|
await db.put(table)
|
||||||
await db.put(table)
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err.status === 409) {
|
|
||||||
// Some conflicts with the autocolumns occurred, we need to refetch the table and recalculate
|
|
||||||
await locks.doWithLock(
|
|
||||||
{
|
|
||||||
type: LockType.AUTO_EXTEND,
|
|
||||||
name: LockName.PROCESS_AUTO_COLUMNS,
|
|
||||||
resource: table._id,
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
const latestTable = await sdk.tables.getTable(table._id!)
|
|
||||||
let response = processAutoColumn(null, latestTable, row, {
|
|
||||||
reprocessing: true,
|
|
||||||
})
|
|
||||||
await db.put(response.table)
|
|
||||||
row = response.row
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const response = await db.put(row)
|
const response = await db.put(row)
|
||||||
// for response, calculate the formulas for the enriched row
|
// for response, calculate the formulas for the enriched row
|
||||||
|
|
|
@ -145,6 +145,7 @@ describe("sdk >> rows >> internal", () => {
|
||||||
lastID: 1,
|
lastID: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
_rev: expect.stringMatching("2-.*"),
|
||||||
},
|
},
|
||||||
row: {
|
row: {
|
||||||
...row,
|
...row,
|
||||||
|
@ -189,7 +190,6 @@ describe("sdk >> rows >> internal", () => {
|
||||||
type: FieldType.AUTO,
|
type: FieldType.AUTO,
|
||||||
subtype: AutoFieldSubType.AUTO_ID,
|
subtype: AutoFieldSubType.AUTO_ID,
|
||||||
autocolumn: true,
|
autocolumn: true,
|
||||||
lastID: 0,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -199,7 +199,7 @@ describe("sdk >> rows >> internal", () => {
|
||||||
await internalSdk.save(table._id!, row, config.getUser()._id)
|
await internalSdk.save(table._id!, row, config.getUser()._id)
|
||||||
}
|
}
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
makeRows(10).map(row =>
|
makeRows(20).map(row =>
|
||||||
internalSdk.save(table._id!, row, config.getUser()._id)
|
internalSdk.save(table._id!, row, config.getUser()._id)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -209,19 +209,21 @@ describe("sdk >> rows >> internal", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const persistedRows = await config.getRows(table._id!)
|
const persistedRows = await config.getRows(table._id!)
|
||||||
expect(persistedRows).toHaveLength(20)
|
expect(persistedRows).toHaveLength(30)
|
||||||
expect(persistedRows).toEqual(
|
expect(persistedRows).toEqual(
|
||||||
expect.arrayContaining(
|
expect.arrayContaining(
|
||||||
Array.from({ length: 20 }).map((_, i) =>
|
Array.from({ length: 30 }).map((_, i) =>
|
||||||
expect.objectContaining({ id: i + 1 })
|
expect.objectContaining({ id: i + 1 })
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const persistedTable = await config.getTable(table._id)
|
const persistedTable = await config.getTable(table._id)
|
||||||
expect((table.schema.id as AutoColumnFieldMetadata).lastID).toBe(0)
|
expect(
|
||||||
|
(table.schema.id as AutoColumnFieldMetadata).lastID
|
||||||
|
).toBeUndefined()
|
||||||
expect((persistedTable.schema.id as AutoColumnFieldMetadata).lastID).toBe(
|
expect((persistedTable.schema.id as AutoColumnFieldMetadata).lastID).toBe(
|
||||||
20
|
30
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as linkRows from "../../db/linkedRows"
|
import * as linkRows from "../../db/linkedRows"
|
||||||
import { processFormulas, fixAutoColumnSubType } from "./utils"
|
import { processFormulas, fixAutoColumnSubType } from "./utils"
|
||||||
import { objectStore, utils } from "@budibase/backend-core"
|
import { context, objectStore, utils } from "@budibase/backend-core"
|
||||||
import { InternalTables } from "../../db/utils"
|
import { InternalTables } from "../../db/utils"
|
||||||
import { TYPE_TRANSFORM_MAP } from "./map"
|
import { TYPE_TRANSFORM_MAP } from "./map"
|
||||||
import {
|
import {
|
||||||
|
@ -25,7 +25,44 @@ type AutoColumnProcessingOpts = {
|
||||||
noAutoRelationships?: boolean
|
noAutoRelationships?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const BASE_AUTO_ID = 1
|
// Returns the next auto ID for a column in a table. On success, the table will
|
||||||
|
// be updated which is why it gets returned. The nextID returned is guaranteed
|
||||||
|
// to be given only to you, and if you don't use it it's gone forever (a gap
|
||||||
|
// will be left in the auto ID sequence).
|
||||||
|
//
|
||||||
|
// This function can throw if it fails to generate an auto ID after so many
|
||||||
|
// attempts.
|
||||||
|
async function getNextAutoId(
|
||||||
|
table: Table,
|
||||||
|
column: string
|
||||||
|
): Promise<{ table: Table; nextID: number }> {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
for (let attempt = 0; attempt < 5; attempt++) {
|
||||||
|
const schema = table.schema[column]
|
||||||
|
if (schema.type !== FieldType.NUMBER && schema.type !== FieldType.AUTO) {
|
||||||
|
throw new Error(`Column ${column} is not an auto column`)
|
||||||
|
}
|
||||||
|
schema.lastID = (schema.lastID || 0) + 1
|
||||||
|
try {
|
||||||
|
const resp = await db.put(table)
|
||||||
|
table._rev = resp.rev
|
||||||
|
return { table, nextID: schema.lastID }
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.status !== 409) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
// We wait for a random amount of time before retrying. The randomness
|
||||||
|
// makes it less likely for multiple requests modifying this table to
|
||||||
|
// collide.
|
||||||
|
await new Promise(resolve =>
|
||||||
|
setTimeout(resolve, Math.random() * 1.2 ** attempt * 1000)
|
||||||
|
)
|
||||||
|
table = await db.get(table._id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Failed to generate an auto ID")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will update any auto columns that are found on the row/table with the correct information based on
|
* This will update any auto columns that are found on the row/table with the correct information based on
|
||||||
|
@ -37,7 +74,7 @@ const BASE_AUTO_ID = 1
|
||||||
* @returns The updated row and table, the table may need to be updated
|
* @returns The updated row and table, the table may need to be updated
|
||||||
* for automatic ID purposes.
|
* for automatic ID purposes.
|
||||||
*/
|
*/
|
||||||
export function processAutoColumn(
|
export async function processAutoColumn(
|
||||||
userId: string | null | undefined,
|
userId: string | null | undefined,
|
||||||
table: Table,
|
table: Table,
|
||||||
row: Row,
|
row: Row,
|
||||||
|
@ -79,8 +116,9 @@ export function processAutoColumn(
|
||||||
break
|
break
|
||||||
case AutoFieldSubType.AUTO_ID:
|
case AutoFieldSubType.AUTO_ID:
|
||||||
if (creating) {
|
if (creating) {
|
||||||
schema.lastID = !schema.lastID ? BASE_AUTO_ID : schema.lastID + 1
|
const { table: newTable, nextID } = await getNextAutoId(table, key)
|
||||||
row[key] = schema.lastID
|
table = newTable
|
||||||
|
row[key] = nextID
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ export interface UpdateSelfRequest {
|
||||||
password?: string
|
password?: string
|
||||||
forceResetPassword?: boolean
|
forceResetPassword?: boolean
|
||||||
onboardedAt?: string
|
onboardedAt?: string
|
||||||
|
freeTrialConfirmedAt?: string
|
||||||
appFavourites?: string[]
|
appFavourites?: string[]
|
||||||
tours?: Record<string, Date>
|
tours?: Record<string, Date>
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@ export interface User extends Document {
|
||||||
dayPassRecordedAt?: string
|
dayPassRecordedAt?: string
|
||||||
userGroups?: string[]
|
userGroups?: string[]
|
||||||
onboardedAt?: string
|
onboardedAt?: string
|
||||||
|
freeTrialConfirmedAt?: string
|
||||||
tours?: Record<string, Date>
|
tours?: Record<string, Date>
|
||||||
scimInfo?: { isSync: true } & Record<string, any>
|
scimInfo?: { isSync: true } & Record<string, any>
|
||||||
appFavourites?: string[]
|
appFavourites?: string[]
|
||||||
|
|
|
@ -21,7 +21,6 @@ export enum LockName {
|
||||||
PERSIST_WRITETHROUGH = "persist_writethrough",
|
PERSIST_WRITETHROUGH = "persist_writethrough",
|
||||||
QUOTA_USAGE_EVENT = "quota_usage_event",
|
QUOTA_USAGE_EVENT = "quota_usage_event",
|
||||||
APP_MIGRATION = "app_migrations",
|
APP_MIGRATION = "app_migrations",
|
||||||
PROCESS_AUTO_COLUMNS = "process_auto_columns",
|
|
||||||
PROCESS_USER_INVITE = "process_user_invite",
|
PROCESS_USER_INVITE = "process_user_invite",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ describe("/api/global/self", () => {
|
||||||
const res = await config.api.self
|
const res = await config.api.self
|
||||||
.updateSelf(user, {
|
.updateSelf(user, {
|
||||||
onboardedAt: "2023-03-07T14:10:54.869Z",
|
onboardedAt: "2023-03-07T14:10:54.869Z",
|
||||||
|
freeTrialConfirmedAt: "2024-03-17T14:10:54.869Z",
|
||||||
})
|
})
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
|
@ -63,6 +64,7 @@ describe("/api/global/self", () => {
|
||||||
user._rev = dbUser._rev
|
user._rev = dbUser._rev
|
||||||
user.dayPassRecordedAt = mocks.date.MOCK_DATE.toISOString()
|
user.dayPassRecordedAt = mocks.date.MOCK_DATE.toISOString()
|
||||||
expect(dbUser.onboardedAt).toBe("2023-03-07T14:10:54.869Z")
|
expect(dbUser.onboardedAt).toBe("2023-03-07T14:10:54.869Z")
|
||||||
|
expect(dbUser.freeTrialConfirmedAt).toBe("2024-03-17T14:10:54.869Z")
|
||||||
expect(res.body._id).toBe(user._id)
|
expect(res.body._id).toBe(user._id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -26,6 +26,7 @@ export const buildSelfSaveValidation = () => {
|
||||||
firstName: OPTIONAL_STRING,
|
firstName: OPTIONAL_STRING,
|
||||||
lastName: OPTIONAL_STRING,
|
lastName: OPTIONAL_STRING,
|
||||||
onboardedAt: Joi.string().optional(),
|
onboardedAt: Joi.string().optional(),
|
||||||
|
freeTrialConfirmedAt: Joi.string().optional(),
|
||||||
appFavourites: Joi.array().optional(),
|
appFavourites: Joi.array().optional(),
|
||||||
tours: Joi.object().optional(),
|
tours: Joi.object().optional(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ cd src/main/resources/models
|
||||||
echo "deploy processes..."
|
echo "deploy processes..."
|
||||||
zbctl deploy resource offboarding.bpmn --insecure
|
zbctl deploy resource offboarding.bpmn --insecure
|
||||||
zbctl deploy resource onboarding.bpmn --insecure
|
zbctl deploy resource onboarding.bpmn --insecure
|
||||||
|
zbctl deploy resource free_trial.bpmn --insecure
|
||||||
|
|
||||||
cd ../../../../../budibase/packages/account-portal/packages/server
|
cd ../../../../../budibase/packages/account-portal/packages/server
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue