diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js
index 23704556ad..5181e756c6 100644
--- a/packages/builder/src/builderStore/index.js
+++ b/packages/builder/src/builderStore/index.js
@@ -1,6 +1,5 @@
import { getFrontendStore } from "./store/frontend"
import { getAutomationStore } from "./store/automation"
-import { getHostingStore } from "./store/hosting"
import { getThemeStore } from "./store/theme"
import { derived, writable } from "svelte/store"
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
@@ -9,7 +8,6 @@ import { findComponent } from "./componentUtils"
export const store = getFrontendStore()
export const automationStore = getAutomationStore()
export const themeStore = getThemeStore()
-export const hostingStore = getHostingStore()
export const currentAsset = derived(store, $store => {
const type = $store.currentFrontEndType
diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js
index fdfe450edf..0d740e08e0 100644
--- a/packages/builder/src/builderStore/store/frontend.js
+++ b/packages/builder/src/builderStore/store/frontend.js
@@ -2,7 +2,6 @@ import { get, writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import {
allScreens,
- hostingStore,
currentAsset,
mainLayout,
selectedComponent,
@@ -100,7 +99,6 @@ export const getFrontendStore = () => {
version: application.version,
revertableVersion: application.revertableVersion,
}))
- await hostingStore.actions.fetch()
// Initialise backend stores
const [_integrations] = await Promise.all([
diff --git a/packages/builder/src/builderStore/store/hosting.js b/packages/builder/src/builderStore/store/hosting.js
deleted file mode 100644
index fb174c2663..0000000000
--- a/packages/builder/src/builderStore/store/hosting.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { writable } from "svelte/store"
-import api, { get } from "../api"
-
-const INITIAL_HOSTING_UI_STATE = {
- appUrl: "",
- deployedApps: {},
- deployedAppNames: [],
- deployedAppUrls: [],
-}
-
-export const getHostingStore = () => {
- const store = writable({ ...INITIAL_HOSTING_UI_STATE })
- store.actions = {
- fetch: async () => {
- const response = await api.get("/api/hosting/urls")
- const urls = await response.json()
- store.update(state => {
- state.appUrl = urls.app
- return state
- })
- },
- fetchDeployedApps: async () => {
- let deployments = await (await get("/api/hosting/apps")).json()
- store.update(state => {
- state.deployedApps = deployments
- state.deployedAppNames = Object.values(deployments).map(app => app.name)
- state.deployedAppUrls = Object.values(deployments).map(app => app.url)
- return state
- })
- return deployments
- },
- }
- return store
-}
diff --git a/packages/builder/src/components/deploy/DeploymentHistory.svelte b/packages/builder/src/components/deploy/DeploymentHistory.svelte
index f6bbcef4d4..36c2433c27 100644
--- a/packages/builder/src/components/deploy/DeploymentHistory.svelte
+++ b/packages/builder/src/components/deploy/DeploymentHistory.svelte
@@ -6,7 +6,7 @@
import api from "builderStore/api"
import { notifications } from "@budibase/bbui"
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
- import { store, hostingStore } from "builderStore"
+ import { store } from "builderStore"
const DeploymentStatus = {
SUCCESS: "SUCCESS",
@@ -37,7 +37,7 @@
let poll
let deployments = []
let urlComponent = $store.url || `/${appId}`
- let deploymentUrl = `${$hostingStore.appUrl}${urlComponent}`
+ let deploymentUrl = `${urlComponent}`
const formatDate = (date, format) =>
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)
diff --git a/packages/builder/src/components/start/CreateAppModal.svelte b/packages/builder/src/components/start/CreateAppModal.svelte
index 60065b6eef..3efd0231aa 100644
--- a/packages/builder/src/components/start/CreateAppModal.svelte
+++ b/packages/builder/src/components/start/CreateAppModal.svelte
@@ -1,100 +1,46 @@
{#if template?.fromFile}
{
$values.file = e.detail?.[0]
- $touched.file = true
+ $validation.touched.file = true
}}
/>
{/if}
($touched.name = true)}
+ error={$validation.touched.name && $validation.errors.name}
+ on:blur={() => ($validation.touched.name = true)}
label="Name"
placeholder={$auth.user.firstName
- ? `${$auth.user.firstName}'s app`
+ ? `${$auth.user.firstName}s app`
: "My app"}
/>
+ ($validation.touched.url = true)}
+ label="URL"
+ placeholder={$values.name
+ ? "/" + encodeURIComponent($values.name).toLowerCase()
+ : "/"}
+ />
diff --git a/packages/builder/src/components/start/UpdateAppModal.svelte b/packages/builder/src/components/start/UpdateAppModal.svelte
index 432b13c7c3..7549876fc0 100644
--- a/packages/builder/src/components/start/UpdateAppModal.svelte
+++ b/packages/builder/src/components/start/UpdateAppModal.svelte
@@ -1,120 +1,75 @@
-
-
- Update the name of your app.
- ($touched.name = true)}
- on:change={() => (dirty = true)}
- label="Name"
- />
-
-
+
+ Update the name of your app.
+ ($validation.touched.name = true)}
+ label="Name"
+ />
+ ($validation.touched.url = true)}
+ label="URL"
+ placeholder={$values.name
+ ? "/" + encodeURIComponent($values.name).toLowerCase()
+ : "/"}
+ />
+
diff --git a/packages/builder/src/constants/index.js b/packages/builder/src/constants/index.js
index 04f12672e8..6b7aafdfa9 100644
--- a/packages/builder/src/constants/index.js
+++ b/packages/builder/src/constants/index.js
@@ -36,4 +36,7 @@ export const LAYOUT_NAMES = {
export const BUDIBASE_INTERNAL_DB = "bb_internal"
+// one or more word characters and whitespace
export const APP_NAME_REGEX = /^[\w\s]+$/
+// zero or more non-whitespace characters
+export const APP_URL_REGEX = /^\S*$/
diff --git a/packages/builder/src/helpers/validation/validation.js b/packages/builder/src/helpers/validation/validation.js
index 8d80d720a1..db5dfe4430 100644
--- a/packages/builder/src/helpers/validation/validation.js
+++ b/packages/builder/src/helpers/validation/validation.js
@@ -1,5 +1,7 @@
import { writable, derived } from "svelte/store"
+// DEPRECATED - Use the yup based validators for future validation
+
export function createValidationStore(initialValue, ...validators) {
let touched = false
diff --git a/packages/builder/src/helpers/validation/validators.js b/packages/builder/src/helpers/validation/validators.js
index 036487fd50..f842f11313 100644
--- a/packages/builder/src/helpers/validation/validators.js
+++ b/packages/builder/src/helpers/validation/validators.js
@@ -1,3 +1,5 @@
+// TODO: Convert to yup based validators
+
export function emailValidator(value) {
return (
(value &&
diff --git a/packages/builder/src/helpers/validation/yup/app.js b/packages/builder/src/helpers/validation/yup/app.js
new file mode 100644
index 0000000000..de0f86446c
--- /dev/null
+++ b/packages/builder/src/helpers/validation/yup/app.js
@@ -0,0 +1,83 @@
+import { string, mixed } from "yup"
+import { APP_NAME_REGEX, APP_URL_REGEX } from "constants"
+
+export const name = (validation, { apps, currentApp } = { apps: [] }) => {
+ validation.addValidator(
+ "name",
+ string()
+ .trim()
+ .required("Your application must have a name")
+ .matches(
+ APP_NAME_REGEX,
+ "App name must be letters, numbers and spaces only"
+ )
+ .test(
+ "non-existing-app-name",
+ "Another app with the same name already exists",
+ value => {
+ if (!value) {
+ // exit early, above validator will fail
+ return true
+ }
+ if (currentApp) {
+ // filter out the current app if present
+ apps = apps.filter(app => app.appId !== currentApp.appId)
+ }
+ return !apps
+ .map(app => app.name)
+ .some(appName => appName.toLowerCase() === value.toLowerCase())
+ }
+ )
+ )
+}
+
+export const url = (validation, { apps, currentApp } = { apps: [] }) => {
+ validation.addValidator(
+ "url",
+ string()
+ .nullable()
+ .matches(APP_URL_REGEX, "App URL must not contain spaces")
+ .test(
+ "non-existing-app-url",
+ "Another app with the same URL already exists",
+ value => {
+ // url is nullable
+ if (!value) {
+ return true
+ }
+ if (currentApp) {
+ // filter out the current app if present
+ apps = apps.filter(app => app.appId !== currentApp.appId)
+ }
+ return !apps
+ .map(app => app.url)
+ .some(appUrl => appUrl?.toLowerCase() === value.toLowerCase())
+ }
+ )
+ .test("valid-url", "Not a valid URL", value => {
+ // url is nullable
+ if (!value) {
+ return true
+ }
+ // make it clear that this is a url path and cannot be a full url
+ return (
+ value.startsWith("/") &&
+ !value.includes("http") &&
+ !value.includes("www") &&
+ !value.includes(".") &&
+ value.length > 1 // just '/' is not valid
+ )
+ })
+ )
+}
+
+export const file = (validation, { template } = {}) => {
+ const templateToUse =
+ template && Object.keys(template).length === 0 ? null : template
+ validation.addValidator(
+ "file",
+ templateToUse?.fromFile
+ ? mixed().required("Please choose a file to import")
+ : null
+ )
+}
diff --git a/packages/builder/src/helpers/validation/yup/index.js b/packages/builder/src/helpers/validation/yup/index.js
new file mode 100644
index 0000000000..6783ad7e58
--- /dev/null
+++ b/packages/builder/src/helpers/validation/yup/index.js
@@ -0,0 +1,66 @@
+import { capitalise } from "helpers"
+import { object } from "yup"
+import { writable, get } from "svelte/store"
+import { notifications } from "@budibase/bbui"
+
+export const createValidationStore = () => {
+ const DEFAULT = {
+ errors: {},
+ touched: {},
+ valid: false,
+ }
+
+ const validator = {}
+ const validation = writable(DEFAULT)
+
+ const addValidator = (propertyName, propertyValidator) => {
+ if (!propertyValidator || !propertyName) {
+ return
+ }
+ validator[propertyName] = propertyValidator
+ }
+
+ const check = async values => {
+ const obj = object().shape(validator)
+ // clear the previous errors
+ const properties = Object.keys(validator)
+ properties.forEach(property => (get(validation).errors[property] = null))
+
+ let validationError = false
+ try {
+ await obj.validate(values, { abortEarly: false })
+ } catch (error) {
+ if (!error.inner) {
+ notifications.error("Unexpected validation error", error)
+ validationError = true
+ } else {
+ error.inner.forEach(err => {
+ validation.update(store => {
+ store.errors[err.path] = capitalise(err.message)
+ return store
+ })
+ })
+ }
+ }
+
+ let valid
+ if (properties.length && !validationError) {
+ valid = await obj.isValid(values)
+ } else {
+ // don't say valid until validators have been loaded
+ valid = false
+ }
+
+ validation.update(store => {
+ store.valid = valid
+ return store
+ })
+ }
+
+ return {
+ subscribe: validation.subscribe,
+ set: validation.set,
+ check,
+ addValidator,
+ }
+}
diff --git a/packages/builder/src/pages/builder/apps/index.svelte b/packages/builder/src/pages/builder/apps/index.svelte
index aafc28cd92..c98e749e45 100644
--- a/packages/builder/src/pages/builder/apps/index.svelte
+++ b/packages/builder/src/pages/builder/apps/index.svelte
@@ -12,7 +12,7 @@
Modal,
} from "@budibase/bbui"
import { onMount } from "svelte"
- import { apps, organisation, auth, admin } from "stores/portal"
+ import { apps, organisation, auth } from "stores/portal"
import { goto } from "@roxi/routify"
import { AppStatus } from "constants"
import { gradient } from "actions"
@@ -34,7 +34,6 @@
const publishedAppsOnly = app => app.status === AppStatus.DEPLOYED
$: publishedApps = $apps.filter(publishedAppsOnly)
- $: isCloud = $admin.cloud
$: userApps = $auth.user?.builder?.global
? publishedApps
: publishedApps.filter(app =>
@@ -42,7 +41,11 @@
)
function getUrl(app) {
- return !isCloud ? `/app/${encodeURIComponent(app.name)}` : `/${app.prodId}`
+ if (app.url) {
+ return `/app${app.url}`
+ } else {
+ return `/${app.prodId}`
+ }
}
diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte
index 047c60e979..faa57e5df3 100644
--- a/packages/builder/src/pages/builder/portal/apps/index.svelte
+++ b/packages/builder/src/pages/builder/portal/apps/index.svelte
@@ -49,7 +49,6 @@
$: filteredApps = enrichedApps.filter(app =>
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
)
- $: isCloud = $admin.cloud
const enrichApps = (apps, user, sortBy) => {
const enrichedApps = apps.map(app => ({
@@ -80,7 +79,7 @@
}
const initiateAppCreation = () => {
- template = {}
+ template = null
creationModal.show()
creatingApp = true
}
@@ -162,12 +161,10 @@
}
const viewApp = app => {
- if (!isCloud && app.deployed) {
- // special case to use the short form name if self hosted
- window.open(`/app/${encodeURIComponent(app.name)}`)
+ if (app.url) {
+ window.open(`/app${app.url}`)
} else {
- const id = app.deployed ? app.prodId : app.devId
- window.open(`/${id}`, "_blank")
+ window.open(`/${app.prodId}`)
}
}
@@ -442,6 +439,11 @@
>
+
+
+
+
+
{selectedApp?.name}?
-