Merge pull request #4223 from Budibase/frontend-core

Frontend core
This commit is contained in:
Andrew Kingston 2022-02-09 15:05:53 +00:00 committed by GitHub
commit 982ae39907
231 changed files with 4148 additions and 3306 deletions

View File

@ -8,7 +8,7 @@ const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions") const { createASession } = require("../../security/sessions")
const { getTenantId } = require("../../tenancy") const { getTenantId } = require("../../tenancy")
const INVALID_ERR = "Invalid Credentials" const INVALID_ERR = "Invalid credentials"
const SSO_NO_PASSWORD = "SSO user does not have a password set" const SSO_NO_PASSWORD = "SSO user does not have a password set"
const EXPIRED = "This account has expired. Please reset your password" const EXPIRED = "This account has expired. Please reset your password"

View File

@ -5,7 +5,7 @@
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
import Icon from "../Icon/Icon.svelte" import Icon from "../Icon/Icon.svelte"
import Input from "../Form/Input.svelte" import Input from "../Form/Input.svelte"
import { capitalise } from "../utils/helpers" import { capitalise } from "../helpers"
export let value export let value
export let size = "M" export let size = "M"

View File

@ -5,7 +5,7 @@
import "@spectrum-css/textfield/dist/index-vars.css" import "@spectrum-css/textfield/dist/index-vars.css"
import "@spectrum-css/picker/dist/index-vars.css" import "@spectrum-css/picker/dist/index-vars.css"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { generateID } from "../../utils/helpers" import { uuid } from "../../helpers"
export let id = null export let id = null
export let disabled = false export let disabled = false
@ -17,7 +17,7 @@
export let timeOnly = false export let timeOnly = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const flatpickrId = `${generateID()}-wrapper` const flatpickrId = `${uuid()}-wrapper`
let open = false let open = false
let flatpickr, flatpickrOptions, isTimeOnly let flatpickr, flatpickrOptions, isTimeOnly

View File

@ -3,7 +3,7 @@
import "@spectrum-css/typography/dist/index-vars.css" import "@spectrum-css/typography/dist/index-vars.css"
import "@spectrum-css/illustratedmessage/dist/index-vars.css" import "@spectrum-css/illustratedmessage/dist/index-vars.css"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { generateID } from "../../utils/helpers" import { uuid } from "../../helpers"
import Icon from "../../Icon/Icon.svelte" import Icon from "../../Icon/Icon.svelte"
import Link from "../../Link/Link.svelte" import Link from "../../Link/Link.svelte"
import Tag from "../../Tags/Tag.svelte" import Tag from "../../Tags/Tag.svelte"
@ -37,7 +37,7 @@
"jfif", "jfif",
] ]
const fieldId = id || generateID() const fieldId = id || uuid()
let selectedImageIdx = 0 let selectedImageIdx = 0
let fileDragged = false let fileDragged = false
let selectedUrl let selectedUrl

View File

@ -4,7 +4,7 @@
import CellRenderer from "./CellRenderer.svelte" import CellRenderer from "./CellRenderer.svelte"
import SelectEditRenderer from "./SelectEditRenderer.svelte" import SelectEditRenderer from "./SelectEditRenderer.svelte"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import { deepGet } from "../utils/helpers" import { deepGet } from "../helpers"
/** /**
* The expected schema is our normal couch schemas for our tables. * The expected schema is our normal couch schemas for our tables.

View File

@ -1,11 +1,45 @@
export const generateID = () => { /**
const rand = Math.random().toString(32).substring(2) * Generates a DOM safe UUID.
* Starting with a letter is important to make it DOM safe.
// Starts with a letter so that its a valid DOM ID * @return {string} a random DOM safe UUID
return `A${rand}` */
export function uuid() {
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0
const v = c === "x" ? r : (r & 0x3) | 0x8
return v.toString(16)
})
} }
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) /**
* Capitalises a string
* @param string the string to capitalise
* @return {string} the capitalised string
*/
export const capitalise = string => {
if (!string) {
return string
}
return string.substring(0, 1).toUpperCase() + string.substring(1)
}
/**
* Computes a short hash of a string
* @param string the string to compute a hash of
* @return {string} the hash string
*/
export const hashString = string => {
if (!string) {
return "0"
}
let hash = 0
for (let i = 0; i < string.length; i++) {
let char = string.charCodeAt(i)
hash = (hash << 5) - hash + char
hash = hash & hash // Convert to 32bit integer
}
return hash.toString()
}
/** /**
* Gets a key within an object. The key supports dot syntax for retrieving deep * Gets a key within an object. The key supports dot syntax for retrieving deep

View File

@ -85,5 +85,5 @@ export { default as clickOutside } from "./Actions/click_outside"
// Stores // Stores
export { notifications, createNotificationStore } from "./Stores/notifications" export { notifications, createNotificationStore } from "./Stores/notifications"
// Utils // Helpers
export * from "./utils/helpers" export * as Helpers from "./helpers"

View File

@ -6,8 +6,6 @@
"scripts": { "scripts": {
"build": "routify -b && vite build --emptyOutDir", "build": "routify -b && vite build --emptyOutDir",
"start": "routify -c rollup", "start": "routify -c rollup",
"test": "jest",
"test:watch": "jest --watchAll",
"dev:builder": "routify -c dev:vite", "dev:builder": "routify -c dev:vite",
"dev:vite": "vite --host 0.0.0.0", "dev:vite": "vite --host 0.0.0.0",
"rollup": "rollup -c -w", "rollup": "rollup -c -w",
@ -68,7 +66,7 @@
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.50-alpha.5", "@budibase/bbui": "^1.0.50-alpha.5",
"@budibase/client": "^1.0.50-alpha.5", "@budibase/client": "^1.0.50-alpha.5",
"@budibase/colorpicker": "1.1.2", "@budibase/frontend-core": "^1.0.50-alpha.5",
"@budibase/string-templates": "^1.0.50-alpha.5", "@budibase/string-templates": "^1.0.50-alpha.5",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",

View File

@ -1,4 +1,4 @@
import api from "builderStore/api" import { API } from "api"
import PosthogClient from "./PosthogClient" import PosthogClient from "./PosthogClient"
import IntercomClient from "./IntercomClient" import IntercomClient from "./IntercomClient"
import SentryClient from "./SentryClient" import SentryClient from "./SentryClient"
@ -17,14 +17,12 @@ class AnalyticsHub {
} }
async activate() { async activate() {
const analyticsStatus = await api.get("/api/analytics") // Check analytics are enabled
const json = await analyticsStatus.json() const analyticsStatus = await API.getAnalyticsStatus()
if (analyticsStatus.enabled) {
// Analytics disabled
if (!json.enabled) return
this.clients.forEach(client => client.init()) this.clients.forEach(client => client.init())
} }
}
identify(id, metadata) { identify(id, metadata) {
posthog.identify(id) posthog.identify(id)

View File

@ -0,0 +1,48 @@
import {
createAPIClient,
CookieUtils,
Constants,
} from "@budibase/frontend-core"
import { store } from "./builderStore"
import { get } from "svelte/store"
import { auth } from "./stores/portal"
export const API = createAPIClient({
attachHeaders: headers => {
// Attach app ID header from store
headers["x-budibase-app-id"] = get(store).appId
// Add csrf token if authenticated
const user = get(auth).user
if (user?.csrfToken) {
headers["x-csrf-token"] = user.csrfToken
}
},
onError: error => {
const { url, message, status, method, handled } = error || {}
// Log all API errors to Sentry
// analytics.captureException(error)
// Log any errors that we haven't manually handled
if (!handled) {
console.error("Unhandled error from API client", error)
return
}
// Log all errors to console
console.warn(`[Builder] HTTP ${status} on ${method}:${url}\n\t${message}`)
// Logout on 403's
if (status === 403) {
// Remove cookies
CookieUtils.removeCookie(Constants.Cookies.Auth)
// Reload after removing cookie, go to login
if (!url.includes("self") && !url.includes("login")) {
location.reload()
}
}
},
})

View File

@ -1,49 +0,0 @@
import { store } from "./index"
import { get as svelteGet } from "svelte/store"
import { removeCookie, Cookies } from "./cookies"
import { auth } from "stores/portal"
const apiCall =
method =>
async (url, body, headers = { "Content-Type": "application/json" }) => {
headers["x-budibase-app-id"] = svelteGet(store).appId
headers["x-budibase-api-version"] = "1"
// add csrf token if authenticated
const user = svelteGet(auth).user
if (user && user.csrfToken) {
headers["x-csrf-token"] = user.csrfToken
}
const json = headers["Content-Type"] === "application/json"
const resp = await fetch(url, {
method: method,
body: json ? JSON.stringify(body) : body,
headers,
})
if (resp.status === 403) {
if (url.includes("/api/templates")) {
return { json: () => [] }
}
removeCookie(Cookies.Auth)
// reload after removing cookie, go to login
if (!url.includes("self") && !url.includes("login")) {
location.reload()
}
}
return resp
}
export const post = apiCall("POST")
export const get = apiCall("GET")
export const patch = apiCall("PATCH")
export const del = apiCall("DELETE")
export const put = apiCall("PUT")
export default {
post: apiCall("POST"),
get: apiCall("GET"),
patch: apiCall("PATCH"),
delete: apiCall("DELETE"),
put: apiCall("PUT"),
}

View File

@ -15,10 +15,7 @@ import {
encodeJSBinding, encodeJSBinding,
} from "@budibase/string-templates" } from "@budibase/string-templates"
import { TableNames } from "../constants" import { TableNames } from "../constants"
import { import { JSONUtils } from "@budibase/frontend-core"
convertJSONSchemaToTableSchema,
getJSONArrayDatasourceSchema,
} from "./jsonUtils"
import ActionDefinitions from "components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json" import ActionDefinitions from "components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json"
// Regex to match all instances of template strings // Regex to match all instances of template strings
@ -439,7 +436,7 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
else if (type === "jsonarray") { else if (type === "jsonarray") {
table = tables.find(table => table._id === datasource.tableId) table = tables.find(table => table._id === datasource.tableId)
let tableSchema = table?.schema let tableSchema = table?.schema
schema = getJSONArrayDatasourceSchema(tableSchema, datasource) schema = JSONUtils.getJSONArrayDatasourceSchema(tableSchema, datasource)
} }
// Otherwise we assume we're targeting an internal table or a plus // Otherwise we assume we're targeting an internal table or a plus
@ -471,9 +468,12 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
Object.keys(schema).forEach(fieldKey => { Object.keys(schema).forEach(fieldKey => {
const fieldSchema = schema[fieldKey] const fieldSchema = schema[fieldKey]
if (fieldSchema?.type === "json") { if (fieldSchema?.type === "json") {
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, { const jsonSchema = JSONUtils.convertJSONSchemaToTableSchema(
fieldSchema,
{
squashObjects: true, squashObjects: true,
}) }
)
Object.keys(jsonSchema).forEach(jsonKey => { Object.keys(jsonSchema).forEach(jsonKey => {
jsonAdditions[`${fieldKey}.${jsonKey}`] = { jsonAdditions[`${fieldKey}.${jsonKey}`] = {
type: jsonSchema[jsonKey].type, type: jsonSchema[jsonKey].type,

View File

@ -1,16 +0,0 @@
import { get } from "builderStore/api"
/**
* Fetches the definitions for component library components. This includes
* their props and other metadata from components.json.
* @param {string} appId - ID of the currently running app
*/
export const fetchComponentLibDefinitions = async appId => {
const LIB_DEFINITION_URL = `/api/${appId}/components/definitions`
try {
const libDefinitionResponse = await get(LIB_DEFINITION_URL)
return await libDefinitionResponse.json()
} catch (err) {
console.error(`Error fetching component definitions for ${appId}`, err)
}
}

View File

@ -1,26 +1,40 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import api from "../../api" import { API } from "api"
import Automation from "./Automation" import Automation from "./Automation"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
const initialAutomationState = {
automations: [],
blockDefinitions: {
TRIGGER: [],
ACTION: [],
},
selectedAutomation: null,
}
export const getAutomationStore = () => {
const store = writable(initialAutomationState)
store.actions = automationActions(store)
return store
}
const automationActions = store => ({ const automationActions = store => ({
fetch: async () => { fetch: async () => {
const responses = await Promise.all([ const responses = await Promise.all([
api.get(`/api/automations`), API.getAutomations(),
api.get(`/api/automations/definitions/list`), API.getAutomationDefinitions(),
]) ])
const jsonResponses = await Promise.all(responses.map(x => x.json()))
store.update(state => { store.update(state => {
let selected = state.selectedAutomation?.automation let selected = state.selectedAutomation?.automation
state.automations = jsonResponses[0] state.automations = responses[0]
state.blockDefinitions = { state.blockDefinitions = {
TRIGGER: jsonResponses[1].trigger, TRIGGER: responses[1].trigger,
ACTION: jsonResponses[1].action, ACTION: responses[1].action,
} }
// if previously selected find the new obj and select it // If previously selected find the new obj and select it
if (selected) { if (selected) {
selected = jsonResponses[0].filter( selected = responses[0].filter(
automation => automation._id === selected._id automation => automation._id === selected._id
) )
state.selectedAutomation = new Automation(selected[0]) state.selectedAutomation = new Automation(selected[0])
@ -36,40 +50,36 @@ const automationActions = store => ({
steps: [], steps: [],
}, },
} }
const CREATE_AUTOMATION_URL = `/api/automations` const response = await API.createAutomation(automation)
const response = await api.post(CREATE_AUTOMATION_URL, automation)
const json = await response.json()
store.update(state => { store.update(state => {
state.automations = [...state.automations, json.automation] state.automations = [...state.automations, response.automation]
store.actions.select(json.automation) store.actions.select(response.automation)
return state return state
}) })
}, },
save: async automation => { save: async automation => {
const UPDATE_AUTOMATION_URL = `/api/automations` const response = await API.updateAutomation(automation)
const response = await api.put(UPDATE_AUTOMATION_URL, automation)
const json = await response.json()
store.update(state => { store.update(state => {
const newAutomation = json.automation const updatedAutomation = response.automation
const existingIdx = state.automations.findIndex( const existingIdx = state.automations.findIndex(
existing => existing._id === automation._id existing => existing._id === automation._id
) )
if (existingIdx !== -1) { if (existingIdx !== -1) {
state.automations.splice(existingIdx, 1, newAutomation) state.automations.splice(existingIdx, 1, updatedAutomation)
state.automations = [...state.automations] state.automations = [...state.automations]
store.actions.select(newAutomation) store.actions.select(updatedAutomation)
return state return state
} }
}) })
}, },
delete: async automation => { delete: async automation => {
const { _id, _rev } = automation await API.deleteAutomation({
const DELETE_AUTOMATION_URL = `/api/automations/${_id}/${_rev}` automationId: automation?._id,
await api.delete(DELETE_AUTOMATION_URL) automationRev: automation?._rev,
})
store.update(state => { store.update(state => {
const existingIdx = state.automations.findIndex( const existingIdx = state.automations.findIndex(
existing => existing._id === _id existing => existing._id === automation?._id
) )
state.automations.splice(existingIdx, 1) state.automations.splice(existingIdx, 1)
state.automations = [...state.automations] state.automations = [...state.automations]
@ -78,16 +88,17 @@ const automationActions = store => ({
return state return state
}) })
}, },
trigger: async automation => {
const { _id } = automation
return await api.post(`/api/automations/${_id}/trigger`)
},
test: async (automation, testData) => { test: async (automation, testData) => {
const { _id } = automation
const response = await api.post(`/api/automations/${_id}/test`, testData)
const json = await response.json()
store.update(state => { store.update(state => {
state.selectedAutomation.testResults = json state.selectedAutomation.testResults = null
return state
})
const result = await API.testAutomation({
automationId: automation?._id,
testData,
})
store.update(state => {
state.selectedAutomation.testResults = result
return state return state
}) })
}, },
@ -143,17 +154,3 @@ const automationActions = store => ({
}) })
}, },
}) })
export const getAutomationStore = () => {
const INITIAL_AUTOMATION_STATE = {
automations: [],
blockDefinitions: {
TRIGGER: [],
ACTION: [],
},
selectedAutomation: null,
}
const store = writable(INITIAL_AUTOMATION_STATE)
store.actions = automationActions(store)
return store
}

View File

@ -14,8 +14,7 @@ import {
database, database,
tables, tables,
} from "stores/backend" } from "stores/backend"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries" import { API } from "api"
import api from "../api"
import { FrontendTypes } from "constants" import { FrontendTypes } from "constants"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
import { import {
@ -26,7 +25,7 @@ import {
findComponent, findComponent,
getComponentSettings, getComponentSettings,
} from "../componentUtils" } from "../componentUtils"
import { uuid } from "../uuid" import { Helpers } from "@budibase/bbui"
import { removeBindings } from "../dataBinding" import { removeBindings } from "../dataBinding"
const INITIAL_FRONTEND_STATE = { const INITIAL_FRONTEND_STATE = {
@ -70,15 +69,12 @@ export const getFrontendStore = () => {
}, },
initialise: async pkg => { initialise: async pkg => {
const { layouts, screens, application, clientLibPath } = pkg const { layouts, screens, application, clientLibPath } = pkg
const components = await fetchComponentLibDefinitions(application.appId)
// make sure app isn't locked // Fetch component definitions.
if ( // Allow errors to propagate.
components && let components = await API.fetchComponentLibDefinitions(application.appId)
components.status === 400 &&
components.message?.includes("lock") // Reset store state
) {
throw { ok: false, reason: "locked" }
}
store.update(state => ({ store.update(state => ({
...state, ...state,
libraries: application.componentLibraries, libraries: application.componentLibraries,
@ -91,8 +87,8 @@ export const getFrontendStore = () => {
description: application.description, description: application.description,
appId: application.appId, appId: application.appId,
url: application.url, url: application.url,
layouts, layouts: layouts || [],
screens, screens: screens || [],
theme: application.theme || "spectrum--light", theme: application.theme || "spectrum--light",
customTheme: application.customTheme, customTheme: application.customTheme,
hasAppPackage: true, hasAppPackage: true,
@ -104,51 +100,43 @@ export const getFrontendStore = () => {
})) }))
// Initialise backend stores // Initialise backend stores
const [_integrations] = await Promise.all([
api.get("/api/integrations").then(r => r.json()),
])
datasources.init()
integrations.set(_integrations)
queries.init()
database.set(application.instance) database.set(application.instance)
tables.init() await datasources.init()
await integrations.init()
await queries.init()
await tables.init()
}, },
theme: { theme: {
save: async theme => { save: async theme => {
const appId = get(store).appId const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, { theme }) await API.saveAppMetadata({
if (response.status === 200) { appId,
metadata: { theme },
})
store.update(state => { store.update(state => {
state.theme = theme state.theme = theme
return state return state
}) })
} else {
throw new Error("Error updating theme")
}
}, },
}, },
customTheme: { customTheme: {
save: async customTheme => { save: async customTheme => {
const appId = get(store).appId const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, { await API.saveAppMetadata({
customTheme, appId,
metadata: { customTheme },
}) })
if (response.status === 200) {
store.update(state => { store.update(state => {
state.customTheme = customTheme state.customTheme = customTheme
return state return state
}) })
} else {
throw new Error("Error updating theme")
}
}, },
}, },
routing: { routing: {
fetch: async () => { fetch: async () => {
const response = await api.get("/api/routing") const response = await API.fetchAppRoutes()
const json = await response.json()
store.update(state => { store.update(state => {
state.routes = json.routes state.routes = response.routes
return state return state
}) })
}, },
@ -172,82 +160,76 @@ export const getFrontendStore = () => {
return state return state
}) })
}, },
create: async screen => {
screen = await store.actions.screens.save(screen)
store.update(state => {
state.selectedScreenId = screen._id
state.selectedComponentId = screen.props._id
state.currentFrontEndType = FrontendTypes.SCREEN
selectedAccessRole.set(screen.routing.roleId)
return state
})
return screen
},
save: async screen => { save: async screen => {
const creatingNewScreen = screen._id === undefined const creatingNewScreen = screen._id === undefined
const response = await api.post(`/api/screens`, screen) const savedScreen = await API.saveScreen(screen)
if (response.status !== 200) {
return
}
screen = await response.json()
await store.actions.routing.fetch()
store.update(state => { store.update(state => {
const foundScreen = state.screens.findIndex( const idx = state.screens.findIndex(x => x._id === savedScreen._id)
el => el._id === screen._id if (idx !== -1) {
) state.screens.splice(idx, 1, savedScreen)
if (foundScreen !== -1) { } else {
state.screens.splice(foundScreen, 1) state.screens.push(savedScreen)
} }
state.screens.push(screen)
return state return state
}) })
if (creatingNewScreen) { // Refresh routes
store.actions.screens.select(screen._id) await store.actions.routing.fetch()
}
return screen // Select the new screen if creating a new one
if (creatingNewScreen) {
store.actions.screens.select(savedScreen._id)
}
return savedScreen
}, },
delete: async screens => { delete: async screens => {
const screensToDelete = Array.isArray(screens) ? screens : [screens] const screensToDelete = Array.isArray(screens) ? screens : [screens]
const screenDeletePromises = [] // Build array of promises to speed up bulk deletions
store.update(state => { const promises = []
for (let screenToDelete of screensToDelete) { screensToDelete.forEach(screen => {
state.screens = state.screens.filter( // Delete the screen
screen => screen._id !== screenToDelete._id promises.push(
API.deleteScreen({
screenId: screen._id,
screenRev: screen._rev,
})
) )
screenDeletePromises.push( // Remove links to this screen
api.delete( promises.push(
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
)
)
if (screenToDelete._id === state.selectedScreenId) {
state.selectedScreenId = null
}
//remove the link for this screen
screenDeletePromises.push(
store.actions.components.links.delete( store.actions.components.links.delete(
screenToDelete.routing.route, screen.routing.route,
screenToDelete.props._instanceName screen.props._instanceName
) )
) )
})
await Promise.all(promises)
const deletedIds = screensToDelete.map(screen => screen._id)
store.update(state => {
// Remove deleted screens from state
state.screens = state.screens.filter(screen => {
return !deletedIds.includes(screen._id)
})
// Deselect the current screen if it was deleted
if (deletedIds.includes(state.selectedScreenId)) {
state.selectedScreenId = null
} }
return state return state
}) })
await Promise.all(screenDeletePromises)
// Refresh routes
await store.actions.routing.fetch()
}, },
}, },
preview: { preview: {
saveSelected: async () => { saveSelected: async () => {
const state = get(store) const state = get(store)
const selectedAsset = get(currentAsset) const selectedAsset = get(currentAsset)
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) { if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
await store.actions.screens.save(selectedAsset) return await store.actions.screens.save(selectedAsset)
} else { } else {
await store.actions.layouts.save(selectedAsset) return await store.actions.layouts.save(selectedAsset)
} }
}, },
setDevice: device => { setDevice: device => {
@ -271,25 +253,13 @@ export const getFrontendStore = () => {
}) })
}, },
save: async layout => { save: async layout => {
const layoutToSave = cloneDeep(layout) const creatingNewLayout = layout._id === undefined
const creatingNewLayout = layoutToSave._id === undefined const savedLayout = await API.saveLayout(layout)
const response = await api.post(`/api/layouts`, layoutToSave)
const savedLayout = await response.json()
// Abort if saving failed
if (response.status !== 200) {
return
}
store.update(state => { store.update(state => {
const layoutIdx = state.layouts.findIndex( const idx = state.layouts.findIndex(x => x._id === savedLayout._id)
stateLayout => stateLayout._id === savedLayout._id if (idx !== -1) {
) state.layouts.splice(idx, 1, savedLayout)
if (layoutIdx >= 0) {
// update existing layout
state.layouts.splice(layoutIdx, 1, savedLayout)
} else { } else {
// save new layout
state.layouts.push(savedLayout) state.layouts.push(savedLayout)
} }
return state return state
@ -299,7 +269,6 @@ export const getFrontendStore = () => {
if (creatingNewLayout) { if (creatingNewLayout) {
store.actions.layouts.select(savedLayout._id) store.actions.layouts.select(savedLayout._id)
} }
return savedLayout return savedLayout
}, },
find: layoutId => { find: layoutId => {
@ -309,21 +278,20 @@ export const getFrontendStore = () => {
const storeContents = get(store) const storeContents = get(store)
return storeContents.layouts.find(layout => layout._id === layoutId) return storeContents.layouts.find(layout => layout._id === layoutId)
}, },
delete: async layoutToDelete => { delete: async layout => {
const response = await api.delete( if (!layout?._id) {
`/api/layouts/${layoutToDelete._id}/${layoutToDelete._rev}` return
)
if (response.status !== 200) {
const json = await response.json()
throw new Error(json.message)
} }
await API.deleteLayout({
layoutId: layout._id,
layoutRev: layout._rev,
})
store.update(state => { store.update(state => {
state.layouts = state.layouts.filter( // Select main layout if we deleted the selected layout
layout => layout._id !== layoutToDelete._id if (layout._id === state.selectedLayoutId) {
)
if (layoutToDelete._id === state.selectedLayoutId) {
state.selectedLayoutId = get(mainLayout)._id state.selectedLayoutId = get(mainLayout)._id
} }
state.layouts = state.layouts.filter(x => x._id !== layout._id)
return state return state
}) })
}, },
@ -398,7 +366,7 @@ export const getFrontendStore = () => {
} }
return { return {
_id: uuid(), _id: Helpers.uuid(),
_component: definition.component, _component: definition.component,
_styles: { normal: {}, hover: {}, active: {} }, _styles: { normal: {}, hover: {}, active: {} },
_instanceName: `New ${definition.name}`, _instanceName: `New ${definition.name}`,
@ -415,16 +383,12 @@ export const getFrontendStore = () => {
componentName, componentName,
presetProps presetProps
) )
if (!componentInstance) { if (!componentInstance || !asset) {
return return
} }
// Find parent node to attach this component to // Find parent node to attach this component to
let parentComponent let parentComponent
if (!asset) {
return
}
if (selected) { if (selected) {
// Use current screen or layout as parent if no component is selected // Use current screen or layout as parent if no component is selected
const definition = store.actions.components.getDefinition( const definition = store.actions.components.getDefinition(
@ -552,7 +516,7 @@ export const getFrontendStore = () => {
if (!component) { if (!component) {
return return
} }
component._id = uuid() component._id = Helpers.uuid()
component._children?.forEach(randomizeIds) component._children?.forEach(randomizeIds)
} }
randomizeIds(componentToPaste) randomizeIds(componentToPaste)
@ -606,11 +570,6 @@ export const getFrontendStore = () => {
selected._styles.custom = style selected._styles.custom = style
await store.actions.preview.saveSelected() await store.actions.preview.saveSelected()
}, },
resetStyles: async () => {
const selected = get(selectedComponent)
selected._styles = { normal: {}, hover: {}, active: {} }
await store.actions.preview.saveSelected()
},
updateConditions: async conditions => { updateConditions: async conditions => {
const selected = get(selectedComponent) const selected = get(selectedComponent)
selected._conditions = conditions selected._conditions = conditions
@ -665,7 +624,7 @@ export const getFrontendStore = () => {
newLink = cloneDeep(nav._children[0]) newLink = cloneDeep(nav._children[0])
// Set our new props // Set our new props
newLink._id = uuid() newLink._id = Helpers.uuid()
newLink._instanceName = `${title} Link` newLink._instanceName = `${title} Link`
newLink.url = url newLink.url = url
newLink.text = title newLink.text = title

View File

@ -1,23 +0,0 @@
import { writable } from "svelte/store"
import { generate } from "shortid"
export const notificationStore = writable({
notifications: [],
})
export function send(message, type = "default") {
notificationStore.update(state => {
state.notifications = [
...state.notifications,
{ id: generate(), type, message },
]
return state
})
}
export const notifier = {
danger: msg => send(msg, "danger"),
warning: msg => send(msg, "warning"),
info: msg => send(msg, "info"),
success: msg => send(msg, "success"),
}

View File

@ -1,4 +1,4 @@
import { uuid } from "builderStore/uuid" import { Helpers } from "@budibase/bbui"
import { BaseStructure } from "./BaseStructure" import { BaseStructure } from "./BaseStructure"
export class Component extends BaseStructure { export class Component extends BaseStructure {
@ -6,7 +6,7 @@ export class Component extends BaseStructure {
super(false) super(false)
this._children = [] this._children = []
this._json = { this._json = {
_id: uuid(), _id: Helpers.uuid(),
_component: name, _component: name,
_styles: { _styles: {
normal: {}, normal: {},

View File

@ -1,5 +1,5 @@
import { BaseStructure } from "./BaseStructure" import { BaseStructure } from "./BaseStructure"
import { uuid } from "builderStore/uuid" import { Helpers } from "@budibase/bbui"
export class Screen extends BaseStructure { export class Screen extends BaseStructure {
constructor() { constructor() {
@ -7,7 +7,7 @@ export class Screen extends BaseStructure {
this._json = { this._json = {
layoutId: "layout_private_master", layoutId: "layout_private_master",
props: { props: {
_id: uuid(), _id: Helpers.uuid(),
_component: "@budibase/standard-components/container", _component: "@budibase/standard-components/container",
_styles: { _styles: {
normal: {}, normal: {},

View File

@ -1,4 +1,4 @@
import { localStorageStore } from "./localStorage" import { createLocalStorageStore } from "@budibase/frontend-core"
export const getThemeStore = () => { export const getThemeStore = () => {
const themeElement = document.documentElement const themeElement = document.documentElement
@ -6,7 +6,7 @@ export const getThemeStore = () => {
theme: "darkest", theme: "darkest",
options: ["lightest", "light", "dark", "darkest"], options: ["lightest", "light", "dark", "darkest"],
} }
const store = localStorageStore("bb-theme", initialValue) const store = createLocalStorageStore("bb-theme", initialValue)
// Update theme class when store changes // Update theme class when store changes
store.subscribe(state => { store.subscribe(state => {

View File

@ -1,9 +0,0 @@
export function uuid() {
// always want to make this start with a letter, as this makes it
// easier to use with template string bindings in the client
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}

View File

@ -6,6 +6,7 @@
Body, Body,
Icon, Icon,
Tooltip, Tooltip,
notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import { admin } from "stores/portal" import { admin } from "stores/portal"
@ -47,6 +48,7 @@
} }
async function addBlockToAutomation() { async function addBlockToAutomation() {
try {
const newBlock = $automationStore.selectedAutomation.constructBlock( const newBlock = $automationStore.selectedAutomation.constructBlock(
"ACTION", "ACTION",
actionVal.stepId, actionVal.stepId,
@ -56,6 +58,9 @@
await automationStore.actions.save( await automationStore.actions.save(
$automationStore.selectedAutomation?.automation $automationStore.selectedAutomation?.automation
) )
} catch (error) {
notifications.error("Error saving automation")
}
} }
</script> </script>

View File

@ -30,26 +30,13 @@
} }
async function deleteAutomation() { async function deleteAutomation() {
try {
await automationStore.actions.delete( await automationStore.actions.delete(
$automationStore.selectedAutomation?.automation $automationStore.selectedAutomation?.automation
) )
notifications.success("Automation deleted.") } catch (error) {
notifications.error("Error deleting automation")
} }
async function testAutomation() {
const result = await automationStore.actions.trigger(
$automationStore.selectedAutomation.automation
)
if (result.status === 200) {
notifications.success(
`Automation ${$automationStore.selectedAutomation.automation.name} triggered successfully.`
)
} else {
notifications.error(
`Failed to trigger automation ${$automationStore.selectedAutomation.automation.name}.`
)
}
return result
} }
</script> </script>
@ -85,7 +72,7 @@
animate:flip={{ duration: 500 }} animate:flip={{ duration: 500 }}
in:fly|local={{ x: 500, duration: 1500 }} in:fly|local={{ x: 500, duration: 1500 }}
> >
<FlowItem {testDataModal} {testAutomation} {onSelect} {block} /> <FlowItem {testDataModal} {onSelect} {block} />
</div> </div>
{/each} {/each}
</div> </div>
@ -101,7 +88,7 @@
</ConfirmDialog> </ConfirmDialog>
<Modal bind:this={testDataModal} width="30%"> <Modal bind:this={testDataModal} width="30%">
<TestDataModal {testAutomation} /> <TestDataModal />
</Modal> </Modal>
</div> </div>

View File

@ -10,6 +10,7 @@
Button, Button,
StatusLight, StatusLight,
ActionButton, ActionButton,
notifications,
} 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"
@ -54,10 +55,14 @@
).every(x => block?.inputs[x]) ).every(x => block?.inputs[x])
async function deleteStep() { async function deleteStep() {
try {
automationStore.actions.deleteAutomationBlock(block) automationStore.actions.deleteAutomationBlock(block)
await automationStore.actions.save( await automationStore.actions.save(
$automationStore.selectedAutomation?.automation $automationStore.selectedAutomation?.automation
) )
} catch (error) {
notifications.error("Error saving notification")
}
} }
</script> </script>

View File

@ -1,5 +1,12 @@
<script> <script>
import { ModalContent, Tabs, Tab, TextArea, Label } from "@budibase/bbui" import {
ModalContent,
Tabs,
Tab,
TextArea,
Label,
notifications,
} from "@budibase/bbui"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
@ -37,6 +44,17 @@
failedParse = "Invalid JSON" failedParse = "Invalid JSON"
} }
} }
const testAutomation = async () => {
try {
await automationStore.actions.test(
$automationStore.selectedAutomation?.automation,
testData
)
} catch (error) {
notifications.error("Error testing notification")
}
}
</script> </script>
<ModalContent <ModalContent
@ -44,12 +62,7 @@
confirmText="Test" confirmText="Test"
showConfirmButton={true} showConfirmButton={true}
disabled={isError} disabled={isError}
onConfirm={() => { onConfirm={testAutomation}
automationStore.actions.test(
$automationStore.selectedAutomation?.automation,
testData
)
}}
cancelText="Cancel" cancelText="Cancel"
> >
<Tabs selected="Form" quiet <Tabs selected="Form" quiet

View File

@ -4,10 +4,16 @@
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import EditAutomationPopover from "./EditAutomationPopover.svelte" import EditAutomationPopover from "./EditAutomationPopover.svelte"
import { notifications } from "@budibase/bbui"
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id $: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
onMount(() => {
automationStore.actions.fetch() onMount(async () => {
try {
await automationStore.actions.fetch()
} catch (error) {
notifications.error("Error getting automations list")
}
}) })
function selectAutomation(automation) { function selectAutomation(automation) {

View File

@ -24,6 +24,7 @@
nameTouched && !name ? "Please specify a name for the automation." : null nameTouched && !name ? "Please specify a name for the automation." : null
async function createAutomation() { async function createAutomation() {
try {
await automationStore.actions.create({ await automationStore.actions.create({
name, name,
instanceId, instanceId,
@ -43,10 +44,13 @@
$automationStore.selectedAutomation?.automation $automationStore.selectedAutomation?.automation
) )
notifications.success(`Automation ${name} created.`) notifications.success(`Automation ${name} created`)
$goto(`./${$automationStore.selectedAutomation.automation._id}`) $goto(`./${$automationStore.selectedAutomation.automation._id}`)
analytics.captureEvent(Events.AUTOMATION.CREATED, { name }) analytics.captureEvent(Events.AUTOMATION.CREATED, { name })
} catch (error) {
notifications.error("Error creating automation")
}
} }
$: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER) $: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER)

View File

@ -11,9 +11,13 @@
let updateAutomationDialog let updateAutomationDialog
async function deleteAutomation() { async function deleteAutomation() {
try {
await automationStore.actions.delete(automation) await automationStore.actions.delete(automation)
notifications.success("Automation deleted.") notifications.success("Automation deleted successfully")
$goto("../automate") $goto("../automate")
} catch (error) {
notifications.error("Error deleting automation")
}
} }
</script> </script>

View File

@ -20,14 +20,18 @@
} }
async function saveAutomation() { async function saveAutomation() {
try {
const updatedAutomation = { const updatedAutomation = {
...automation, ...automation,
name, name,
} }
await automationStore.actions.save(updatedAutomation) await automationStore.actions.save(updatedAutomation)
notifications.success(`Automation ${name} updated successfully.`) notifications.success(`Automation ${name} updated successfully`)
analytics.captureEvent(Events.AUTOMATION.SAVED, { name }) analytics.captureEvent(Events.AUTOMATION.SAVED, { name })
hide() hide()
} catch (error) {
notifications.error("Error saving automation")
}
} }
function checkValid(evt) { function checkValid(evt) {

View File

@ -11,6 +11,7 @@
Drawer, Drawer,
Modal, Modal,
Detail, Detail,
notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
@ -28,7 +29,7 @@
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte" import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte" import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
// need the client lucene builder to convert to the structure API expects // need the client lucene builder to convert to the structure API expects
import { buildLuceneQuery } from "helpers/lucene" import { LuceneUtils } from "@budibase/frontend-core"
export let block export let block
export let testData export let testData
@ -54,6 +55,7 @@
$: schemaFields = table ? Object.values(table.schema) : [] $: schemaFields = table ? Object.values(table.schema) : []
const onChange = debounce(async function (e, key) { const onChange = debounce(async function (e, key) {
try {
if (isTestModal) { if (isTestModal) {
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents // Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === "WEBHOOK") { if (stepId === "WEBHOOK") {
@ -77,6 +79,9 @@
$automationStore.selectedAutomation?.automation $automationStore.selectedAutomation?.automation
) )
} }
} catch (error) {
notifications.error("Error saving automation")
}
}, 800) }, 800)
function getAvailableBindings(block, automation) { function getAvailableBindings(block, automation) {
@ -131,7 +136,7 @@
} }
function saveFilters(key) { function saveFilters(key) {
const filters = buildLuceneQuery(tempFilters) const filters = LuceneUtils.buildLuceneQuery(tempFilters)
const defKey = `${key}-def` const defKey = `${key}-def`
inputData[key] = filters inputData[key] = filters
inputData[defKey] = tempFilters inputData[defKey] = tempFilters

View File

@ -1,5 +1,5 @@
<script> <script>
import { Icon } from "@budibase/bbui" import { Icon, notifications } from "@budibase/bbui"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import WebhookDisplay from "./WebhookDisplay.svelte" import WebhookDisplay from "./WebhookDisplay.svelte"
import { ModalContent } from "@budibase/bbui" import { ModalContent } from "@budibase/bbui"
@ -16,16 +16,25 @@
onMount(async () => { onMount(async () => {
if (!automation?.definition?.trigger?.inputs.schemaUrl) { if (!automation?.definition?.trigger?.inputs.schemaUrl) {
// save the automation initially // save the automation initially
try {
await automationStore.actions.save(automation) await automationStore.actions.save(automation)
} catch (error) {
notifications.error("Error saving automation")
}
} }
interval = setInterval(async () => { interval = setInterval(async () => {
try {
await automationStore.actions.fetch() await automationStore.actions.fetch()
const outputs = automation?.definition?.trigger.schema.outputs?.properties const outputs =
automation?.definition?.trigger.schema.outputs?.properties
// always one prop for the "body" // always one prop for the "body"
if (Object.keys(outputs).length > 1) { if (Object.keys(outputs).length > 1) {
propCount = Object.keys(outputs).length - 1 propCount = Object.keys(outputs).length - 1
finished = true finished = true
} }
} catch (error) {
notifications.error("Error getting automations list")
}
}, POLL_RATE_MS) }, POLL_RATE_MS)
schemaURL = automation?.definition?.trigger?.inputs.schemaUrl schemaURL = automation?.definition?.trigger?.inputs.schemaUrl
}) })

View File

@ -14,18 +14,19 @@
import Table from "./Table.svelte" import Table from "./Table.svelte"
import { TableNames } from "constants" import { TableNames } from "constants"
import CreateEditRow from "./modals/CreateEditRow.svelte" import CreateEditRow from "./modals/CreateEditRow.svelte"
import { fetchTableData } from "helpers/fetchTableData"
import { Pagination } from "@budibase/bbui" import { Pagination } from "@budibase/bbui"
import { fetchData } from "@budibase/frontend-core"
import { API } from "api"
let hideAutocolumns = true let hideAutocolumns = true
$: isUsersTable = $tables.selected?._id === TableNames.USERS $: isUsersTable = $tables.selected?._id === TableNames.USERS
$: type = $tables.selected?.type $: type = $tables.selected?.type
$: isInternal = type !== "external" $: isInternal = type !== "external"
$: schema = $tables.selected?.schema $: schema = $tables.selected?.schema
$: enrichedSchema = enrichSchema($tables.selected?.schema) $: enrichedSchema = enrichSchema($tables.selected?.schema)
$: id = $tables.selected?._id $: id = $tables.selected?._id
$: search = searchTable(id) $: fetch = createFetch(id)
$: columnOptions = Object.keys($search.schema || {})
const enrichSchema = schema => { const enrichSchema = schema => {
let tempSchema = { ...schema } let tempSchema = { ...schema }
@ -47,18 +48,24 @@
return tempSchema return tempSchema
} }
// Fetches new data whenever the table changes // Fetches new data whenever the table changes
const searchTable = tableId => { const createFetch = tableId => {
return fetchTableData({ return fetchData({
API,
datasource: {
tableId, tableId,
type: "table",
},
options: {
schema, schema,
limit: 10, limit: 10,
paginate: true, paginate: true,
},
}) })
} }
// Fetch data whenever sorting option changes // Fetch data whenever sorting option changes
const onSort = e => { const onSort = e => {
search.update({ fetch.update({
sortColumn: e.detail.column, sortColumn: e.detail.column,
sortOrder: e.detail.order, sortOrder: e.detail.order,
}) })
@ -66,22 +73,20 @@
// Fetch data whenever filters change // Fetch data whenever filters change
const onFilter = e => { const onFilter = e => {
search.update({ fetch.update({
filters: e.detail, filter: e.detail,
}) })
} }
// Fetch data whenever schema changes // Fetch data whenever schema changes
const onUpdateColumns = () => { const onUpdateColumns = () => {
search.update({ fetch.refresh()
schema,
})
} }
// Fetch data whenever rows are modified. Unfortunately we have to lose // Fetch data whenever rows are modified. Unfortunately we have to lose
// our pagination place, as our bookmarks will have shifted. // our pagination place, as our bookmarks will have shifted.
const onUpdateRows = () => { const onUpdateRows = () => {
search.update() fetch.refresh()
} }
</script> </script>
@ -91,9 +96,9 @@
schema={enrichedSchema} schema={enrichedSchema}
{type} {type}
tableId={id} tableId={id}
data={$search.rows} data={$fetch.rows}
bind:hideAutocolumns bind:hideAutocolumns
loading={$search.loading} loading={$fetch.loading}
on:sort={onSort} on:sort={onSort}
allowEditing allowEditing
disableSorting disableSorting
@ -138,11 +143,11 @@
<div in:fade={{ delay: 200, duration: 100 }}> <div in:fade={{ delay: 200, duration: 100 }}>
<div class="pagination"> <div class="pagination">
<Pagination <Pagination
page={$search.pageNumber + 1} page={$fetch.pageNumber + 1}
hasPrevPage={$search.hasPrevPage} hasPrevPage={$fetch.hasPrevPage}
hasNextPage={$search.hasNextPage} hasNextPage={$fetch.hasNextPage}
goToPrevPage={$search.loading ? null : search.prevPage} goToPrevPage={$fetch.loading ? null : fetch.prevPage}
goToNextPage={$search.loading ? null : search.nextPage} goToNextPage={$fetch.loading ? null : fetch.nextPage}
/> />
</div> </div>
</div> </div>

View File

@ -1,7 +1,8 @@
<script> <script>
import api from "builderStore/api" import { API } from "api"
import Table from "./Table.svelte" import Table from "./Table.svelte"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { notifications } from "@budibase/bbui"
export let tableId export let tableId
export let rowId export let rowId
@ -27,9 +28,15 @@
} }
async function fetchData(tableId, rowId) { async function fetchData(tableId, rowId) {
const QUERY_VIEW_URL = `/api/${tableId}/${rowId}/enrich` try {
const response = await api.get(QUERY_VIEW_URL) row = await API.fetchRelationshipData({
row = await response.json() tableId,
rowId,
})
} catch (error) {
row = null
notifications.error("Error fetching relationship data")
}
} }
</script> </script>

View File

@ -2,7 +2,7 @@
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { goto, params } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { Table, Modal, Heading, notifications, Layout } from "@budibase/bbui" import { Table, Modal, Heading, notifications, Layout } from "@budibase/bbui"
import api from "builderStore/api" import { API } from "api"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte" import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
import CreateEditRow from "./modals/CreateEditRow.svelte" import CreateEditRow from "./modals/CreateEditRow.svelte"
@ -88,12 +88,17 @@
} }
const deleteRows = async () => { const deleteRows = async () => {
await api.delete(`/api/${tableId}/rows`, { try {
await API.deleteRows({
tableId,
rows: selectedRows, rows: selectedRows,
}) })
data = data.filter(row => !selectedRows.includes(row)) data = data.filter(row => !selectedRows.includes(row))
notifications.success(`Successfully deleted ${selectedRows.length} rows`) notifications.success(`Successfully deleted ${selectedRows.length} rows`)
selectedRows = [] selectedRows = []
} catch (error) {
notifications.error("Error deleting rows")
}
} }
const editRow = row => { const editRow = row => {

View File

@ -1,5 +1,5 @@
<script> <script>
import api from "builderStore/api" import { API } from "api"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import Table from "./Table.svelte" import Table from "./Table.svelte"
@ -9,6 +9,7 @@
import ExportButton from "./buttons/ExportButton.svelte" import ExportButton from "./buttons/ExportButton.svelte"
import ManageAccessButton from "./buttons/ManageAccessButton.svelte" import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte" import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
import { notifications } from "@budibase/bbui"
export let view = {} export let view = {}
@ -20,33 +21,31 @@
$: name = view.name $: name = view.name
// Fetch rows for specified view // Fetch rows for specified view
$: { $: fetchViewData(name, view.field, view.groupBy, view.calculation)
loading = true
fetchViewData(name, view.field, view.groupBy, view.calculation)
}
async function fetchViewData(name, field, groupBy, calculation) { async function fetchViewData(name, field, groupBy, calculation) {
loading = true
const _tables = $tables.list const _tables = $tables.list
const allTableViews = _tables.map(table => table.views) const allTableViews = _tables.map(table => table.views)
const thisView = allTableViews.filter( const thisView = allTableViews.filter(
views => views != null && views[name] != null views => views != null && views[name] != null
)[0] )[0]
// don't fetch view data if the view no longer exists // Don't fetch view data if the view no longer exists
if (!thisView) { if (!thisView) {
loading = false
return return
} }
const params = new URLSearchParams() try {
if (calculation) { data = await API.fetchViewData({
params.set("field", field) name,
params.set("calculation", calculation) calculation,
field,
groupBy,
})
} catch (error) {
notifications.error("Error fetching view data")
} }
if (groupBy) {
params.set("group", groupBy)
}
const QUERY_VIEW_URL = `/api/views/${name}?${params}`
const response = await api.get(QUERY_VIEW_URL)
data = await response.json()
loading = false loading = false
} }
</script> </script>

View File

@ -1,34 +0,0 @@
import api from "builderStore/api"
export async function createUser(user) {
const CREATE_USER_URL = `/api/users/metadata`
const response = await api.post(CREATE_USER_URL, user)
return await response.json()
}
export async function saveRow(row, tableId) {
const SAVE_ROW_URL = `/api/${tableId}/rows`
const response = await api.post(SAVE_ROW_URL, row)
return await response.json()
}
export async function deleteRow(row) {
const DELETE_ROWS_URL = `/api/${row.tableId}/rows`
return api.delete(DELETE_ROWS_URL, {
_id: row._id,
_rev: row._rev,
})
}
export async function fetchDataForTable(tableId) {
const FETCH_ROWS_URL = `/api/${tableId}/rows`
const response = await api.get(FETCH_ROWS_URL)
const json = await response.json()
if (response.status !== 200) {
throw new Error(json.message)
}
return json
}

View File

@ -38,9 +38,13 @@
}) })
function saveView() { function saveView() {
try {
views.save(view) views.save(view)
notifications.success(`View ${view.name} saved.`) notifications.success(`View ${view.name} saved`)
analytics.captureEvent(Events.VIEW.ADDED_CALCULATE, { field: view.field }) analytics.captureEvent(Events.VIEW.ADDED_CALCULATE, { field: view.field })
} catch (error) {
notifications.error("Error saving view")
}
} }
</script> </script>

View File

@ -124,7 +124,7 @@
}) })
dispatch("updatecolumns") dispatch("updatecolumns")
} catch (err) { } catch (err) {
notifications.error(err) notifications.error("Error saving column")
} }
} }
@ -133,6 +133,7 @@
} }
function deleteColumn() { function deleteColumn() {
try {
field.name = deleteColName field.name = deleteColName
if (field.name === $tables.selected.primaryDisplay) { if (field.name === $tables.selected.primaryDisplay) {
notifications.error("You cannot delete the display column") notifications.error("You cannot delete the display column")
@ -142,9 +143,12 @@
confirmDeleteDialog.hide() confirmDeleteDialog.hide()
hide() hide()
deletion = false deletion = false
}
dispatch("updatecolumns") dispatch("updatecolumns")
} }
} catch (error) {
notifications.error("Error deleting column")
}
}
function handleTypeChange(event) { function handleTypeChange(event) {
// remove any extra fields that may not be related to this type // remove any extra fields that may not be related to this type

View File

@ -3,7 +3,7 @@
import { tables, rows } from "stores/backend" import { tables, rows } from "stores/backend"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte" import RowFieldControl from "../RowFieldControl.svelte"
import * as api from "../api" import { API } from "api"
import { ModalContent } from "@budibase/bbui" import { ModalContent } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte" import ErrorsBox from "components/common/ErrorsBox.svelte"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
@ -22,30 +22,30 @@
$: tableSchema = Object.entries(table?.schema ?? {}) $: tableSchema = Object.entries(table?.schema ?? {})
async function saveRow() { async function saveRow() {
const rowResponse = await api.saveRow( errors = []
{ ...row, tableId: table._id }, try {
table._id await API.saveRow({ ...row, tableId: table._id })
) notifications.success("Row saved successfully")
rows.save()
if (rowResponse.errors) { dispatch("updaterows")
errors = Object.entries(rowResponse.errors) } catch (error) {
if (error.handled) {
const response = error.json
if (response?.errors) {
errors = Object.entries(response.errors)
.map(([key, error]) => ({ dataPath: key, message: error })) .map(([key, error]) => ({ dataPath: key, message: error }))
.flat() .flat()
} else if (error.status === 400 && response?.validationErrors) {
errors = Object.keys(response.validationErrors).map(field => ({
message: `${field} ${response.validationErrors[field][0]}`,
}))
}
} else {
notifications.error("Failed to save row")
}
// Prevent modal closing if there were errors // Prevent modal closing if there were errors
return false return false
} else if (rowResponse.status === 400 && rowResponse.validationErrors) {
errors = Object.keys(rowResponse.validationErrors).map(field => ({
message: `${field} ${rowResponse.validationErrors[field][0]}`,
}))
return false
} else if (rowResponse.status >= 400) {
errors = [{ message: rowResponse.message }]
return false
} }
notifications.success("Row saved successfully.")
rows.save(rowResponse)
dispatch("updaterows")
} }
</script> </script>

View File

@ -4,7 +4,7 @@
import { roles } from "stores/backend" import { roles } from "stores/backend"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte" import RowFieldControl from "../RowFieldControl.svelte"
import * as backendApi from "../api" import { API } from "api"
import { ModalContent, Select } from "@budibase/bbui" import { ModalContent, Select } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte" import ErrorsBox from "components/common/ErrorsBox.svelte"
@ -53,27 +53,31 @@
return false return false
} }
const rowResponse = await backendApi.saveRow( try {
{ ...row, tableId: table._id }, await API.saveRow({ ...row, tableId: table._id })
table._id notifications.success("User saved successfully")
) rows.save()
if (rowResponse.errors) { dispatch("updaterows")
if (Array.isArray(rowResponse.errors)) { } catch (error) {
errors = rowResponse.errors.map(error => ({ message: error })) if (error.handled) {
const response = error.json
if (response?.errors) {
if (Array.isArray(response.errors)) {
errors = response.errors.map(error => ({ message: error }))
} else { } else {
errors = Object.entries(rowResponse.errors) errors = Object.entries(response.errors)
.map(([key, error]) => ({ dataPath: key, message: error })) .map(([key, error]) => ({ dataPath: key, message: error }))
.flat() .flat()
} }
return false } else if (error.status === 400) {
} else if (rowResponse.status === 400 || rowResponse.status === 500) { errors = [{ message: response?.message || "Unknown error" }]
errors = [{ message: rowResponse.message }] }
} else {
notifications.error("Error saving user")
}
// Prevent closing the modal on errors
return false return false
} }
notifications.success("User saved successfully")
rows.save(rowResponse)
dispatch("updaterows")
} }
</script> </script>

View File

@ -12,9 +12,10 @@
function saveView() { function saveView() {
if (views.includes(name)) { if (views.includes(name)) {
notifications.error(`View exists with name ${name}.`) notifications.error(`View exists with name ${name}`)
return return
} }
try {
viewsStore.save({ viewsStore.save({
name, name,
tableId: $tables.selected._id, tableId: $tables.selected._id,
@ -23,6 +24,9 @@
notifications.success(`View ${name} created`) notifications.success(`View ${name} created`)
analytics.captureEvent(Events.VIEW.CREATED, { name }) analytics.captureEvent(Events.VIEW.CREATED, { name })
$goto(`../../view/${name}`) $goto(`../../view/${name}`)
} catch (error) {
notifications.error("Error creating view")
}
} }
</script> </script>

View File

@ -1,7 +1,7 @@
<script> <script>
import { ModalContent, Select, Input, Button } from "@budibase/bbui" import { ModalContent, Select, Input, Button } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import api from "builderStore/api" import { API } from "api"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte" import ErrorsBox from "components/common/ErrorsBox.svelte"
import { roles } from "stores/backend" import { roles } from "stores/backend"
@ -24,8 +24,12 @@
!builtInRoles.includes(selectedRole.name) !builtInRoles.includes(selectedRole.name)
const fetchBasePermissions = async () => { const fetchBasePermissions = async () => {
const permissionsResponse = await api.get("/api/permission/builtin") try {
basePermissions = await permissionsResponse.json() basePermissions = await API.getBasePermissions()
} catch (error) {
notifications.error("Error fetching base permission options")
basePermissions = []
}
} }
// Changes the selected role // Changes the selected role
@ -68,23 +72,23 @@
} }
// Save/create the role // Save/create the role
const response = await roles.save(selectedRole) try {
if (response.status === 200) { await roles.save(selectedRole)
notifications.success("Role saved successfully.") notifications.success("Role saved successfully")
} else { } catch (error) {
notifications.error("Error saving role.") notifications.error("Error saving role")
return false return false
} }
} }
// Deletes the selected role // Deletes the selected role
const deleteRole = async () => { const deleteRole = async () => {
const response = await roles.delete(selectedRole) try {
if (response.status === 200) { await roles.delete(selectedRole)
changeRole() changeRole()
notifications.success("Role deleted successfully.") notifications.success("Role deleted successfully")
} else { } catch (error) {
notifications.error("Error deleting role.") notifications.error("Error deleting role")
} }
} }

View File

@ -1,7 +1,7 @@
<script> <script>
import { Select, ModalContent, notifications } from "@budibase/bbui" import { Select, ModalContent, notifications } from "@budibase/bbui"
import download from "downloadjs" import download from "downloadjs"
import { get } from "builderStore/api" import { API } from "api"
const FORMATS = [ const FORMATS = [
{ {
@ -19,17 +19,14 @@
let exportFormat = FORMATS[0].key let exportFormat = FORMATS[0].key
async function exportView() { async function exportView() {
const uri = encodeURIComponent(view) try {
const response = await get( const data = await API.exportView({
`/api/views/export?view=${uri}&format=${exportFormat}` viewName: view,
) format: exportFormat,
if (response.status === 200) { })
const data = await response.text()
download(data, `export.${exportFormat}`) download(data, `export.${exportFormat}`)
} else { } catch (error) {
notifications.error( notifications.error(`Unable to export ${exportFormat.toUpperCase()} data`)
`Unable to export ${exportFormat.toUpperCase()} data.`
)
} }
} }
</script> </script>

View File

@ -72,11 +72,15 @@
$: schema = viewTable && viewTable.schema ? viewTable.schema : {} $: schema = viewTable && viewTable.schema ? viewTable.schema : {}
function saveView() { function saveView() {
try {
views.save(view) views.save(view)
notifications.success(`View ${view.name} saved.`) notifications.success(`View ${view.name} saved`)
analytics.captureEvent(Events.VIEW.ADDED_FILTER, { analytics.captureEvent(Events.VIEW.ADDED_FILTER, {
filters: JSON.stringify(view.filters), filters: JSON.stringify(view.filters),
}) })
} catch (error) {
notifications.error("Error saving view")
}
} }
function removeFilter(idx) { function removeFilter(idx) {
@ -158,7 +162,7 @@
<Select <Select
bind:value={filter.value} bind:value={filter.value}
options={fieldOptions(filter.key)} options={fieldOptions(filter.key)}
getOptionLabel={x => x.toString()} getOptionLabel={x => x?.toString() || ""}
/> />
{:else if filter.key && isDate(filter.key)} {:else if filter.key && isDate(filter.key)}
<DatePicker <DatePicker

View File

@ -19,8 +19,12 @@
.map(([key]) => key) .map(([key]) => key)
function saveView() { function saveView() {
try {
views.save(view) views.save(view)
notifications.success(`View ${view.name} saved.`) notifications.success(`View ${view.name} saved`)
} catch (error) {
notifications.error("Error saving view")
}
} }
</script> </script>

View File

@ -1,7 +1,13 @@
<script> <script>
import { ModalContent, Label, notifications, Body } from "@budibase/bbui" import {
ModalContent,
Label,
notifications,
Body,
Layout,
} from "@budibase/bbui"
import TableDataImport from "../../TableNavigator/TableDataImport.svelte" import TableDataImport from "../../TableNavigator/TableDataImport.svelte"
import api from "builderStore/api" import { API } from "api"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -12,15 +18,17 @@
$: valid = dataImport?.csvString != null && dataImport?.valid $: valid = dataImport?.csvString != null && dataImport?.valid
async function importData() { async function importData() {
const response = await api.post(`/api/tables/${tableId}/import`, { try {
dataImport, await API.importTableData({
tableId,
data: dataImport,
}) })
if (response.status !== 200) { notifications.success("Rows successfully imported")
const error = await response.text() } catch (error) {
notifications.error(`Unable to import data - ${error}`) notifications.error("Unable to import data")
} else {
notifications.success("Rows successfully imported.")
} }
// Always refresh rows just to be sure
dispatch("updaterows") dispatch("updaterows")
} }
</script> </script>
@ -31,12 +39,14 @@
onConfirm={importData} onConfirm={importData}
disabled={!valid} disabled={!valid}
> >
<Body <Body size="S">
>Import rows to an existing table from a CSV. Only columns from the CSV Import rows to an existing table from a CSV. Only columns from the CSV which
which exist in the table will be imported.</Body exist in the table will be imported.
> </Body>
<Layout gap="XS" noPadding>
<Label grey extraSmall>CSV to import</Label> <Label grey extraSmall>CSV to import</Label>
<TableDataImport bind:dataImport bind:existingTableId={tableId} /> <TableDataImport bind:dataImport bind:existingTableId={tableId} />
</Layout>
</ModalContent> </ModalContent>
<style> <style>

View File

@ -14,6 +14,7 @@
export let permissions export let permissions
async function changePermission(level, role) { async function changePermission(level, role) {
try {
await permissionsStore.save({ await permissionsStore.save({
level, level,
role, role,
@ -22,7 +23,10 @@
// Show updated permissions in UI: REMOVE // Show updated permissions in UI: REMOVE
permissions = await permissionsStore.forResource(resourceId) permissions = await permissionsStore.forResource(resourceId)
notifications.success("Updated permissions.") notifications.success("Updated permissions")
} catch (error) {
notifications.error("Error updating permissions")
}
} }
</script> </script>

View File

@ -10,6 +10,7 @@
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte" import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
import { customQueryIconText, customQueryIconColor } from "helpers/data/utils" import { customQueryIconText, customQueryIconColor } from "helpers/data/utils"
import ICONS from "./icons" import ICONS from "./icons"
import { notifications } from "@budibase/bbui"
let openDataSources = [] let openDataSources = []
$: enrichedDataSources = Array.isArray($datasources.list) $: enrichedDataSources = Array.isArray($datasources.list)
@ -63,9 +64,13 @@
} }
} }
onMount(() => { onMount(async () => {
datasources.fetch() try {
queries.fetch() await datasources.fetch()
await queries.fetch()
} catch (error) {
notifications.error("Error fetching datasources and queries")
}
}) })
const containsActiveEntity = datasource => { const containsActiveEntity = datasource => {

View File

@ -1,5 +1,5 @@
<script> <script>
import { ModalContent, Body, Input } from "@budibase/bbui" import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
import { tables, datasources } from "stores/backend" import { tables, datasources } from "stores/backend"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
@ -29,10 +29,14 @@
} }
async function saveTable() { async function saveTable() {
try {
submitted = true submitted = true
const table = await tables.save(buildDefaultTable(name, datasource._id)) const table = await tables.save(buildDefaultTable(name, datasource._id))
await datasources.fetch() await datasources.fetch()
$goto(`../../table/${table._id}`) $goto(`../../table/${table._id}`)
} catch (error) {
notifications.error("Error saving table")
}
} }
</script> </script>

View File

@ -90,8 +90,8 @@
await datasources.updateSchema(datasource) await datasources.updateSchema(datasource)
notifications.success(`Datasource ${name} tables updated successfully.`) notifications.success(`Datasource ${name} tables updated successfully.`)
await tables.fetch() await tables.fetch()
} catch (err) { } catch (error) {
notifications.error(`Error updating datasource schema: ${err}`) notifications.error("Error updating datasource schema")
} }
} }

View File

@ -1,7 +1,7 @@
<script> <script>
import { Body } from "@budibase/bbui" import { Body, notifications } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import api from "builderStore/api" import { API } from "api"
import ICONS from "../icons" import ICONS from "../icons"
export let integration = {} export let integration = {}
@ -9,14 +9,17 @@
const INTERNAL = "BUDIBASE" const INTERNAL = "BUDIBASE"
async function fetchIntegrations() { async function fetchIntegrations() {
const response = await api.get("/api/integrations") let otherIntegrations
const json = await response.json() try {
otherIntegrations = await API.getIntegrations()
} catch (error) {
otherIntegrations = {}
notifications.error("Error getting integrations")
}
integrations = { integrations = {
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" }, [INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
...json, ...otherIntegrations,
} }
return json
} }
function selectIntegration(integrationType) { function selectIntegration(integrationType) {

View File

@ -2,7 +2,7 @@
import { Table, Modal, Layout, ActionButton } from "@budibase/bbui" import { Table, Modal, Layout, ActionButton } from "@budibase/bbui"
import AuthTypeRenderer from "./AuthTypeRenderer.svelte" import AuthTypeRenderer from "./AuthTypeRenderer.svelte"
import RestAuthenticationModal from "./RestAuthenticationModal.svelte" import RestAuthenticationModal from "./RestAuthenticationModal.svelte"
import { uuid } from "builderStore/uuid" import { Helpers } from "@budibase/bbui"
export let configs = [] export let configs = []
@ -29,7 +29,7 @@
return c return c
}) })
} else { } else {
config._id = uuid() config._id = Helpers.uuid()
configs = [...configs, config] configs = [...configs, config]
} }
} }

View File

@ -1,8 +1,15 @@
<script> <script>
import { ModalContent, Modal, Body, Layout, Detail } from "@budibase/bbui" import {
ModalContent,
Modal,
Body,
Layout,
Detail,
notifications,
} from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import ICONS from "../icons" import ICONS from "../icons"
import api from "builderStore/api" import { API } from "api"
import { IntegrationNames, IntegrationTypes } from "constants/backend" import { IntegrationNames, IntegrationTypes } from "constants/backend"
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte" import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte" import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
@ -12,7 +19,7 @@
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte" import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
export let modal export let modal
let integrations = [] let integrations = {}
let integration = {} let integration = {}
let internalTableModal let internalTableModal
let externalDatasourceModal let externalDatasourceModal
@ -57,22 +64,32 @@
externalDatasourceModal.hide() externalDatasourceModal.hide()
internalTableModal.show() internalTableModal.show()
} else if (integration.type === IntegrationTypes.REST) { } else if (integration.type === IntegrationTypes.REST) {
// skip modal for rest, create straight away try {
// Skip modal for rest, create straight away
const resp = await createRestDatasource(integration) const resp = await createRestDatasource(integration)
$goto(`./datasource/${resp._id}`) $goto(`./datasource/${resp._id}`)
} catch (error) {
notifications.error("Error creating datasource")
}
} else { } else {
externalDatasourceModal.show() externalDatasourceModal.show()
} }
} }
async function fetchIntegrations() { async function fetchIntegrations() {
const response = await api.get("/api/integrations") let newIntegrations = {
const json = await response.json()
integrations = {
[IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" }, [IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
...json,
} }
return json try {
const integrationList = await API.getIntegrations()
newIntegrations = {
...newIntegrations,
...integrationList,
}
} catch (error) {
notifications.error("Error fetching integrations")
}
integrations = newIntegrations
} }
</script> </script>

View File

@ -20,7 +20,7 @@
$goto(`./datasource/${resp._id}`) $goto(`./datasource/${resp._id}`)
notifications.success(`Datasource updated successfully.`) notifications.success(`Datasource updated successfully.`)
} catch (err) { } catch (err) {
notifications.error(`Error saving datasource: ${err}`) notifications.error("Error saving datasource")
} }
} }

View File

@ -79,8 +79,8 @@
}) })
return true return true
} catch (err) { } catch (error) {
notifications.error(`Error importing: ${err}`) notifications.error("Error importing queries")
return false return false
} }
} }

View File

@ -12,6 +12,7 @@
let updateDatasourceDialog let updateDatasourceDialog
async function deleteDatasource() { async function deleteDatasource() {
try {
let wasSelectedSource = $datasources.selected let wasSelectedSource = $datasources.selected
if (!wasSelectedSource && $queries.selected) { if (!wasSelectedSource && $queries.selected) {
const queryId = $queries.selected const queryId = $queries.selected
@ -22,7 +23,7 @@
const wasSelectedTable = $tables.selected const wasSelectedTable = $tables.selected
await datasources.delete(datasource) await datasources.delete(datasource)
notifications.success("Datasource deleted") notifications.success("Datasource deleted")
// navigate to first index page if the source you are deleting is selected // Navigate to first index page if the source you are deleting is selected
const entities = Object.values(datasource?.entities || {}) const entities = Object.values(datasource?.entities || {})
if ( if (
wasSelectedSource === datasource._id || wasSelectedSource === datasource._id ||
@ -31,6 +32,9 @@
) { ) {
$goto("./datasource") $goto("./datasource")
} }
} catch (error) {
notifications.error("Error deleting datasource")
}
} }
</script> </script>

View File

@ -10,6 +10,7 @@
let confirmDeleteDialog let confirmDeleteDialog
async function deleteQuery() { async function deleteQuery() {
try {
const wasSelectedQuery = $queries.selected const wasSelectedQuery = $queries.selected
// need to calculate this before the query is deleted // need to calculate this before the query is deleted
const navigateToDatasource = wasSelectedQuery === query._id const navigateToDatasource = wasSelectedQuery === query._id
@ -22,14 +23,17 @@
$goto(`./datasource/${query.datasourceId}`) $goto(`./datasource/${query.datasourceId}`)
} }
notifications.success("Query deleted") notifications.success("Query deleted")
} catch (error) {
notifications.error("Error deleting query")
}
} }
async function duplicateQuery() { async function duplicateQuery() {
try { try {
const newQuery = await queries.duplicate(query) const newQuery = await queries.duplicate(query)
onClickQuery(newQuery) onClickQuery(newQuery)
} catch (e) { } catch (error) {
notifications.error(e.message) notifications.error("Error duplicating query")
} }
} }
</script> </script>

View File

@ -9,7 +9,7 @@
Body, Body,
} from "@budibase/bbui" } from "@budibase/bbui"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { uuid } from "builderStore/uuid" import { Helpers } from "@budibase/bbui"
import { writable } from "svelte/store" import { writable } from "svelte/store"
export let save export let save
@ -140,7 +140,7 @@
const manyToMany = const manyToMany =
fromRelationship.relationshipType === RelationshipTypes.MANY_TO_MANY fromRelationship.relationshipType === RelationshipTypes.MANY_TO_MANY
// main is simply used to know this is the side the user configured it from // main is simply used to know this is the side the user configured it from
const id = uuid() const id = Helpers.uuid()
if (!manyToMany) { if (!manyToMany) {
delete fromRelationship.through delete fromRelationship.through
delete toRelationship.through delete toRelationship.through

View File

@ -1,7 +1,7 @@
<script> <script>
import { Select, InlineAlert, notifications } from "@budibase/bbui" import { Select, InlineAlert, notifications } from "@budibase/bbui"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import api from "builderStore/api" import { API } from "api"
const BYTES_IN_MB = 1000000 const BYTES_IN_MB = 1000000
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5 const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
@ -50,14 +50,13 @@
} }
async function validateCSV() { async function validateCSV() {
const response = await api.post("/api/tables/csv/validate", { try {
const parseResult = await API.validateTableCSV({
csvString, csvString,
schema: schema || {}, schema: schema || {},
tableId: existingTableId, tableId: existingTableId,
}) })
schema = parseResult?.schema
const parseResult = await response.json()
schema = parseResult && parseResult.schema
fields = Object.keys(schema || {}).filter( fields = Object.keys(schema || {}).filter(
key => schema[key].type !== "omit" key => schema[key].type !== "omit"
) )
@ -67,11 +66,10 @@
primaryDisplay = fields[0] primaryDisplay = fields[0]
} }
if (response.status !== 200) {
notifications.error("CSV Invalid, please try another CSV file")
return []
}
hasValidated = true hasValidated = true
} catch (error) {
notifications.error("CSV Invalid, please try another CSV file")
}
} }
async function handleFile(evt) { async function handleFile(evt) {

View File

@ -49,8 +49,8 @@
if (wasSelectedTable && wasSelectedTable._id === table._id) { if (wasSelectedTable && wasSelectedTable._id === table._id) {
$goto("./table") $goto("./table")
} }
} catch (err) { } catch (error) {
notifications.error(err) notifications.error("Error deleting table")
} }
} }

View File

@ -27,11 +27,15 @@
} }
async function deleteView() { async function deleteView() {
try {
const name = view.name const name = view.name
const id = view.tableId const id = view.tableId
await views.delete(name) await views.delete(name)
notifications.success("View deleted") notifications.success("View deleted")
$goto(`./table/${id}`) $goto(`./table/${id}`)
} catch (error) {
notifications.error("Error deleting view")
}
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<script> <script>
import { Dropzone, notifications } from "@budibase/bbui" import { Dropzone, notifications } from "@budibase/bbui"
import api from "builderStore/api" import { API } from "api"
export let value = [] export let value = []
export let label export let label
@ -20,8 +20,12 @@
for (let i = 0; i < fileList.length; i++) { for (let i = 0; i < fileList.length; i++) {
data.append("file", fileList[i]) data.append("file", fileList[i])
} }
const response = await api.post(`/api/attachments/process`, data, {}) try {
return await response.json() return await API.uploadBuilderAttachment(data)
} catch (error) {
notifications.error("Failed to upload attachment")
return []
}
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<script> <script>
import { tables } from "stores/backend" import { tables } from "stores/backend"
import api from "builderStore/api" import { API } from "api"
import { Select, Label, Multiselect } from "@budibase/bbui" import { Select, Label, Multiselect } from "@budibase/bbui"
import { capitalise } from "../../helpers" import { capitalise } from "../../helpers"
@ -17,12 +17,9 @@
$: fetchRows(linkedTableId) $: fetchRows(linkedTableId)
async function fetchRows(linkedTableId) { async function fetchRows(linkedTableId) {
const FETCH_ROWS_URL = `/api/${linkedTableId}/rows`
try { try {
const response = await api.get(FETCH_ROWS_URL) rows = await API.fetchTableData(linkedTableId)
rows = await response.json()
} catch (error) { } catch (error) {
console.log(error)
rows = [] rows = []
} }
} }

View File

@ -1,6 +1,6 @@
<script> <script>
import { Button, Modal, notifications, ModalContent } from "@budibase/bbui" import { Button, Modal, notifications, ModalContent } from "@budibase/bbui"
import api from "builderStore/api" import { API } from "api"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
import { store } from "builderStore" import { store } from "builderStore"
@ -9,18 +9,14 @@
async function deployApp() { async function deployApp() {
try { try {
const response = await api.post("/api/deploy") await API.deployAppChanges()
if (response.status !== 200) {
throw new Error(`status ${response.status}`)
} else {
analytics.captureEvent(Events.APP.PUBLISHED, { analytics.captureEvent(Events.APP.PUBLISHED, {
appId: $store.appId, appId: $store.appId,
}) })
notifications.success(`Application published successfully`) notifications.success("Application published successfully")
} } catch (error) {
} catch (err) { analytics.captureException(error)
analytics.captureException(err) notifications.error("Error publishing app")
notifications.error(`Error publishing app: ${err}`)
} }
} }
</script> </script>

View File

@ -3,7 +3,7 @@
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import { slide } from "svelte/transition" import { slide } from "svelte/transition"
import { Heading, Button, Modal, ModalContent } from "@budibase/bbui" import { Heading, Button, Modal, ModalContent } from "@budibase/bbui"
import api from "builderStore/api" import { API } from "api"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte" import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
import { store } from "builderStore" import { store } from "builderStore"
@ -63,20 +63,14 @@
async function fetchDeployments() { async function fetchDeployments() {
try { try {
const response = await api.get(`/api/deployments`) const newDeployments = await API.getAppDeployments()
const json = await response.json()
if (deployments.length > 0) { if (deployments.length > 0) {
checkIncomingDeploymentStatus(deployments, json) checkIncomingDeploymentStatus(deployments, newDeployments)
} }
deployments = newDeployments
deployments = json
} catch (err) { } catch (err) {
console.error(err)
clearInterval(poll) clearInterval(poll)
notifications.error( notifications.error("Error fetching deployment history")
"Error fetching deployment history. Please try again."
)
} }
} }

View File

@ -7,7 +7,7 @@
ModalContent, ModalContent,
} from "@budibase/bbui" } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import api from "builderStore/api" import { API } from "api"
let revertModal let revertModal
let appName let appName
@ -16,24 +16,14 @@
const revert = async () => { const revert = async () => {
try { try {
const response = await api.post(`/api/dev/${appId}/revert`) await API.revertAppChanges(appId)
const json = await response.json()
if (response.status !== 200) throw json.message
// Reset frontend state after revert // Reset frontend state after revert
const applicationPkg = await api.get( const applicationPkg = await API.fetchAppPackage(appId)
`/api/applications/${appId}/appPackage` await store.actions.initialise(applicationPkg)
) notifications.info("Changes reverted successfully")
const pkg = await applicationPkg.json() } catch (error) {
if (applicationPkg.ok) { notifications.error(`Error reverting changes: ${error}`)
await store.actions.initialise(pkg)
} else {
throw new Error(pkg)
}
notifications.info("Changes reverted.")
} catch (err) {
notifications.error(`Error reverting changes: ${err}`)
} }
} }
</script> </script>

View File

@ -8,7 +8,7 @@
Button, Button,
} from "@budibase/bbui" } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import api from "builderStore/api" import { API } from "api"
import clientPackage from "@budibase/client/package.json" import clientPackage from "@budibase/client/package.json"
let updateModal let updateModal
@ -18,26 +18,17 @@
$: revertAvailable = $store.revertableVersion != null $: revertAvailable = $store.revertableVersion != null
const refreshAppPackage = async () => { const refreshAppPackage = async () => {
const applicationPkg = await api.get( try {
`/api/applications/${appId}/appPackage` const pkg = await API.fetchAppPackage(appId)
)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
await store.actions.initialise(pkg) await store.actions.initialise(pkg)
} else { } catch (error) {
throw new Error(pkg) notifications.error("Error fetching app package")
} }
} }
const update = async () => { const update = async () => {
try { try {
const response = await api.post( await API.updateAppClientVersion(appId)
`/api/applications/${appId}/client/update`
)
const json = await response.json()
if (response.status !== 200) {
throw json.message
}
// Don't wait for the async refresh, since this causes modal flashing // Don't wait for the async refresh, since this causes modal flashing
refreshAppPackage() refreshAppPackage()
@ -47,23 +38,17 @@
} catch (err) { } catch (err) {
notifications.error(`Error updating app: ${err}`) notifications.error(`Error updating app: ${err}`)
} }
updateModal.hide()
} }
const revert = async () => { const revert = async () => {
try { try {
const revertableVersion = $store.revertableVersion await API.revertAppClientVersion(appId)
const response = await api.post(
`/api/applications/${appId}/client/revert`
)
const json = await response.json()
if (response.status !== 200) {
throw json.message
}
// Don't wait for the async refresh, since this causes modal flashing // Don't wait for the async refresh, since this causes modal flashing
refreshAppPackage() refreshAppPackage()
notifications.success( notifications.success(
`App reverted successfully to version ${revertableVersion}` `App reverted successfully to version ${$store.revertableVersion}`
) )
} catch (err) { } catch (err) {
notifications.error(`Error reverting app: ${err}`) notifications.error(`Error reverting app: ${err}`)

View File

@ -1,5 +1,5 @@
<script> <script>
import { Select } from "@budibase/bbui" import { notifications, Select } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import { get } from "svelte/store" import { get } from "svelte/store"
@ -23,6 +23,7 @@
] ]
const onChangeTheme = async theme => { const onChangeTheme = async theme => {
try {
await store.actions.theme.save(theme) await store.actions.theme.save(theme)
await store.actions.customTheme.save({ await store.actions.customTheme.save({
...get(store).customTheme, ...get(store).customTheme,
@ -31,6 +32,9 @@
? "var(--spectrum-global-color-gray-50)" ? "var(--spectrum-global-color-gray-50)"
: "var(--spectrum-global-color-gray-100)", : "var(--spectrum-global-color-gray-100)",
}) })
} catch (error) {
notifications.error("Error updating theme")
}
} }
</script> </script>

View File

@ -1,5 +1,11 @@
<script> <script>
import { ActionMenu, ActionButton, MenuItem, Icon } from "@budibase/bbui" import {
ActionMenu,
ActionButton,
MenuItem,
Icon,
notifications,
} from "@budibase/bbui"
import { store, currentAssetName, selectedComponent } from "builderStore" import { store, currentAssetName, selectedComponent } from "builderStore"
import structure from "./componentStructure.json" import structure from "./componentStructure.json"
@ -36,7 +42,11 @@
const onItemChosen = async item => { const onItemChosen = async item => {
if (!item.isCategory) { if (!item.isCategory) {
try {
await store.actions.components.create(item.component) await store.actions.components.create(item.component)
} catch (error) {
notifications.error("Error creating component")
}
} }
} }
</script> </script>

View File

@ -13,7 +13,7 @@
Body, Body,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import ErrorSVG from "assets/error.svg?raw" import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
import { findComponent, findComponentPath } from "builderStore/componentUtils" import { findComponent, findComponentPath } from "builderStore/componentUtils"
let iframe let iframe
@ -146,16 +146,17 @@
} }
}) })
const handleBudibaseEvent = event => { const handleBudibaseEvent = async event => {
const { type, data } = event.data || event.detail const { type, data } = event.data || event.detail
if (!type) { if (!type) {
return return
} }
try {
if (type === "select-component" && data.id) { if (type === "select-component" && data.id) {
store.actions.components.select({ _id: data.id }) store.actions.components.select({ _id: data.id })
} else if (type === "update-prop") { } else if (type === "update-prop") {
store.actions.components.updateProp(data.prop, data.value) await store.actions.components.updateProp(data.prop, data.value)
} else if (type === "delete-component" && data.id) { } else if (type === "delete-component" && data.id) {
confirmDeleteComponent(data.id) confirmDeleteComponent(data.id)
} else if (type === "preview-loaded") { } else if (type === "preview-loaded") {
@ -180,11 +181,15 @@
// Cut and paste the component to the new destination // Cut and paste the component to the new destination
if (source && destination) { if (source && destination) {
store.actions.components.copy(source, true) store.actions.components.copy(source, true)
store.actions.components.paste(destination, data.mode) await store.actions.components.paste(destination, data.mode)
} }
} else { } else {
console.warn(`Client sent unknown event type: ${type}`) console.warn(`Client sent unknown event type: ${type}`)
} }
} catch (error) {
console.warn(error)
notifications.error("Error handling event from app preview")
}
} }
const confirmDeleteComponent = componentId => { const confirmDeleteComponent = componentId => {
@ -196,7 +201,7 @@
try { try {
await store.actions.components.delete({ _id: idToDelete }) await store.actions.components.delete({ _id: idToDelete })
} catch (error) { } catch (error) {
notifications.error(error) notifications.error("Error deleting component")
} }
idToDelete = null idToDelete = null
} }

View File

@ -9,6 +9,7 @@
Label, Label,
Select, Select,
Button, Button,
notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import AppThemeSelect from "./AppThemeSelect.svelte" import AppThemeSelect from "./AppThemeSelect.svelte"
@ -43,15 +44,20 @@
] ]
const updateProperty = property => { const updateProperty = property => {
return e => { return async e => {
try {
store.actions.customTheme.save({ store.actions.customTheme.save({
...get(store).customTheme, ...get(store).customTheme,
[property]: e.detail, [property]: e.detail,
}) })
} catch (error) {
notifications.error("Error updating custom theme")
}
} }
} }
const resetTheme = () => { const resetTheme = () => {
try {
const theme = get(store).theme const theme = get(store).theme
store.actions.customTheme.save({ store.actions.customTheme.save({
...defaultTheme, ...defaultTheme,
@ -60,6 +66,9 @@
? "var(--spectrum-global-color-gray-50)" ? "var(--spectrum-global-color-gray-50)"
: "var(--spectrum-global-color-gray-100)", : "var(--spectrum-global-color-gray-100)",
}) })
} catch (error) {
notifications.error("Error saving custom theme")
}
} }
</script> </script>

View File

@ -29,10 +29,14 @@
if (currentIndex === 0) { if (currentIndex === 0) {
return return
} }
try {
const newChildren = parent._children.filter(c => c !== component) const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex - 1, 0, component) newChildren.splice(currentIndex - 1, 0, component)
parent._children = newChildren parent._children = newChildren
store.actions.preview.saveSelected() store.actions.preview.saveSelected()
} catch (error) {
notifications.error("Error saving screen")
}
} }
const moveDownComponent = () => { const moveDownComponent = () => {
@ -45,10 +49,14 @@
if (currentIndex === parent._children.length - 1) { if (currentIndex === parent._children.length - 1) {
return return
} }
try {
const newChildren = parent._children.filter(c => c !== component) const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex + 1, 0, component) newChildren.splice(currentIndex + 1, 0, component)
parent._children = newChildren parent._children = newChildren
store.actions.preview.saveSelected() store.actions.preview.saveSelected()
} catch (error) {
notifications.error("Error saving screen")
}
} }
const duplicateComponent = () => { const duplicateComponent = () => {
@ -60,7 +68,7 @@
try { try {
await store.actions.components.delete(component) await store.actions.components.delete(component)
} catch (error) { } catch (error) {
notifications.error(error) notifications.error("Error deleting component")
} }
} }
@ -70,8 +78,12 @@
} }
const pasteComponent = (mode, preserveBindings = false) => { const pasteComponent = (mode, preserveBindings = false) => {
try {
// lives in store - also used by drag drop // lives in store - also used by drag drop
store.actions.components.paste(component, mode, preserveBindings) store.actions.components.paste(component, mode, preserveBindings)
} catch (error) {
notifications.error("Error saving component")
}
} }
</script> </script>

View File

@ -4,6 +4,7 @@
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte" import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import { notifications } from "@budibase/bbui"
export let components = [] export let components = []
export let currentComponent export let currentComponent
@ -62,6 +63,14 @@
} }
closedNodes = closedNodes closedNodes = closedNodes
} }
const onDrop = async () => {
try {
await dragDropStore.actions.drop()
} catch (error) {
notifications.error("Error saving component")
}
}
</script> </script>
<ul> <ul>
@ -69,7 +78,7 @@
<li on:click|stopPropagation={() => selectComponent(component)}> <li on:click|stopPropagation={() => selectComponent(component)}>
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE} {#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
<div <div
on:drop={dragDropStore.actions.drop} on:drop={onDrop}
ondragover="return false" ondragover="return false"
ondragenter="return false" ondragenter="return false"
class="drop-item" class="drop-item"
@ -83,7 +92,7 @@
on:dragstart={dragstart(component)} on:dragstart={dragstart(component)}
on:dragover={dragover(component, index)} on:dragover={dragover(component, index)}
on:iconClick={() => toggleNodeOpen(component._id)} on:iconClick={() => toggleNodeOpen(component._id)}
on:drop={dragDropStore.actions.drop} on:drop={onDrop}
text={getComponentText(component)} text={getComponentText(component)}
withArrow withArrow
indentLevel={level + 1} indentLevel={level + 1}
@ -105,7 +114,7 @@
{#if $dragDropStore?.targetComponent === component && ($dragDropStore.dropPosition === DropPosition.INSIDE || $dragDropStore.dropPosition === DropPosition.BELOW)} {#if $dragDropStore?.targetComponent === component && ($dragDropStore.dropPosition === DropPosition.INSIDE || $dragDropStore.dropPosition === DropPosition.BELOW)}
<div <div
on:drop={dragDropStore.actions.drop} on:drop={onDrop}
ondragover="return false" ondragover="return false"
ondragenter="return false" ondragenter="return false"
class="drop-item" class="drop-item"

View File

@ -21,9 +21,9 @@
const deleteLayout = async () => { const deleteLayout = async () => {
try { try {
await store.actions.layouts.delete(layout) await store.actions.layouts.delete(layout)
notifications.success(`Layout ${layout.name} deleted successfully.`) notifications.success("Layout deleted successfully")
} catch (err) { } catch (err) {
notifications.error(`Error deleting layout: ${err.message}`) notifications.error("Error deleting layout")
} }
} }
@ -32,9 +32,9 @@
const layoutToSave = cloneDeep(layout) const layoutToSave = cloneDeep(layout)
layoutToSave.name = name layoutToSave.name = name
await store.actions.layouts.save(layoutToSave) await store.actions.layouts.save(layoutToSave)
notifications.success(`Layout saved successfully.`) notifications.success("Layout saved successfully")
} catch (err) { } catch (err) {
notifications.error(`Error saving layout: ${err.message}`) notifications.error("Error saving layout")
} }
} }
</script> </script>

View File

@ -13,7 +13,6 @@
const deleteScreen = async () => { const deleteScreen = async () => {
try { try {
await store.actions.screens.delete(screen) await store.actions.screens.delete(screen)
await store.actions.routing.fetch()
$goto("../") $goto("../")
notifications.success("Deleted screen successfully.") notifications.success("Deleted screen successfully.")
} catch (err) { } catch (err) {

View File

@ -72,7 +72,7 @@ export default function () {
return state return state
}) })
}, },
drop: () => { drop: async () => {
const state = get(store) const state = get(store)
// Stop if the target and source are the same // Stop if the target and source are the same
@ -92,7 +92,7 @@ export default function () {
// Cut and paste the component // Cut and paste the component
frontendStore.actions.components.copy(state.dragged, true) frontendStore.actions.components.copy(state.dragged, true)
frontendStore.actions.components.paste( await frontendStore.actions.components.paste(
state.targetComponent, state.targetComponent,
state.dropPosition state.dropPosition
) )

View File

@ -11,7 +11,15 @@
import ComponentNavigationTree from "components/design/NavigationPanel/ComponentNavigationTree/index.svelte" import ComponentNavigationTree from "components/design/NavigationPanel/ComponentNavigationTree/index.svelte"
import Layout from "components/design/NavigationPanel/Layout.svelte" import Layout from "components/design/NavigationPanel/Layout.svelte"
import NewLayoutModal from "components/design/NavigationPanel/NewLayoutModal.svelte" import NewLayoutModal from "components/design/NavigationPanel/NewLayoutModal.svelte"
import { Icon, Modal, Select, Search, Tabs, Tab } from "@budibase/bbui" import {
Icon,
Modal,
Select,
Search,
Tabs,
Tab,
notifications,
} from "@budibase/bbui"
export let showModal export let showModal
@ -58,8 +66,12 @@
selectedAccessRole.set(role) selectedAccessRole.set(role)
} }
onMount(() => { onMount(async () => {
store.actions.routing.fetch() try {
await store.actions.routing.fetch()
} catch (error) {
notifications.error("Error fetching routes")
}
}) })
</script> </script>

View File

@ -9,8 +9,8 @@
try { try {
await store.actions.layouts.save({ name }) await store.actions.layouts.save({ name })
notifications.success(`Layout ${name} created successfully`) notifications.success(`Layout ${name} created successfully`)
} catch (err) { } catch (error) {
notifications.error(`Error creating layout ${name}.`) notifications.error("Error creating layout")
} }
} }
</script> </script>

View File

@ -2,7 +2,7 @@
import ScreenDetailsModal from "components/design/NavigationPanel/ScreenDetailsModal.svelte" import ScreenDetailsModal from "components/design/NavigationPanel/ScreenDetailsModal.svelte"
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte" import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { Modal } from "@budibase/bbui" import { Modal, notifications } from "@budibase/bbui"
import { store, selectedAccessRole, allScreens } from "builderStore" import { store, selectedAccessRole, allScreens } from "builderStore"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
@ -29,6 +29,7 @@
const save = async () => { const save = async () => {
showProgressCircle = true showProgressCircle = true
try {
await createScreens() await createScreens()
for (let screen of createdScreens) { for (let screen of createdScreens) {
await saveScreens(screen) await saveScreens(screen)
@ -38,6 +39,9 @@
createdScreens = [] createdScreens = []
screenName = "" screenName = ""
url = "" url = ""
} catch (error) {
notifications.error("Error creating screens")
}
showProgressCircle = false showProgressCircle = false
} }
@ -71,12 +75,16 @@
draftScreen.routing.route = route draftScreen.routing.route = route
await store.actions.screens.create(draftScreen) await store.actions.screens.save(draftScreen)
if (draftScreen.props._instanceName.endsWith("List")) { if (draftScreen.props._instanceName.endsWith("List")) {
try {
await store.actions.components.links.save( await store.actions.components.links.save(
draftScreen.routing.route, draftScreen.routing.route,
draftScreen.routing.route.split("/")[1] draftScreen.routing.route.split("/")[1]
) )
} catch (error) {
notifications.error("Error creating link to screen")
}
} }
} }
} }

View File

@ -1,6 +1,6 @@
<script> <script>
import { isEmpty } from "lodash/fp" import { isEmpty } from "lodash/fp"
import { Input, DetailSummary } from "@budibase/bbui" import { Input, DetailSummary, notifications } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import PropertyControl from "./PropertyControls/PropertyControl.svelte" import PropertyControl from "./PropertyControls/PropertyControl.svelte"
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte" import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
@ -40,7 +40,13 @@
] ]
} }
const updateProp = store.actions.components.updateProp const updateProp = async (key, value) => {
try {
await store.actions.components.updateProp(key, value)
} catch (error) {
notifications.error("Error updating component prop")
}
}
const canRenderControl = setting => { const canRenderControl = setting => {
const control = getComponentForSettingType(setting?.type) const control = getComponentForSettingType(setting?.type)

View File

@ -1,5 +1,11 @@
<script> <script>
import { DetailSummary, ActionButton, Drawer, Button } from "@budibase/bbui" import {
DetailSummary,
ActionButton,
Drawer,
Button,
notifications,
} from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import ConditionalUIDrawer from "./PropertyControls/ConditionalUIDrawer.svelte" import ConditionalUIDrawer from "./PropertyControls/ConditionalUIDrawer.svelte"
@ -14,8 +20,12 @@
drawer.show() drawer.show()
} }
const save = () => { const save = async () => {
store.actions.components.updateConditions(tempValue) try {
await store.actions.components.updateConditions(tempValue)
} catch (error) {
notifications.error("Error updating conditions")
}
drawer.hide() drawer.hide()
} }
</script> </script>

View File

@ -8,6 +8,7 @@
Layout, Layout,
Body, Body,
Button, Button,
notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
@ -21,8 +22,12 @@
drawer.show() drawer.show()
} }
const save = () => { const save = async () => {
store.actions.components.updateCustomStyle(tempValue) try {
await store.actions.components.updateCustomStyle(tempValue)
} catch (error) {
notifications.error("Error updating custom style")
}
drawer.hide() drawer.hide()
} }
</script> </script>

View File

@ -36,8 +36,13 @@
// called by the parent modal when actions are saved // called by the parent modal when actions are saved
const createAutomation = async parameters => { const createAutomation = async parameters => {
if (parameters.automationId || !parameters.newAutomationName) return if (parameters.automationId || !parameters.newAutomationName) {
await automationStore.actions.create({ name: parameters.newAutomationName }) return
}
try {
await automationStore.actions.create({
name: parameters.newAutomationName,
})
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
const newBlock = $automationStore.selectedAutomation.constructBlock( const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER", "TRIGGER",
@ -56,8 +61,12 @@
await automationStore.actions.save( await automationStore.actions.save(
$automationStore.selectedAutomation?.automation $automationStore.selectedAutomation?.automation
) )
parameters.automationId = $automationStore.selectedAutomation.automation._id parameters.automationId =
$automationStore.selectedAutomation.automation._id
delete parameters.newAutomationName delete parameters.newAutomationName
} catch (error) {
notifications.error("Error creating automation")
}
} }
</script> </script>

View File

@ -12,7 +12,7 @@
import { dndzone } from "svelte-dnd-action" import { dndzone } from "svelte-dnd-action"
import { generate } from "shortid" import { generate } from "shortid"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { OperatorOptions, getValidOperatorsForType } from "constants/lucene" import { LuceneUtils, Constants } from "@budibase/frontend-core"
import { selectedComponent } from "builderStore" import { selectedComponent } from "builderStore"
import { getComponentForSettingType } from "./componentSettings" import { getComponentForSettingType } from "./componentSettings"
import PropertyControl from "./PropertyControl.svelte" import PropertyControl from "./PropertyControl.svelte"
@ -83,7 +83,7 @@
valueType: "string", valueType: "string",
id: generate(), id: generate(),
action: "hide", action: "hide",
operator: OperatorOptions.Equals.value, operator: Constants.OperatorOptions.Equals.value,
}, },
] ]
} }
@ -108,13 +108,13 @@
} }
const getOperatorOptions = condition => { const getOperatorOptions = condition => {
return getValidOperatorsForType(condition.valueType) return LuceneUtils.getValidOperatorsForType(condition.valueType)
} }
const onOperatorChange = (condition, newOperator) => { const onOperatorChange = (condition, newOperator) => {
const noValueOptions = [ const noValueOptions = [
OperatorOptions.Empty.value, Constants.OperatorOptions.Empty.value,
OperatorOptions.NotEmpty.value, Constants.OperatorOptions.NotEmpty.value,
] ]
condition.noValue = noValueOptions.includes(newOperator) condition.noValue = noValueOptions.includes(newOperator)
if (condition.noValue) { if (condition.noValue) {
@ -127,9 +127,12 @@
condition.referenceValue = null condition.referenceValue = null
// Ensure a valid operator is set // Ensure a valid operator is set
const validOperators = getValidOperatorsForType(newType).map(x => x.value) const validOperators = LuceneUtils.getValidOperatorsForType(newType).map(
x => x.value
)
if (!validOperators.includes(condition.operator)) { if (!validOperators.includes(condition.operator)) {
condition.operator = validOperators[0] ?? OperatorOptions.Equals.value condition.operator =
validOperators[0] ?? Constants.OperatorOptions.Equals.value
onOperatorChange(condition, condition.operator) onOperatorChange(condition, condition.operator)
} }
} }

View File

@ -13,7 +13,7 @@
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { generate } from "shortid" import { generate } from "shortid"
import { getValidOperatorsForType, OperatorOptions } from "constants/lucene" import { LuceneUtils, Constants } from "@budibase/frontend-core"
import { getFields } from "helpers/searchFields" import { getFields } from "helpers/searchFields"
export let schemaFields export let schemaFields
@ -32,7 +32,7 @@
{ {
id: generate(), id: generate(),
field: null, field: null,
operator: OperatorOptions.Equals.value, operator: Constants.OperatorOptions.Equals.value,
value: null, value: null,
valueType: "Value", valueType: "Value",
}, },
@ -54,11 +54,12 @@
expression.type = enrichedSchemaFields.find(x => x.name === field)?.type expression.type = enrichedSchemaFields.find(x => x.name === field)?.type
// Ensure a valid operator is set // Ensure a valid operator is set
const validOperators = getValidOperatorsForType(expression.type).map( const validOperators = LuceneUtils.getValidOperatorsForType(
x => x.value expression.type
) ).map(x => x.value)
if (!validOperators.includes(expression.operator)) { if (!validOperators.includes(expression.operator)) {
expression.operator = validOperators[0] ?? OperatorOptions.Equals.value expression.operator =
validOperators[0] ?? Constants.OperatorOptions.Equals.value
onOperatorChange(expression, expression.operator) onOperatorChange(expression, expression.operator)
} }
@ -73,8 +74,8 @@
const onOperatorChange = (expression, operator) => { const onOperatorChange = (expression, operator) => {
const noValueOptions = [ const noValueOptions = [
OperatorOptions.Empty.value, Constants.OperatorOptions.Empty.value,
OperatorOptions.NotEmpty.value, Constants.OperatorOptions.NotEmpty.value,
] ]
expression.noValue = noValueOptions.includes(operator) expression.noValue = noValueOptions.includes(operator)
if (expression.noValue) { if (expression.noValue) {
@ -110,7 +111,7 @@
/> />
<Select <Select
disabled={!filter.field} disabled={!filter.field}
options={getValidOperatorsForType(filter.type)} options={LuceneUtils.getValidOperatorsForType(filter.type)}
bind:value={filter.operator} bind:value={filter.operator}
on:change={e => onOperatorChange(filter, e.detail)} on:change={e => onOperatorChange(filter, e.detail)}
placeholder={null} placeholder={null}

View File

@ -1,5 +1,5 @@
<script> <script>
import { ActionButton } from "@budibase/bbui" import { ActionButton, notifications } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { findClosestMatchingComponent } from "builderStore/componentUtils" import { findClosestMatchingComponent } from "builderStore/componentUtils"
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents" import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents"
@ -9,7 +9,7 @@
let confirmResetFieldsDialog let confirmResetFieldsDialog
const resetFormFields = () => { const resetFormFields = async () => {
const form = findClosestMatchingComponent( const form = findClosestMatchingComponent(
$currentAsset.props, $currentAsset.props,
componentInstance._id, componentInstance._id,
@ -17,10 +17,14 @@
) )
const dataSource = form?.dataSource const dataSource = form?.dataSource
const fields = makeDatasourceFormComponents(dataSource) const fields = makeDatasourceFormComponents(dataSource)
store.actions.components.updateProp( try {
await store.actions.components.updateProp(
"_children", "_children",
fields.map(field => field.json()) fields.map(field => field.json())
) )
} catch (error) {
notifications.error("Error resetting form fields")
}
} }
</script> </script>

View File

@ -1,7 +1,7 @@
<script> <script>
import { get } from "svelte/store" import { get } from "svelte/store"
import { get as deepGet, setWith } from "lodash" import { get as deepGet, setWith } from "lodash"
import { Input, DetailSummary } from "@budibase/bbui" import { Input, DetailSummary, notifications } from "@budibase/bbui"
import PropertyControl from "./PropertyControls/PropertyControl.svelte" import PropertyControl from "./PropertyControls/PropertyControl.svelte"
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte" import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
import RoleSelect from "./PropertyControls/RoleSelect.svelte" import RoleSelect from "./PropertyControls/RoleSelect.svelte"
@ -29,7 +29,12 @@
} }
return state return state
}) })
try {
store.actions.preview.saveSelected() store.actions.preview.saveSelected()
} catch (error) {
notifications.error("Error saving settings")
}
} }
const screenSettings = [ const screenSettings = [

View File

@ -1,6 +1,6 @@
<script> <script>
import PropertyControl from "./PropertyControls/PropertyControl.svelte" import PropertyControl from "./PropertyControls/PropertyControl.svelte"
import { DetailSummary } from "@budibase/bbui" import { DetailSummary, notifications } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
export let name export let name
@ -23,6 +23,14 @@
delete controlProps.control delete controlProps.control
return controlProps return controlProps
} }
const updateStyle = async (key, val) => {
try {
await store.actions.components.updateStyle(key, val)
} catch (error) {
notifications.error("Error updating style")
}
}
</script> </script>
<DetailSummary collapsible={false} name={`${name}${changed ? " *" : ""}`}> <DetailSummary collapsible={false} name={`${name}${changed ? " *" : ""}`}>
@ -34,7 +42,7 @@
control={prop.control} control={prop.control}
key={prop.key} key={prop.key}
value={style[prop.key]} value={style[prop.key]}
onChange={val => store.actions.components.updateStyle(prop.key, val)} onChange={val => updateStyle(prop.key, val)}
props={getControlProps(prop)} props={getControlProps(prop)}
{bindings} {bindings}
/> />

View File

@ -13,6 +13,7 @@
Detail, Detail,
Divider, Divider,
Layout, Layout,
notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { auth } from "stores/portal" import { auth } from "stores/portal"
@ -45,20 +46,28 @@
improvements, improvements,
comment, comment,
}) })
try {
auth.updateSelf({ auth.updateSelf({
flags: { flags: {
feedbackSubmitted: true, feedbackSubmitted: true,
}, },
}) })
} catch (error) {
notifications.error("Error updating user")
}
dispatch("complete") dispatch("complete")
} }
function cancelFeedback() { function cancelFeedback() {
try {
auth.updateSelf({ auth.updateSelf({
flags: { flags: {
feedbackSubmitted: true, feedbackSubmitted: true,
}, },
}) })
} catch (error) {
notifications.error("Error updating user")
}
dispatch("complete") dispatch("complete")
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<script> <script>
import { Label, Select } from "@budibase/bbui" import { Label, notifications, Select } from "@budibase/bbui"
import { permissions, roles } from "stores/backend" import { permissions, roles } from "stores/backend"
import { Roles } from "constants/backend" import { Roles } from "constants/backend"
@ -11,6 +11,7 @@
let roleId, loaded, fetched let roleId, loaded, fetched
async function updateRole(role, id) { async function updateRole(role, id) {
try {
roleId = role roleId = role
const queryId = query?._id || id const queryId = query?._id || id
if (roleId && queryId) { if (roleId && queryId) {
@ -22,6 +23,9 @@
}) })
} }
} }
} catch (error) {
notifications.error("Error updating role")
}
} }
async function getPermissions(queryToFetch) { async function getPermissions(queryToFetch) {

View File

@ -71,9 +71,9 @@
} }
data = response.rows data = response.rows
fields = response.schema fields = response.schema
notifications.success("Query executed successfully.") notifications.success("Query executed successfully")
} catch (err) { } catch (error) {
notifications.error(err) notifications.error("Error previewing query")
} }
} }
@ -83,9 +83,8 @@
saveId = _id saveId = _id
notifications.success(`Query saved successfully.`) notifications.success(`Query saved successfully.`)
$goto(`../${_id}`) $goto(`../${_id}`)
} catch (err) { } catch (error) {
console.error(err) notifications.error("Error creating query")
notifications.error(`Error creating query. ${err.message}`)
} }
} }
</script> </script>

View File

@ -1,5 +1,12 @@
<script> <script>
import { ModalContent, Modal, Icon, ColorPicker, Label } from "@budibase/bbui" import {
ModalContent,
Modal,
Icon,
ColorPicker,
Label,
notifications,
} from "@budibase/bbui"
import { apps } from "stores/portal" import { apps } from "stores/portal"
export let app export let app
@ -51,12 +58,16 @@
} }
const save = async () => { const save = async () => {
try {
await apps.update(app.instance._id, { await apps.update(app.instance._id, {
icon: { icon: {
name: selectedIcon, name: selectedIcon,
color: selectedColor, color: selectedColor,
}, },
}) })
} catch (error) {
notifications.error("Error updating app")
}
} }
</script> </script>

View File

@ -2,8 +2,8 @@
import { writable, get as svelteGet } from "svelte/store" import { writable, get as svelteGet } from "svelte/store"
import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui" import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
import { store, automationStore } from "builderStore" import { store, automationStore } from "builderStore"
import { API } from "api"
import { apps, admin, auth } from "stores/portal" import { apps, admin, auth } from "stores/portal"
import api, { get, post } from "builderStore/api"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
import { onMount } from "svelte" import { onMount } from "svelte"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
@ -45,43 +45,27 @@
} }
// Create App // Create App
const appResp = await post("/api/applications", data, {}) const createdApp = await API.createApp(data)
const appJson = await appResp.json()
if (!appResp.ok) {
throw new Error(appJson.message)
}
analytics.captureEvent(Events.APP.CREATED, { analytics.captureEvent(Events.APP.CREATED, {
name: $values.name, name: $values.name,
appId: appJson.instance._id, appId: createdApp.instance._id,
templateToUse: template, templateToUse: template,
}) })
// Select Correct Application/DB in prep for creating user // Select Correct Application/DB in prep for creating user
const applicationPkg = await get( const pkg = await API.fetchAppPackage(createdApp.instance._id)
`/api/applications/${appJson.instance._id}/appPackage`
)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
await store.actions.initialise(pkg) await store.actions.initialise(pkg)
await automationStore.actions.fetch() await automationStore.actions.fetch()
// update checklist - incase first app // Update checklist - in case first app
await admin.init() await admin.init()
} else {
throw new Error(pkg)
}
// Create user // Create user
const user = { await API.updateOwnMetadata({ roleId: $values.roleId })
roleId: $values.roleId,
}
const userResp = await api.post(`/api/users/metadata/self`, user)
await userResp.json()
await auth.setInitInfo({}) await auth.setInitInfo({})
$goto(`/builder/app/${appJson.instance._id}`) $goto(`/builder/app/${createdApp.instance._id}`)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
notifications.error(error) notifications.error("Error creating app")
} }
} }

View File

@ -38,7 +38,7 @@
await apps.update(app.instance._id, body) await apps.update(app.instance._id, body)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
notifications.error(error) notifications.error("Error updating app")
} }
} }

View File

@ -1,96 +0,0 @@
/**
* Operator options for lucene queries
*/
export const OperatorOptions = {
Equals: {
value: "equal",
label: "Equals",
},
NotEquals: {
value: "notEqual",
label: "Not equals",
},
Empty: {
value: "empty",
label: "Is empty",
},
NotEmpty: {
value: "notEmpty",
label: "Is not empty",
},
StartsWith: {
value: "string",
label: "Starts with",
},
Like: {
value: "fuzzy",
label: "Like",
},
MoreThan: {
value: "rangeLow",
label: "More than",
},
LessThan: {
value: "rangeHigh",
label: "Less than",
},
Contains: {
value: "equal",
label: "Contains",
},
NotContains: {
value: "notEqual",
label: "Does Not Contain",
},
}
export const NoEmptyFilterStrings = [
OperatorOptions.StartsWith.value,
OperatorOptions.Like.value,
OperatorOptions.Equals.value,
OperatorOptions.NotEquals.value,
OperatorOptions.Contains.value,
OperatorOptions.NotContains.value,
]
/**
* Returns the valid operator options for a certain data type
* @param type the data type
*/
export const getValidOperatorsForType = type => {
const Op = OperatorOptions
const stringOps = [
Op.Equals,
Op.NotEquals,
Op.StartsWith,
Op.Like,
Op.Empty,
Op.NotEmpty,
]
const numOps = [
Op.Equals,
Op.NotEquals,
Op.MoreThan,
Op.LessThan,
Op.Empty,
Op.NotEmpty,
]
if (type === "string") {
return stringOps
} else if (type === "number") {
return numOps
} else if (type === "options") {
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
} else if (type === "array") {
return [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty]
} else if (type === "boolean") {
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
} else if (type === "longform") {
return stringOps
} else if (type === "datetime") {
return numOps
} else if (type === "formula") {
return stringOps.concat([Op.MoreThan, Op.LessThan])
}
return []
}

View File

@ -1,5 +1,5 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import api from "builderStore/api" import { API } from "api"
export default function (url) { export default function (url) {
const store = writable({ status: "LOADING", data: {}, error: {} }) const store = writable({ status: "LOADING", data: {}, error: {} })
@ -7,8 +7,8 @@ export default function (url) {
async function get() { async function get() {
store.update(u => ({ ...u, status: "LOADING" })) store.update(u => ({ ...u, status: "LOADING" }))
try { try {
const response = await api.get(url) const data = await API.get({ url })
store.set({ data: await response.json(), status: "SUCCESS" }) store.set({ data, status: "SUCCESS" })
} catch (e) { } catch (e) {
store.set({ data: {}, error: e, status: "ERROR" }) store.set({ data: {}, error: e, status: "ERROR" })
} }

View File

@ -1,210 +0,0 @@
// Do not use any aliased imports in common files, as these will be bundled
// by multiple bundlers which may not be able to resolve them.
// This will eventually be replaced by the new client implementation when we
// add a core package.
import { writable, derived, get } from "svelte/store"
import * as API from "../builderStore/api"
import { buildLuceneQuery } from "./lucene"
const defaultOptions = {
tableId: null,
filters: null,
limit: 10,
sortColumn: null,
sortOrder: "ascending",
paginate: true,
schema: null,
}
export const fetchTableData = opts => {
// Save option set so we can override it later rather than relying on params
let options = {
...defaultOptions,
...opts,
}
// Local non-observable state
let query
let sortType
let lastBookmark
// Local observable state
const store = writable({
rows: [],
schema: null,
loading: false,
loaded: false,
bookmarks: [],
pageNumber: 0,
})
// Derive certain properties to return
const derivedStore = derived(store, $store => {
return {
...$store,
hasNextPage: $store.bookmarks[$store.pageNumber + 1] != null,
hasPrevPage: $store.pageNumber > 0,
}
})
const fetchPage = async bookmark => {
lastBookmark = bookmark
const { tableId, limit, sortColumn, sortOrder, paginate } = options
const res = await API.post(`/api/${options.tableId}/search`, {
tableId,
query,
limit,
sort: sortColumn,
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
sortType,
paginate,
bookmark,
})
return await res.json()
}
// Fetches a fresh set of results from the server
const fetchData = async () => {
const { tableId, schema, sortColumn, filters } = options
// Ensure table ID exists
if (!tableId) {
return
}
// Get and enrich schema.
// Ensure there are "name" properties for all fields and that field schema
// are objects
let enrichedSchema = schema
if (!enrichedSchema) {
const definition = await API.get(`/api/tables/${tableId}`)
enrichedSchema = definition?.schema ?? null
}
if (enrichedSchema) {
Object.entries(schema).forEach(([fieldName, fieldSchema]) => {
if (typeof fieldSchema === "string") {
enrichedSchema[fieldName] = {
type: fieldSchema,
name: fieldName,
}
} else {
enrichedSchema[fieldName] = {
...fieldSchema,
name: fieldName,
}
}
})
// Save fixed schema so we can provide it later
options.schema = enrichedSchema
}
// Ensure schema exists
if (!schema) {
return
}
store.update($store => ({ ...$store, schema, loading: true }))
// Work out what sort type to use
if (!sortColumn || !schema[sortColumn]) {
sortType = "string"
}
const type = schema?.[sortColumn]?.type
sortType = type === "number" ? "number" : "string"
// Build the lucene query
query = buildLuceneQuery(filters)
// Actually fetch data
const page = await fetchPage()
store.update($store => ({
...$store,
loading: false,
loaded: true,
pageNumber: 0,
rows: page.rows,
bookmarks: page.hasNextPage ? [null, page.bookmark] : [null],
}))
}
// Fetches the next page of data
const nextPage = async () => {
const state = get(derivedStore)
if (state.loading || !options.paginate || !state.hasNextPage) {
return
}
// Fetch next page
store.update($store => ({ ...$store, loading: true }))
const page = await fetchPage(state.bookmarks[state.pageNumber + 1])
// Update state
store.update($store => {
let { bookmarks, pageNumber } = $store
if (page.hasNextPage) {
bookmarks[pageNumber + 2] = page.bookmark
}
return {
...$store,
pageNumber: pageNumber + 1,
rows: page.rows,
bookmarks,
loading: false,
}
})
}
// Fetches the previous page of data
const prevPage = async () => {
const state = get(derivedStore)
if (state.loading || !options.paginate || !state.hasPrevPage) {
return
}
// Fetch previous page
store.update($store => ({ ...$store, loading: true }))
const page = await fetchPage(state.bookmarks[state.pageNumber - 1])
// Update state
store.update($store => {
return {
...$store,
pageNumber: $store.pageNumber - 1,
rows: page.rows,
loading: false,
}
})
}
// Resets the data set and updates options
const update = async newOptions => {
if (newOptions) {
options = {
...options,
...newOptions,
}
}
await fetchData()
}
// Loads the same page again
const refresh = async () => {
if (get(store).loading) {
return
}
const page = await fetchPage(lastBookmark)
store.update($store => ({ ...$store, rows: page.rows }))
}
// Initially fetch data but don't bother waiting for the result
fetchData()
// Return our derived store which will be updated over time
return {
subscribe: derivedStore.subscribe,
nextPage,
prevPage,
update,
refresh,
}
}

View File

@ -2,12 +2,7 @@
import { isActive, redirect, params } from "@roxi/routify" import { isActive, redirect, params } from "@roxi/routify"
import { admin, auth } from "stores/portal" import { admin, auth } from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import { import { CookieUtils, Constants } from "@budibase/frontend-core"
Cookies,
getCookie,
removeCookie,
setCookie,
} from "builderStore/cookies"
let loaded = false let loaded = false
@ -46,9 +41,12 @@
if (user.tenantId !== urlTenantId) { if (user.tenantId !== urlTenantId) {
// user should not be here - play it safe and log them out // user should not be here - play it safe and log them out
try {
await auth.logout() await auth.logout()
await auth.setOrganisation(null) await auth.setOrganisation(null)
return } catch (error) {
// Swallow error and do nothing
}
} }
} else { } else {
// no user - set the org according to the url // no user - set the org according to the url
@ -57,17 +55,23 @@
} }
onMount(async () => { onMount(async () => {
try {
await auth.getSelf()
await admin.init()
// Set init info if present
if ($params["?template"]) { if ($params["?template"]) {
await auth.setInitInfo({ init_template: $params["?template"] }) await auth.setInitInfo({ init_template: $params["?template"] })
} }
await auth.getSelf() // Validate tenant if in a multi-tenant env
await admin.init()
if (useAccountPortal && multiTenancyEnabled) { if (useAccountPortal && multiTenancyEnabled) {
await validateTenantId() await validateTenantId()
} }
} catch (error) {
// Don't show a notification here, as we might 403 initially due to not
// being logged in
}
loaded = true loaded = true
}) })
@ -79,7 +83,7 @@
loaded && loaded &&
apiReady && apiReady &&
!$auth.user && !$auth.user &&
!getCookie(Cookies.ReturnUrl) && !CookieUtils.getCookie(Constants.Cookies.ReturnUrl) &&
// logout triggers a page refresh, so we don't want to set the return url // logout triggers a page refresh, so we don't want to set the return url
!$auth.postLogout && !$auth.postLogout &&
// don't set the return url on pre-login pages // don't set the return url on pre-login pages
@ -88,7 +92,7 @@
!$isActive("./admin") !$isActive("./admin")
) { ) {
const url = window.location.pathname const url = window.location.pathname
setCookie(Cookies.ReturnUrl, url) CookieUtils.setCookie(Constants.Cookies.ReturnUrl, url)
} }
// if tenant is not set go to it // if tenant is not set go to it
@ -122,9 +126,9 @@
} }
// lastly, redirect to the return url if it has been set // lastly, redirect to the return url if it has been set
else if (loaded && apiReady && $auth.user) { else if (loaded && apiReady && $auth.user) {
const returnUrl = getCookie(Cookies.ReturnUrl) const returnUrl = CookieUtils.getCookie(Constants.Cookies.ReturnUrl)
if (returnUrl) { if (returnUrl) {
removeCookie(Cookies.ReturnUrl) CookieUtils.removeCookie(Constants.Cookies.ReturnUrl)
window.location.href = returnUrl window.location.href = returnUrl
} }
} }

View File

@ -1,6 +1,6 @@
<script> <script>
import { notifications, ModalContent, Dropzone, Body } from "@budibase/bbui" import { notifications, ModalContent, Dropzone, Body } from "@budibase/bbui"
import { post } from "builderStore/api" import { API } from "api"
import { admin } from "stores/portal" import { admin } from "stores/portal"
let submitting = false let submitting = false
@ -9,24 +9,19 @@
async function importApps() { async function importApps() {
submitting = true submitting = true
try { try {
// Create form data to create app // Create form data to create app
let data = new FormData() let data = new FormData()
data.append("importFile", value.file) data.append("importFile", value.file)
// Create App // Create App
const importResp = await post("/api/cloud/import", data, {}) await API.importApps(data)
const importJson = await importResp.json()
if (!importResp.ok) {
throw new Error(importJson.message)
}
await admin.checkImportComplete() await admin.checkImportComplete()
notifications.success("Import complete, please finish registration!") notifications.success("Import complete, please finish registration!")
} catch (error) { } catch (error) {
notifications.error(error) notifications.error("Failed to import apps")
submitting = false
} }
submitting = false
} }
</script> </script>
@ -36,10 +31,10 @@
onConfirm={importApps} onConfirm={importApps}
disabled={!value.file} disabled={!value.file}
> >
<Body <Body>
>Please upload the file that was exported from your Cloud environment to get Please upload the file that was exported from your Cloud environment to get
started</Body started
> </Body>
<Dropzone <Dropzone
gallery={false} gallery={false}
label="File to import" label="File to import"

View File

@ -10,7 +10,7 @@
Modal, Modal,
} from "@budibase/bbui" } from "@budibase/bbui"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import api from "builderStore/api" import { API } from "api"
import { admin, auth } from "stores/portal" import { admin, auth } from "stores/portal"
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte" import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
import ImportAppsModal from "./_components/ImportAppsModal.svelte" import ImportAppsModal from "./_components/ImportAppsModal.svelte"
@ -30,22 +30,22 @@
try { try {
adminUser.tenantId = tenantId adminUser.tenantId = tenantId
// Save the admin user // Save the admin user
const response = await api.post(`/api/global/users/init`, adminUser) await API.createAdminUser(adminUser)
const json = await response.json() notifications.success("Admin user created")
if (response.status !== 200) {
throw new Error(json.message)
}
notifications.success(`Admin user created`)
await admin.init() await admin.init()
$goto("../portal") $goto("../portal")
} catch (err) { } catch (error) {
notifications.error(`Failed to create admin user: ${err}`) notifications.error("Failed to create admin user")
} }
} }
onMount(async () => { onMount(async () => {
if (!cloud) { if (!cloud) {
try {
await admin.checkImportComplete() await admin.checkImportComplete()
} catch (error) {
notifications.error("Error checking import status")
}
} }
}) })
</script> </script>

View File

@ -6,25 +6,26 @@
import RevertModal from "components/deploy/RevertModal.svelte" import RevertModal from "components/deploy/RevertModal.svelte"
import VersionModal from "components/deploy/VersionModal.svelte" import VersionModal from "components/deploy/VersionModal.svelte"
import NPSFeedbackForm from "components/feedback/NPSFeedbackForm.svelte" import NPSFeedbackForm from "components/feedback/NPSFeedbackForm.svelte"
import { get, post } from "builderStore/api" import { API } from "api"
import { auth, admin } from "stores/portal" import { auth, admin } from "stores/portal"
import { isActive, goto, layout, redirect } from "@roxi/routify" import { isActive, goto, layout, redirect } from "@roxi/routify"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import UpgradeModal from "../../../../components/upgrade/UpgradeModal.svelte" import UpgradeModal from "components/upgrade/UpgradeModal.svelte"
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
// Get Package and set store
export let application export let application
// Get Package and set store
let promise = getPackage() let promise = getPackage()
// sync once when you load the app
// Sync once when you load the app
let hasSynced = false let hasSynced = false
let userShouldPostFeedback = false
$: selected = capitalise( $: selected = capitalise(
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data" $layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
) )
let userShouldPostFeedback = false
function previewApp() { function previewApp() {
if (!$auth?.user?.flags?.feedbackSubmitted) { if (!$auth?.user?.flags?.feedbackSubmitted) {
userShouldPostFeedback = true userShouldPostFeedback = true
@ -33,34 +34,24 @@
} }
async function getPackage() { async function getPackage() {
const res = await get(`/api/applications/${application}/appPackage`)
const pkg = await res.json()
if (res.ok) {
try { try {
const pkg = await API.fetchAppPackage(application)
await store.actions.initialise(pkg) await store.actions.initialise(pkg)
// edge case, lock wasn't known to client when it re-directed, or user went directly
} catch (err) {
if (!err.ok && err.reason === "locked") {
$redirect("../../")
} else {
throw err
}
}
await automationStore.actions.fetch() await automationStore.actions.fetch()
await roles.fetch() await roles.fetch()
await flags.fetch() await flags.fetch()
return pkg return pkg
} else { } catch (error) {
throw new Error(pkg) notifications.error(`Error initialising app: ${error?.message}`)
$redirect("../../")
} }
} }
// handles navigation between frontend, backend, automation. // Handles navigation between frontend, backend, automation.
// this remembers your last place on each of the sections // This remembers your last place on each of the sections
// e.g. if one of your screens is selected on front end, then // e.g. if one of your screens is selected on front end, then
// you browse to backend, when you click frontend, you will be // you browse to backend, when you click frontend, you will be
// brought back to the same screen // brought back to the same screen.
const topItemNavigate = path => () => { const topItemNavigate = path => () => {
const activeTopNav = $layout.children.find(c => $isActive(c.path)) const activeTopNav = $layout.children.find(c => $isActive(c.path))
if (!activeTopNav) return if (!activeTopNav) return
@ -74,8 +65,9 @@
onMount(async () => { onMount(async () => {
if (!hasSynced && application) { if (!hasSynced && application) {
const res = await post(`/api/applications/${application}/sync`) try {
if (res.status !== 200) { await API.syncApp(application)
} catch (error) {
notifications.error("Failed to sync with production database") notifications.error("Failed to sync with production database")
} }
hasSynced = true hasSynced = true

Some files were not shown because too many files have changed in this diff Show More