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 { 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 EXPIRED = "This account has expired. Please reset your password"

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
import CellRenderer from "./CellRenderer.svelte"
import SelectEditRenderer from "./SelectEditRenderer.svelte"
import { cloneDeep } from "lodash"
import { deepGet } from "../utils/helpers"
import { deepGet } from "../helpers"
/**
* 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)
// Starts with a letter so that its a valid DOM ID
return `A${rand}`
/**
* Generates a DOM safe UUID.
* Starting with a letter is important to make it DOM safe.
* @return {string} a random DOM safe UUID
*/
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

View File

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

View File

@ -6,8 +6,6 @@
"scripts": {
"build": "routify -b && vite build --emptyOutDir",
"start": "routify -c rollup",
"test": "jest",
"test:watch": "jest --watchAll",
"dev:builder": "routify -c dev:vite",
"dev:vite": "vite --host 0.0.0.0",
"rollup": "rollup -c -w",
@ -68,7 +66,7 @@
"dependencies": {
"@budibase/bbui": "^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",
"@sentry/browser": "5.19.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 IntercomClient from "./IntercomClient"
import SentryClient from "./SentryClient"
@ -17,13 +17,11 @@ class AnalyticsHub {
}
async activate() {
const analyticsStatus = await api.get("/api/analytics")
const json = await analyticsStatus.json()
// Analytics disabled
if (!json.enabled) return
this.clients.forEach(client => client.init())
// Check analytics are enabled
const analyticsStatus = await API.getAnalyticsStatus()
if (analyticsStatus.enabled) {
this.clients.forEach(client => client.init())
}
}
identify(id, metadata) {

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,
} from "@budibase/string-templates"
import { TableNames } from "../constants"
import {
convertJSONSchemaToTableSchema,
getJSONArrayDatasourceSchema,
} from "./jsonUtils"
import { JSONUtils } from "@budibase/frontend-core"
import ActionDefinitions from "components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json"
// Regex to match all instances of template strings
@ -439,7 +436,7 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
else if (type === "jsonarray") {
table = tables.find(table => table._id === datasource.tableId)
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
@ -471,9 +468,12 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
Object.keys(schema).forEach(fieldKey => {
const fieldSchema = schema[fieldKey]
if (fieldSchema?.type === "json") {
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
squashObjects: true,
})
const jsonSchema = JSONUtils.convertJSONSchemaToTableSchema(
fieldSchema,
{
squashObjects: true,
}
)
Object.keys(jsonSchema).forEach(jsonKey => {
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
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 api from "../../api"
import { API } from "api"
import Automation from "./Automation"
import { cloneDeep } from "lodash/fp"
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 => ({
fetch: async () => {
const responses = await Promise.all([
api.get(`/api/automations`),
api.get(`/api/automations/definitions/list`),
API.getAutomations(),
API.getAutomationDefinitions(),
])
const jsonResponses = await Promise.all(responses.map(x => x.json()))
store.update(state => {
let selected = state.selectedAutomation?.automation
state.automations = jsonResponses[0]
state.automations = responses[0]
state.blockDefinitions = {
TRIGGER: jsonResponses[1].trigger,
ACTION: jsonResponses[1].action,
TRIGGER: responses[1].trigger,
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) {
selected = jsonResponses[0].filter(
selected = responses[0].filter(
automation => automation._id === selected._id
)
state.selectedAutomation = new Automation(selected[0])
@ -36,40 +50,36 @@ const automationActions = store => ({
steps: [],
},
}
const CREATE_AUTOMATION_URL = `/api/automations`
const response = await api.post(CREATE_AUTOMATION_URL, automation)
const json = await response.json()
const response = await API.createAutomation(automation)
store.update(state => {
state.automations = [...state.automations, json.automation]
store.actions.select(json.automation)
state.automations = [...state.automations, response.automation]
store.actions.select(response.automation)
return state
})
},
save: async automation => {
const UPDATE_AUTOMATION_URL = `/api/automations`
const response = await api.put(UPDATE_AUTOMATION_URL, automation)
const json = await response.json()
const response = await API.updateAutomation(automation)
store.update(state => {
const newAutomation = json.automation
const updatedAutomation = response.automation
const existingIdx = state.automations.findIndex(
existing => existing._id === automation._id
)
if (existingIdx !== -1) {
state.automations.splice(existingIdx, 1, newAutomation)
state.automations.splice(existingIdx, 1, updatedAutomation)
state.automations = [...state.automations]
store.actions.select(newAutomation)
store.actions.select(updatedAutomation)
return state
}
})
},
delete: async automation => {
const { _id, _rev } = automation
const DELETE_AUTOMATION_URL = `/api/automations/${_id}/${_rev}`
await api.delete(DELETE_AUTOMATION_URL)
await API.deleteAutomation({
automationId: automation?._id,
automationRev: automation?._rev,
})
store.update(state => {
const existingIdx = state.automations.findIndex(
existing => existing._id === _id
existing => existing._id === automation?._id
)
state.automations.splice(existingIdx, 1)
state.automations = [...state.automations]
@ -78,16 +88,17 @@ const automationActions = store => ({
return state
})
},
trigger: async automation => {
const { _id } = automation
return await api.post(`/api/automations/${_id}/trigger`)
},
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 => {
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
})
},
@ -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,
tables,
} from "stores/backend"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
import { API } from "api"
import { FrontendTypes } from "constants"
import analytics, { Events } from "analytics"
import {
@ -26,7 +25,7 @@ import {
findComponent,
getComponentSettings,
} from "../componentUtils"
import { uuid } from "../uuid"
import { Helpers } from "@budibase/bbui"
import { removeBindings } from "../dataBinding"
const INITIAL_FRONTEND_STATE = {
@ -70,15 +69,12 @@ export const getFrontendStore = () => {
},
initialise: async pkg => {
const { layouts, screens, application, clientLibPath } = pkg
const components = await fetchComponentLibDefinitions(application.appId)
// make sure app isn't locked
if (
components &&
components.status === 400 &&
components.message?.includes("lock")
) {
throw { ok: false, reason: "locked" }
}
// Fetch component definitions.
// Allow errors to propagate.
let components = await API.fetchComponentLibDefinitions(application.appId)
// Reset store state
store.update(state => ({
...state,
libraries: application.componentLibraries,
@ -91,8 +87,8 @@ export const getFrontendStore = () => {
description: application.description,
appId: application.appId,
url: application.url,
layouts,
screens,
layouts: layouts || [],
screens: screens || [],
theme: application.theme || "spectrum--light",
customTheme: application.customTheme,
hasAppPackage: true,
@ -104,51 +100,43 @@ export const getFrontendStore = () => {
}))
// 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)
tables.init()
await datasources.init()
await integrations.init()
await queries.init()
await tables.init()
},
theme: {
save: async theme => {
const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, { theme })
if (response.status === 200) {
store.update(state => {
state.theme = theme
return state
})
} else {
throw new Error("Error updating theme")
}
await API.saveAppMetadata({
appId,
metadata: { theme },
})
store.update(state => {
state.theme = theme
return state
})
},
},
customTheme: {
save: async customTheme => {
const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, {
customTheme,
await API.saveAppMetadata({
appId,
metadata: { customTheme },
})
store.update(state => {
state.customTheme = customTheme
return state
})
if (response.status === 200) {
store.update(state => {
state.customTheme = customTheme
return state
})
} else {
throw new Error("Error updating theme")
}
},
},
routing: {
fetch: async () => {
const response = await api.get("/api/routing")
const json = await response.json()
const response = await API.fetchAppRoutes()
store.update(state => {
state.routes = json.routes
state.routes = response.routes
return state
})
},
@ -172,82 +160,76 @@ export const getFrontendStore = () => {
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 => {
const creatingNewScreen = screen._id === undefined
const response = await api.post(`/api/screens`, screen)
if (response.status !== 200) {
return
}
screen = await response.json()
await store.actions.routing.fetch()
const savedScreen = await API.saveScreen(screen)
store.update(state => {
const foundScreen = state.screens.findIndex(
el => el._id === screen._id
)
if (foundScreen !== -1) {
state.screens.splice(foundScreen, 1)
const idx = state.screens.findIndex(x => x._id === savedScreen._id)
if (idx !== -1) {
state.screens.splice(idx, 1, savedScreen)
} else {
state.screens.push(savedScreen)
}
state.screens.push(screen)
return state
})
if (creatingNewScreen) {
store.actions.screens.select(screen._id)
}
// Refresh routes
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 => {
const screensToDelete = Array.isArray(screens) ? screens : [screens]
const screenDeletePromises = []
// Build array of promises to speed up bulk deletions
const promises = []
screensToDelete.forEach(screen => {
// Delete the screen
promises.push(
API.deleteScreen({
screenId: screen._id,
screenRev: screen._rev,
})
)
// Remove links to this screen
promises.push(
store.actions.components.links.delete(
screen.routing.route,
screen.props._instanceName
)
)
})
await Promise.all(promises)
const deletedIds = screensToDelete.map(screen => screen._id)
store.update(state => {
for (let screenToDelete of screensToDelete) {
state.screens = state.screens.filter(
screen => screen._id !== screenToDelete._id
)
screenDeletePromises.push(
api.delete(
`/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(
screenToDelete.routing.route,
screenToDelete.props._instanceName
)
)
// 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
})
await Promise.all(screenDeletePromises)
// Refresh routes
await store.actions.routing.fetch()
},
},
preview: {
saveSelected: async () => {
const state = get(store)
const selectedAsset = get(currentAsset)
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
await store.actions.screens.save(selectedAsset)
return await store.actions.screens.save(selectedAsset)
} else {
await store.actions.layouts.save(selectedAsset)
return await store.actions.layouts.save(selectedAsset)
}
},
setDevice: device => {
@ -271,25 +253,13 @@ export const getFrontendStore = () => {
})
},
save: async layout => {
const layoutToSave = cloneDeep(layout)
const creatingNewLayout = layoutToSave._id === undefined
const response = await api.post(`/api/layouts`, layoutToSave)
const savedLayout = await response.json()
// Abort if saving failed
if (response.status !== 200) {
return
}
const creatingNewLayout = layout._id === undefined
const savedLayout = await API.saveLayout(layout)
store.update(state => {
const layoutIdx = state.layouts.findIndex(
stateLayout => stateLayout._id === savedLayout._id
)
if (layoutIdx >= 0) {
// update existing layout
state.layouts.splice(layoutIdx, 1, savedLayout)
const idx = state.layouts.findIndex(x => x._id === savedLayout._id)
if (idx !== -1) {
state.layouts.splice(idx, 1, savedLayout)
} else {
// save new layout
state.layouts.push(savedLayout)
}
return state
@ -299,7 +269,6 @@ export const getFrontendStore = () => {
if (creatingNewLayout) {
store.actions.layouts.select(savedLayout._id)
}
return savedLayout
},
find: layoutId => {
@ -309,21 +278,20 @@ export const getFrontendStore = () => {
const storeContents = get(store)
return storeContents.layouts.find(layout => layout._id === layoutId)
},
delete: async layoutToDelete => {
const response = await api.delete(
`/api/layouts/${layoutToDelete._id}/${layoutToDelete._rev}`
)
if (response.status !== 200) {
const json = await response.json()
throw new Error(json.message)
delete: async layout => {
if (!layout?._id) {
return
}
await API.deleteLayout({
layoutId: layout._id,
layoutRev: layout._rev,
})
store.update(state => {
state.layouts = state.layouts.filter(
layout => layout._id !== layoutToDelete._id
)
if (layoutToDelete._id === state.selectedLayoutId) {
// Select main layout if we deleted the selected layout
if (layout._id === state.selectedLayoutId) {
state.selectedLayoutId = get(mainLayout)._id
}
state.layouts = state.layouts.filter(x => x._id !== layout._id)
return state
})
},
@ -398,7 +366,7 @@ export const getFrontendStore = () => {
}
return {
_id: uuid(),
_id: Helpers.uuid(),
_component: definition.component,
_styles: { normal: {}, hover: {}, active: {} },
_instanceName: `New ${definition.name}`,
@ -415,16 +383,12 @@ export const getFrontendStore = () => {
componentName,
presetProps
)
if (!componentInstance) {
if (!componentInstance || !asset) {
return
}
// Find parent node to attach this component to
let parentComponent
if (!asset) {
return
}
if (selected) {
// Use current screen or layout as parent if no component is selected
const definition = store.actions.components.getDefinition(
@ -552,7 +516,7 @@ export const getFrontendStore = () => {
if (!component) {
return
}
component._id = uuid()
component._id = Helpers.uuid()
component._children?.forEach(randomizeIds)
}
randomizeIds(componentToPaste)
@ -606,11 +570,6 @@ export const getFrontendStore = () => {
selected._styles.custom = style
await store.actions.preview.saveSelected()
},
resetStyles: async () => {
const selected = get(selectedComponent)
selected._styles = { normal: {}, hover: {}, active: {} }
await store.actions.preview.saveSelected()
},
updateConditions: async conditions => {
const selected = get(selectedComponent)
selected._conditions = conditions
@ -665,7 +624,7 @@ export const getFrontendStore = () => {
newLink = cloneDeep(nav._children[0])
// Set our new props
newLink._id = uuid()
newLink._id = Helpers.uuid()
newLink._instanceName = `${title} Link`
newLink.url = url
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"
export class Component extends BaseStructure {
@ -6,7 +6,7 @@ export class Component extends BaseStructure {
super(false)
this._children = []
this._json = {
_id: uuid(),
_id: Helpers.uuid(),
_component: name,
_styles: {
normal: {},

View File

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

View File

@ -1,4 +1,4 @@
import { localStorageStore } from "./localStorage"
import { createLocalStorageStore } from "@budibase/frontend-core"
export const getThemeStore = () => {
const themeElement = document.documentElement
@ -6,7 +6,7 @@ export const getThemeStore = () => {
theme: "darkest",
options: ["lightest", "light", "dark", "darkest"],
}
const store = localStorageStore("bb-theme", initialValue)
const store = createLocalStorageStore("bb-theme", initialValue)
// Update theme class when store changes
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,
Icon,
Tooltip,
notifications,
} from "@budibase/bbui"
import { automationStore } from "builderStore"
import { admin } from "stores/portal"
@ -47,15 +48,19 @@
}
async function addBlockToAutomation() {
const newBlock = $automationStore.selectedAutomation.constructBlock(
"ACTION",
actionVal.stepId,
actionVal
)
automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
try {
const newBlock = $automationStore.selectedAutomation.constructBlock(
"ACTION",
actionVal.stepId,
actionVal
)
automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} catch (error) {
notifications.error("Error saving automation")
}
}
</script>

View File

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

View File

@ -10,6 +10,7 @@
Button,
StatusLight,
ActionButton,
notifications,
} from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
@ -54,10 +55,14 @@
).every(x => block?.inputs[x])
async function deleteStep() {
automationStore.actions.deleteAutomationBlock(block)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
try {
automationStore.actions.deleteAutomationBlock(block)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} catch (error) {
notifications.error("Error saving notification")
}
}
</script>

View File

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

View File

@ -4,10 +4,16 @@
import { automationStore } from "builderStore"
import NavItem from "components/common/NavItem.svelte"
import EditAutomationPopover from "./EditAutomationPopover.svelte"
import { notifications } from "@budibase/bbui"
$: 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) {

View File

@ -24,29 +24,33 @@
nameTouched && !name ? "Please specify a name for the automation." : null
async function createAutomation() {
await automationStore.actions.create({
name,
instanceId,
})
const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER",
triggerVal.stepId,
triggerVal
)
try {
await automationStore.actions.create({
name,
instanceId,
})
const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER",
triggerVal.stepId,
triggerVal
)
automationStore.actions.addBlockToAutomation(newBlock)
if (triggerVal.stepId === "WEBHOOK") {
webhookModal.show
automationStore.actions.addBlockToAutomation(newBlock)
if (triggerVal.stepId === "WEBHOOK") {
webhookModal.show
}
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
notifications.success(`Automation ${name} created`)
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
analytics.captureEvent(Events.AUTOMATION.CREATED, { name })
} catch (error) {
notifications.error("Error creating automation")
}
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
notifications.success(`Automation ${name} created.`)
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
analytics.captureEvent(Events.AUTOMATION.CREATED, { name })
}
$: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER)

View File

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

View File

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

View File

@ -11,6 +11,7 @@
Drawer,
Modal,
Detail,
notifications,
} from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
@ -28,7 +29,7 @@
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
// 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 testData
@ -54,28 +55,32 @@
$: schemaFields = table ? Object.values(table.schema) : []
const onChange = debounce(async function (e, key) {
if (isTestModal) {
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === "WEBHOOK") {
try {
if (isTestModal) {
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === "WEBHOOK") {
automationStore.actions.addTestDataToAutomation({
body: {
[key]: e.detail,
...$automationStore.selectedAutomation.automation.testData.body,
},
})
}
automationStore.actions.addTestDataToAutomation({
body: {
[key]: e.detail,
...$automationStore.selectedAutomation.automation.testData.body,
},
[key]: e.detail,
})
testData[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} else {
block.inputs[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
}
automationStore.actions.addTestDataToAutomation({
[key]: e.detail,
})
testData[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} else {
block.inputs[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} catch (error) {
notifications.error("Error saving automation")
}
}, 800)
@ -131,7 +136,7 @@
}
function saveFilters(key) {
const filters = buildLuceneQuery(tempFilters)
const filters = LuceneUtils.buildLuceneQuery(tempFilters)
const defKey = `${key}-def`
inputData[key] = filters
inputData[defKey] = tempFilters

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<script>
import api from "builderStore/api"
import { API } from "api"
import { tables } from "stores/backend"
import Table from "./Table.svelte"
@ -9,6 +9,7 @@
import ExportButton from "./buttons/ExportButton.svelte"
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
import { notifications } from "@budibase/bbui"
export let view = {}
@ -20,33 +21,31 @@
$: name = view.name
// Fetch rows for specified view
$: {
loading = true
fetchViewData(name, view.field, view.groupBy, view.calculation)
}
$: fetchViewData(name, view.field, view.groupBy, view.calculation)
async function fetchViewData(name, field, groupBy, calculation) {
loading = true
const _tables = $tables.list
const allTableViews = _tables.map(table => table.views)
const thisView = allTableViews.filter(
views => views != null && views[name] != null
)[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) {
loading = false
return
}
const params = new URLSearchParams()
if (calculation) {
params.set("field", field)
params.set("calculation", calculation)
try {
data = await API.fetchViewData({
name,
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
}
</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() {
views.save(view)
notifications.success(`View ${view.name} saved.`)
analytics.captureEvent(Events.VIEW.ADDED_CALCULATE, { field: view.field })
try {
views.save(view)
notifications.success(`View ${view.name} saved`)
analytics.captureEvent(Events.VIEW.ADDED_CALCULATE, { field: view.field })
} catch (error) {
notifications.error("Error saving view")
}
}
</script>

View File

@ -124,7 +124,7 @@
})
dispatch("updatecolumns")
} catch (err) {
notifications.error(err)
notifications.error("Error saving column")
}
}
@ -133,17 +133,21 @@
}
function deleteColumn() {
field.name = deleteColName
if (field.name === $tables.selected.primaryDisplay) {
notifications.error("You cannot delete the display column")
} else {
tables.deleteField(field)
notifications.success(`Column ${field.name} deleted.`)
confirmDeleteDialog.hide()
hide()
deletion = false
try {
field.name = deleteColName
if (field.name === $tables.selected.primaryDisplay) {
notifications.error("You cannot delete the display column")
} else {
tables.deleteField(field)
notifications.success(`Column ${field.name} deleted.`)
confirmDeleteDialog.hide()
hide()
deletion = false
dispatch("updatecolumns")
}
} catch (error) {
notifications.error("Error deleting column")
}
dispatch("updatecolumns")
}
function handleTypeChange(event) {

View File

@ -3,7 +3,7 @@
import { tables, rows } from "stores/backend"
import { notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte"
import * as api from "../api"
import { API } from "api"
import { ModalContent } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import { FIELDS } from "constants/backend"
@ -22,30 +22,30 @@
$: tableSchema = Object.entries(table?.schema ?? {})
async function saveRow() {
const rowResponse = await api.saveRow(
{ ...row, tableId: table._id },
table._id
)
if (rowResponse.errors) {
errors = Object.entries(rowResponse.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
errors = []
try {
await API.saveRow({ ...row, tableId: table._id })
notifications.success("Row saved successfully")
rows.save()
dispatch("updaterows")
} catch (error) {
if (error.handled) {
const response = error.json
if (response?.errors) {
errors = Object.entries(response.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.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
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,11 @@
<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 structure from "./componentStructure.json"
@ -36,7 +42,11 @@
const onItemChosen = async item => {
if (!item.isCategory) {
await store.actions.components.create(item.component)
try {
await store.actions.components.create(item.component)
} catch (error) {
notifications.error("Error creating component")
}
}
}
</script>

View File

@ -13,7 +13,7 @@
Body,
notifications,
} 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"
let iframe
@ -146,44 +146,49 @@
}
})
const handleBudibaseEvent = event => {
const handleBudibaseEvent = async event => {
const { type, data } = event.data || event.detail
if (!type) {
return
}
if (type === "select-component" && data.id) {
store.actions.components.select({ _id: data.id })
} else if (type === "update-prop") {
store.actions.components.updateProp(data.prop, data.value)
} else if (type === "delete-component" && data.id) {
confirmDeleteComponent(data.id)
} else if (type === "preview-loaded") {
// Wait for this event to show the client library if intelligent
// loading is supported
loading = false
} else if (type === "move-component") {
const { componentId, destinationComponentId } = data
const rootComponent = get(currentAsset).props
try {
if (type === "select-component" && data.id) {
store.actions.components.select({ _id: data.id })
} else if (type === "update-prop") {
await store.actions.components.updateProp(data.prop, data.value)
} else if (type === "delete-component" && data.id) {
confirmDeleteComponent(data.id)
} else if (type === "preview-loaded") {
// Wait for this event to show the client library if intelligent
// loading is supported
loading = false
} else if (type === "move-component") {
const { componentId, destinationComponentId } = data
const rootComponent = get(currentAsset).props
// Get source and destination components
const source = findComponent(rootComponent, componentId)
const destination = findComponent(rootComponent, destinationComponentId)
// Get source and destination components
const source = findComponent(rootComponent, componentId)
const destination = findComponent(rootComponent, destinationComponentId)
// Stop if the target is a child of source
const path = findComponentPath(source, destinationComponentId)
const ids = path.map(component => component._id)
if (ids.includes(data.destinationComponentId)) {
return
// Stop if the target is a child of source
const path = findComponentPath(source, destinationComponentId)
const ids = path.map(component => component._id)
if (ids.includes(data.destinationComponentId)) {
return
}
// Cut and paste the component to the new destination
if (source && destination) {
store.actions.components.copy(source, true)
await store.actions.components.paste(destination, data.mode)
}
} else {
console.warn(`Client sent unknown event type: ${type}`)
}
// Cut and paste the component to the new destination
if (source && destination) {
store.actions.components.copy(source, true)
store.actions.components.paste(destination, data.mode)
}
} else {
console.warn(`Client sent unknown event type: ${type}`)
} catch (error) {
console.warn(error)
notifications.error("Error handling event from app preview")
}
}
@ -196,7 +201,7 @@
try {
await store.actions.components.delete({ _id: idToDelete })
} catch (error) {
notifications.error(error)
notifications.error("Error deleting component")
}
idToDelete = null
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,15 @@
import ComponentNavigationTree from "components/design/NavigationPanel/ComponentNavigationTree/index.svelte"
import Layout from "components/design/NavigationPanel/Layout.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
@ -58,8 +66,12 @@
selectedAccessRole.set(role)
}
onMount(() => {
store.actions.routing.fetch()
onMount(async () => {
try {
await store.actions.routing.fetch()
} catch (error) {
notifications.error("Error fetching routes")
}
})
</script>

View File

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

View File

@ -2,7 +2,7 @@
import ScreenDetailsModal from "components/design/NavigationPanel/ScreenDetailsModal.svelte"
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
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 analytics, { Events } from "analytics"
@ -29,15 +29,19 @@
const save = async () => {
showProgressCircle = true
await createScreens()
for (let screen of createdScreens) {
await saveScreens(screen)
try {
await createScreens()
for (let screen of createdScreens) {
await saveScreens(screen)
}
await store.actions.routing.fetch()
selectedScreens = []
createdScreens = []
screenName = ""
url = ""
} catch (error) {
notifications.error("Error creating screens")
}
await store.actions.routing.fetch()
selectedScreens = []
createdScreens = []
screenName = ""
url = ""
showProgressCircle = false
}
@ -71,12 +75,16 @@
draftScreen.routing.route = route
await store.actions.screens.create(draftScreen)
await store.actions.screens.save(draftScreen)
if (draftScreen.props._instanceName.endsWith("List")) {
await store.actions.components.links.save(
draftScreen.routing.route,
draftScreen.routing.route.split("/")[1]
)
try {
await store.actions.components.links.save(
draftScreen.routing.route,
draftScreen.routing.route.split("/")[1]
)
} catch (error) {
notifications.error("Error creating link to screen")
}
}
}
}

View File

@ -1,6 +1,6 @@
<script>
import { isEmpty } from "lodash/fp"
import { Input, DetailSummary } from "@budibase/bbui"
import { Input, DetailSummary, notifications } from "@budibase/bbui"
import { store } from "builderStore"
import PropertyControl from "./PropertyControls/PropertyControl.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 control = getComponentForSettingType(setting?.type)

View File

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

View File

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

View File

@ -36,28 +36,37 @@
// called by the parent modal when actions are saved
const createAutomation = async parameters => {
if (parameters.automationId || !parameters.newAutomationName) return
await automationStore.actions.create({ name: parameters.newAutomationName })
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER",
"APP",
appActionDefinition
)
newBlock.inputs = {
fields: Object.keys(parameters.fields).reduce((fields, key) => {
fields[key] = "string"
return fields
}, {}),
if (parameters.automationId || !parameters.newAutomationName) {
return
}
try {
await automationStore.actions.create({
name: parameters.newAutomationName,
})
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER",
"APP",
appActionDefinition
)
automationStore.actions.addBlockToAutomation(newBlock)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
parameters.automationId = $automationStore.selectedAutomation.automation._id
delete parameters.newAutomationName
newBlock.inputs = {
fields: Object.keys(parameters.fields).reduce((fields, key) => {
fields[key] = "string"
return fields
}, {}),
}
automationStore.actions.addBlockToAutomation(newBlock)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
parameters.automationId =
$automationStore.selectedAutomation.automation._id
delete parameters.newAutomationName
} catch (error) {
notifications.error("Error creating automation")
}
}
</script>

View File

@ -12,7 +12,7 @@
import { dndzone } from "svelte-dnd-action"
import { generate } from "shortid"
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 { getComponentForSettingType } from "./componentSettings"
import PropertyControl from "./PropertyControl.svelte"
@ -83,7 +83,7 @@
valueType: "string",
id: generate(),
action: "hide",
operator: OperatorOptions.Equals.value,
operator: Constants.OperatorOptions.Equals.value,
},
]
}
@ -108,13 +108,13 @@
}
const getOperatorOptions = condition => {
return getValidOperatorsForType(condition.valueType)
return LuceneUtils.getValidOperatorsForType(condition.valueType)
}
const onOperatorChange = (condition, newOperator) => {
const noValueOptions = [
OperatorOptions.Empty.value,
OperatorOptions.NotEmpty.value,
Constants.OperatorOptions.Empty.value,
Constants.OperatorOptions.NotEmpty.value,
]
condition.noValue = noValueOptions.includes(newOperator)
if (condition.noValue) {
@ -127,9 +127,12 @@
condition.referenceValue = null
// 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)) {
condition.operator = validOperators[0] ?? OperatorOptions.Equals.value
condition.operator =
validOperators[0] ?? Constants.OperatorOptions.Equals.value
onOperatorChange(condition, condition.operator)
}
}

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<script>
import { get } from "svelte/store"
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 LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
@ -29,7 +29,12 @@
}
return state
})
store.actions.preview.saveSelected()
try {
store.actions.preview.saveSelected()
} catch (error) {
notifications.error("Error saving settings")
}
}
const screenSettings = [

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<script>
import { Label, Select } from "@budibase/bbui"
import { Label, notifications, Select } from "@budibase/bbui"
import { permissions, roles } from "stores/backend"
import { Roles } from "constants/backend"
@ -11,16 +11,20 @@
let roleId, loaded, fetched
async function updateRole(role, id) {
roleId = role
const queryId = query?._id || id
if (roleId && queryId) {
for (let level of ["read", "write"]) {
await permissions.save({
level,
role,
resource: queryId,
})
try {
roleId = role
const queryId = query?._id || id
if (roleId && queryId) {
for (let level of ["read", "write"]) {
await permissions.save({
level,
role,
resource: queryId,
})
}
}
} catch (error) {
notifications.error("Error updating role")
}
}

View File

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

View File

@ -1,5 +1,12 @@
<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"
export let app
@ -51,12 +58,16 @@
}
const save = async () => {
await apps.update(app.instance._id, {
icon: {
name: selectedIcon,
color: selectedColor,
},
})
try {
await apps.update(app.instance._id, {
icon: {
name: selectedIcon,
color: selectedColor,
},
})
} catch (error) {
notifications.error("Error updating app")
}
}
</script>

View File

@ -2,8 +2,8 @@
import { writable, get as svelteGet } from "svelte/store"
import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
import { store, automationStore } from "builderStore"
import { API } from "api"
import { apps, admin, auth } from "stores/portal"
import api, { get, post } from "builderStore/api"
import analytics, { Events } from "analytics"
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
@ -45,43 +45,27 @@
}
// Create App
const appResp = await post("/api/applications", data, {})
const appJson = await appResp.json()
if (!appResp.ok) {
throw new Error(appJson.message)
}
const createdApp = await API.createApp(data)
analytics.captureEvent(Events.APP.CREATED, {
name: $values.name,
appId: appJson.instance._id,
appId: createdApp.instance._id,
templateToUse: template,
})
// Select Correct Application/DB in prep for creating user
const applicationPkg = await get(
`/api/applications/${appJson.instance._id}/appPackage`
)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
// update checklist - incase first app
await admin.init()
} else {
throw new Error(pkg)
}
const pkg = await API.fetchAppPackage(createdApp.instance._id)
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
// Update checklist - in case first app
await admin.init()
// Create user
const user = {
roleId: $values.roleId,
}
const userResp = await api.post(`/api/users/metadata/self`, user)
await userResp.json()
await API.updateOwnMetadata({ roleId: $values.roleId })
await auth.setInitInfo({})
$goto(`/builder/app/${appJson.instance._id}`)
$goto(`/builder/app/${createdApp.instance._id}`)
} catch (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)
} catch (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 api from "builderStore/api"
import { API } from "api"
export default function (url) {
const store = writable({ status: "LOADING", data: {}, error: {} })
@ -7,8 +7,8 @@ export default function (url) {
async function get() {
store.update(u => ({ ...u, status: "LOADING" }))
try {
const response = await api.get(url)
store.set({ data: await response.json(), status: "SUCCESS" })
const data = await API.get({ url })
store.set({ data, status: "SUCCESS" })
} catch (e) {
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 { admin, auth } from "stores/portal"
import { onMount } from "svelte"
import {
Cookies,
getCookie,
removeCookie,
setCookie,
} from "builderStore/cookies"
import { CookieUtils, Constants } from "@budibase/frontend-core"
let loaded = false
@ -46,9 +41,12 @@
if (user.tenantId !== urlTenantId) {
// user should not be here - play it safe and log them out
await auth.logout()
await auth.setOrganisation(null)
return
try {
await auth.logout()
await auth.setOrganisation(null)
} catch (error) {
// Swallow error and do nothing
}
}
} else {
// no user - set the org according to the url
@ -57,17 +55,23 @@
}
onMount(async () => {
if ($params["?template"]) {
await auth.setInitInfo({ init_template: $params["?template"] })
try {
await auth.getSelf()
await admin.init()
// Set init info if present
if ($params["?template"]) {
await auth.setInitInfo({ init_template: $params["?template"] })
}
// Validate tenant if in a multi-tenant env
if (useAccountPortal && multiTenancyEnabled) {
await validateTenantId()
}
} catch (error) {
// Don't show a notification here, as we might 403 initially due to not
// being logged in
}
await auth.getSelf()
await admin.init()
if (useAccountPortal && multiTenancyEnabled) {
await validateTenantId()
}
loaded = true
})
@ -79,7 +83,7 @@
loaded &&
apiReady &&
!$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
!$auth.postLogout &&
// don't set the return url on pre-login pages
@ -88,7 +92,7 @@
!$isActive("./admin")
) {
const url = window.location.pathname
setCookie(Cookies.ReturnUrl, url)
CookieUtils.setCookie(Constants.Cookies.ReturnUrl, url)
}
// if tenant is not set go to it
@ -122,9 +126,9 @@
}
// lastly, redirect to the return url if it has been set
else if (loaded && apiReady && $auth.user) {
const returnUrl = getCookie(Cookies.ReturnUrl)
const returnUrl = CookieUtils.getCookie(Constants.Cookies.ReturnUrl)
if (returnUrl) {
removeCookie(Cookies.ReturnUrl)
CookieUtils.removeCookie(Constants.Cookies.ReturnUrl)
window.location.href = returnUrl
}
}

View File

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

View File

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

View File

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

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