diff --git a/packages/backend-core/src/middleware/passport/local.js b/packages/backend-core/src/middleware/passport/local.js index f95c3a173e..2149bd3e18 100644 --- a/packages/backend-core/src/middleware/passport/local.js +++ b/packages/backend-core/src/middleware/passport/local.js @@ -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" diff --git a/packages/bbui/src/ColorPicker/ColorPicker.svelte b/packages/bbui/src/ColorPicker/ColorPicker.svelte index ff6a292d1b..1fa950fadc 100644 --- a/packages/bbui/src/ColorPicker/ColorPicker.svelte +++ b/packages/bbui/src/ColorPicker/ColorPicker.svelte @@ -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" diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index 69bfdda72a..c1c4cc866f 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -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 diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte index 6b8022a36c..d739e751c9 100644 --- a/packages/bbui/src/Form/Core/Dropzone.svelte +++ b/packages/bbui/src/Form/Core/Dropzone.svelte @@ -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 diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index 09ade22627..2aea31857e 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -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. diff --git a/packages/bbui/src/utils/helpers.js b/packages/bbui/src/helpers.js similarity index 61% rename from packages/bbui/src/utils/helpers.js rename to packages/bbui/src/helpers.js index 6cf432f356..e80db76537 100644 --- a/packages/bbui/src/utils/helpers.js +++ b/packages/bbui/src/helpers.js @@ -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 diff --git a/packages/bbui/src/index.js b/packages/bbui/src/index.js index ea5486aadc..d3bc11cf9d 100644 --- a/packages/bbui/src/index.js +++ b/packages/bbui/src/index.js @@ -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" diff --git a/packages/builder/package.json b/packages/builder/package.json index bc4db6629c..91e568da64 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -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", diff --git a/packages/builder/src/analytics/index.js b/packages/builder/src/analytics/index.js index e9edf38d74..3a4118347d 100644 --- a/packages/builder/src/analytics/index.js +++ b/packages/builder/src/analytics/index.js @@ -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) { diff --git a/packages/builder/src/api.js b/packages/builder/src/api.js new file mode 100644 index 0000000000..5604db5db8 --- /dev/null +++ b/packages/builder/src/api.js @@ -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() + } + } + }, +}) diff --git a/packages/builder/src/builderStore/api.js b/packages/builder/src/builderStore/api.js deleted file mode 100644 index a932799701..0000000000 --- a/packages/builder/src/builderStore/api.js +++ /dev/null @@ -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"), -} diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 0f3cffc4fb..23dede9fe2 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -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, diff --git a/packages/builder/src/builderStore/loadComponentLibraries.js b/packages/builder/src/builderStore/loadComponentLibraries.js deleted file mode 100644 index 8bdfcf7538..0000000000 --- a/packages/builder/src/builderStore/loadComponentLibraries.js +++ /dev/null @@ -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) - } -} diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index 7bd0ccca22..b901a71cb1 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -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 -} diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index d838150efc..9ce66db3c0 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -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 diff --git a/packages/builder/src/builderStore/store/notifications.js b/packages/builder/src/builderStore/store/notifications.js deleted file mode 100644 index 85e708e92a..0000000000 --- a/packages/builder/src/builderStore/store/notifications.js +++ /dev/null @@ -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"), -} diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js b/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js index 182736a1d5..93aa925aa6 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js @@ -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: {}, diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js b/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js index 04ff1f4ba2..272f627163 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js @@ -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: {}, diff --git a/packages/builder/src/builderStore/store/theme.js b/packages/builder/src/builderStore/store/theme.js index fd6b05df59..d4d7460ed2 100644 --- a/packages/builder/src/builderStore/store/theme.js +++ b/packages/builder/src/builderStore/store/theme.js @@ -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 => { diff --git a/packages/builder/src/builderStore/uuid.js b/packages/builder/src/builderStore/uuid.js deleted file mode 100644 index 149da83c68..0000000000 --- a/packages/builder/src/builderStore/uuid.js +++ /dev/null @@ -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) - }) -} diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte index 5ae031e033..4e1e5e1103 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte @@ -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") + } } diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index 2d6881d652..777fcd710a 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -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 } @@ -85,7 +72,7 @@ animate:flip={{ duration: 500 }} in:fly|local={{ x: 500, duration: 1500 }} > - + {/each} @@ -101,7 +88,7 @@ - + diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index fe94b7e63f..f13a827f31 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -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") + } } diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte index e43729edbe..ffd59b4e6a 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte @@ -1,5 +1,12 @@ { - automationStore.actions.test( - $automationStore.selectedAutomation?.automation, - testData - ) - }} + onConfirm={testAutomation} cancelText="Cancel" > { - automationStore.actions.fetch() + + onMount(async () => { + try { + await automationStore.actions.fetch() + } catch (error) { + notifications.error("Error getting automations list") + } }) function selectAutomation(automation) { diff --git a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte index 36723d7726..4fb912939a 100644 --- a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte @@ -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) diff --git a/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte b/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte index fc12b60676..0d858d7a19 100644 --- a/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/EditAutomationPopover.svelte @@ -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") + } } diff --git a/packages/builder/src/components/automation/AutomationPanel/UpdateAutomationModal.svelte b/packages/builder/src/components/automation/AutomationPanel/UpdateAutomationModal.svelte index 64197c3a77..cc2512be8f 100644 --- a/packages/builder/src/components/automation/AutomationPanel/UpdateAutomationModal.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/UpdateAutomationModal.svelte @@ -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) { diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 928897d6f5..64b7cff78d 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -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 diff --git a/packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte b/packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte index 20dd696981..d8bc7fca3b 100644 --- a/packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte +++ b/packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte @@ -1,5 +1,5 @@ @@ -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 @@
diff --git a/packages/builder/src/components/backend/DataTable/RelationshipDataTable.svelte b/packages/builder/src/components/backend/DataTable/RelationshipDataTable.svelte index a0a06d1866..8ef870caca 100644 --- a/packages/builder/src/components/backend/DataTable/RelationshipDataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/RelationshipDataTable.svelte @@ -1,7 +1,8 @@ diff --git a/packages/builder/src/components/backend/DataTable/Table.svelte b/packages/builder/src/components/backend/DataTable/Table.svelte index 3c646bde68..0333bad611 100644 --- a/packages/builder/src/components/backend/DataTable/Table.svelte +++ b/packages/builder/src/components/backend/DataTable/Table.svelte @@ -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 => { diff --git a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte index a52fbdb177..10c6703623 100644 --- a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte @@ -1,5 +1,5 @@ diff --git a/packages/builder/src/components/backend/DataTable/api.js b/packages/builder/src/components/backend/DataTable/api.js deleted file mode 100644 index b461c70c4b..0000000000 --- a/packages/builder/src/components/backend/DataTable/api.js +++ /dev/null @@ -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 -} diff --git a/packages/builder/src/components/backend/DataTable/modals/CalculateModal.svelte b/packages/builder/src/components/backend/DataTable/modals/CalculateModal.svelte index 50d44eca88..81f54032f6 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CalculateModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CalculateModal.svelte @@ -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") + } } diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index edac352f8a..c946dbf9d8 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -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) { diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte index 559e8275db..965458e297 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte @@ -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") } diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte index f1de23fb97..088280e266 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte @@ -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") } diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte index 2f6ec51233..2ea0c1b63b 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte @@ -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") + } } diff --git a/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte b/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte index 7fa9482fbe..e2ccab11af 100644 --- a/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte @@ -1,7 +1,7 @@ diff --git a/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte b/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte index c413ee16ce..d7976cd387 100644 --- a/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte @@ -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 @@ onOperatorChange(filter, e.detail)} placeholder={null} diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ResetFieldsButton.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ResetFieldsButton.svelte index fa2a0d6088..a76a93d7f6 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ResetFieldsButton.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ResetFieldsButton.svelte @@ -1,5 +1,5 @@ diff --git a/packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte b/packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte index 4a7c77746e..ded80a7d5c 100644 --- a/packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte @@ -1,7 +1,7 @@ @@ -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} /> diff --git a/packages/builder/src/components/feedback/NPSFeedbackForm.svelte b/packages/builder/src/components/feedback/NPSFeedbackForm.svelte index 4c5bb46c63..6a6e52ec74 100644 --- a/packages/builder/src/components/feedback/NPSFeedbackForm.svelte +++ b/packages/builder/src/components/feedback/NPSFeedbackForm.svelte @@ -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") } diff --git a/packages/builder/src/components/integration/AccessLevelSelect.svelte b/packages/builder/src/components/integration/AccessLevelSelect.svelte index 86065893d4..59f6b8a105 100644 --- a/packages/builder/src/components/integration/AccessLevelSelect.svelte +++ b/packages/builder/src/components/integration/AccessLevelSelect.svelte @@ -1,5 +1,5 @@ diff --git a/packages/builder/src/components/start/ChooseIconModal.svelte b/packages/builder/src/components/start/ChooseIconModal.svelte index 4efb679a51..b2f68c6ce7 100644 --- a/packages/builder/src/components/start/ChooseIconModal.svelte +++ b/packages/builder/src/components/start/ChooseIconModal.svelte @@ -1,5 +1,12 @@ diff --git a/packages/builder/src/components/start/CreateAppModal.svelte b/packages/builder/src/components/start/CreateAppModal.svelte index 3efd0231aa..91c4807dc8 100644 --- a/packages/builder/src/components/start/CreateAppModal.svelte +++ b/packages/builder/src/components/start/CreateAppModal.svelte @@ -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") } } diff --git a/packages/builder/src/components/start/UpdateAppModal.svelte b/packages/builder/src/components/start/UpdateAppModal.svelte index 7549876fc0..1ce699b834 100644 --- a/packages/builder/src/components/start/UpdateAppModal.svelte +++ b/packages/builder/src/components/start/UpdateAppModal.svelte @@ -38,7 +38,7 @@ await apps.update(app.instance._id, body) } catch (error) { console.error(error) - notifications.error(error) + notifications.error("Error updating app") } } diff --git a/packages/builder/src/constants/lucene.js b/packages/builder/src/constants/lucene.js deleted file mode 100644 index 8a6bf57b5f..0000000000 --- a/packages/builder/src/constants/lucene.js +++ /dev/null @@ -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 [] -} diff --git a/packages/builder/src/helpers/fetchData.js b/packages/builder/src/helpers/fetchData.js index 65061f6b6a..9208419c4e 100644 --- a/packages/builder/src/helpers/fetchData.js +++ b/packages/builder/src/helpers/fetchData.js @@ -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" }) } diff --git a/packages/builder/src/helpers/fetchTableData.js b/packages/builder/src/helpers/fetchTableData.js deleted file mode 100644 index 6d61ec813e..0000000000 --- a/packages/builder/src/helpers/fetchTableData.js +++ /dev/null @@ -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, - } -} diff --git a/packages/builder/src/pages/builder/_layout.svelte b/packages/builder/src/pages/builder/_layout.svelte index 1d41af15e7..cb760cd165 100644 --- a/packages/builder/src/pages/builder/_layout.svelte +++ b/packages/builder/src/pages/builder/_layout.svelte @@ -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 } } diff --git a/packages/builder/src/pages/builder/admin/_components/ImportAppsModal.svelte b/packages/builder/src/pages/builder/admin/_components/ImportAppsModal.svelte index de29e11301..182df63967 100644 --- a/packages/builder/src/pages/builder/admin/_components/ImportAppsModal.svelte +++ b/packages/builder/src/pages/builder/admin/_components/ImportAppsModal.svelte @@ -1,6 +1,6 @@ @@ -36,10 +31,10 @@ onConfirm={importApps} disabled={!value.file} > - Please upload the file that was exported from your Cloud environment to get - started + + Please upload the file that was exported from your Cloud environment to get + started + { if (!cloud) { - await admin.checkImportComplete() + try { + await admin.checkImportComplete() + } catch (error) { + notifications.error("Error checking import status") + } } }) diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index 553125e1a7..1003936214 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -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 diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte index 6025758e71..a0df3a9d07 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte @@ -115,14 +115,13 @@ const { _id } = await queries.save(toSave.datasourceId, toSave) saveId = _id query = getSelectedQuery() - notifications.success(`Request saved successfully.`) - + notifications.success(`Request saved successfully`) if (dynamicVariables) { datasource.config.dynamicVariables = rebuildVariables(saveId) datasource = await datasources.save(datasource) } } catch (err) { - notifications.error(`Error saving query. ${err.message}`) + notifications.error(`Error saving query`) } } @@ -130,14 +129,14 @@ try { response = await queries.preview(buildQuery(query)) if (response.rows.length === 0) { - notifications.info("Request did not return any data.") + notifications.info("Request did not return any data") } else { response.info = response.info || { code: 200 } schema = response.schema - notifications.success("Request sent successfully.") + notifications.success("Request sent successfully") } - } catch (err) { - notifications.error(err) + } catch (error) { + notifications.error("Error running query") } } @@ -229,10 +228,24 @@ ) } + const updateFlag = async (flag, value) => { + try { + await flags.updateFlag(flag, value) + } catch (error) { + notifications.error("Error updating flag") + } + } + onMount(async () => { query = getSelectedQuery() - // clear any unsaved changes to the datasource - await datasources.init() + + try { + // Clear any unsaved changes to the datasource + await datasources.init() + } catch (error) { + notifications.error("Error getting datasources") + } + datasource = $datasources.list.find(ds => ds._id === query?.datasourceId) const datasourceUrl = datasource?.config.url const qs = query?.fields.queryString @@ -405,8 +418,7 @@ window.open( "https://docs.budibase.com/building-apps/data/transformers" )} - on:change={() => - flags.updateFlag("queryTransformerBanner", true)} + on:change={() => updateFlag("queryTransformerBanner", true)} > Add a JavaScript function to transform the query result. diff --git a/packages/builder/src/pages/builder/apps/index.svelte b/packages/builder/src/pages/builder/apps/index.svelte index c98e749e45..39cc780ac7 100644 --- a/packages/builder/src/pages/builder/apps/index.svelte +++ b/packages/builder/src/pages/builder/apps/index.svelte @@ -10,6 +10,7 @@ Icon, Body, Modal, + notifications, } from "@budibase/bbui" import { onMount } from "svelte" import { apps, organisation, auth } from "stores/portal" @@ -26,8 +27,12 @@ let changePasswordModal onMount(async () => { - await organisation.init() - await apps.load() + try { + await organisation.init() + await apps.load() + } catch (error) { + notifications.error("Error loading apps") + } loaded = true }) @@ -47,6 +52,14 @@ return `/${app.prodId}` } } + + const logout = async () => { + try { + await auth.logout() + } catch (error) { + // Swallow error and do nothing + } + } {#if $auth.user && loaded} @@ -82,7 +95,7 @@ Open developer mode {/if} - Log out + Log out diff --git a/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte b/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte index bae68b6548..27f5bde186 100644 --- a/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte +++ b/packages/builder/src/pages/builder/auth/_components/OIDCButton.svelte @@ -1,5 +1,5 @@ diff --git a/packages/builder/src/pages/builder/auth/index.svelte b/packages/builder/src/pages/builder/auth/index.svelte index a2a02e65c1..72b3a8c7cf 100644 --- a/packages/builder/src/pages/builder/auth/index.svelte +++ b/packages/builder/src/pages/builder/auth/index.svelte @@ -2,6 +2,7 @@ import { redirect } from "@roxi/routify" import { auth, admin } from "stores/portal" import { onMount } from "svelte" + import { notifications } from "@budibase/bbui" $: tenantSet = $auth.tenantSet $: multiTenancyEnabled = $admin.multiTenancy @@ -17,8 +18,12 @@ } onMount(async () => { - await admin.init() - await auth.checkQueryString() + try { + await admin.init() + await auth.checkQueryString() + } catch (error) { + notifications.error("Error getting checklist") + } loaded = true }) diff --git a/packages/builder/src/pages/builder/auth/login.svelte b/packages/builder/src/pages/builder/auth/login.svelte index 7a13164c51..d9151b4342 100644 --- a/packages/builder/src/pages/builder/auth/login.svelte +++ b/packages/builder/src/pages/builder/auth/login.svelte @@ -31,7 +31,6 @@ username, password, }) - if ($auth?.user?.forceResetPassword) { $goto("./reset") } else { @@ -39,8 +38,7 @@ $goto("../portal") } } catch (err) { - console.error(err) - notifications.error(err.message ? err.message : "Invalid Credentials") + notifications.error(err.message ? err.message : "Invalid credentials") } } @@ -49,7 +47,11 @@ } onMount(async () => { - await organisation.init() + try { + await organisation.init() + } catch (error) { + notifications.error("Error getting org config") + } loaded = true }) diff --git a/packages/builder/src/pages/builder/auth/org.svelte b/packages/builder/src/pages/builder/auth/org.svelte index 5a484b6c93..8fd94463d9 100644 --- a/packages/builder/src/pages/builder/auth/org.svelte +++ b/packages/builder/src/pages/builder/auth/org.svelte @@ -1,5 +1,13 @@ diff --git a/packages/builder/src/pages/builder/invite/index.svelte b/packages/builder/src/pages/builder/invite/index.svelte index ddf888ad73..c4745d8737 100644 --- a/packages/builder/src/pages/builder/invite/index.svelte +++ b/packages/builder/src/pages/builder/invite/index.svelte @@ -10,14 +10,11 @@ async function acceptInvite() { try { - const res = await users.acceptInvite(inviteCode, password) - if (!res) { - throw new Error(res.message) - } - notifications.success(`User created.`) + await users.acceptInvite(inviteCode, password) + notifications.success("Invitation accepted successfully") $goto("../auth/login") - } catch (err) { - notifications.error(err) + } catch (error) { + notifications.error("Error accepting invitation") } } diff --git a/packages/builder/src/pages/builder/portal/_layout.svelte b/packages/builder/src/pages/builder/portal/_layout.svelte index 8fca18d29d..f4679647ff 100644 --- a/packages/builder/src/pages/builder/portal/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/_layout.svelte @@ -10,6 +10,7 @@ MenuItem, Modal, clickOutside, + notifications, } from "@budibase/bbui" import ConfigChecklist from "components/common/ConfigChecklist.svelte" import { organisation, auth } from "stores/portal" @@ -78,6 +79,14 @@ return menu } + const logout = async () => { + try { + await auth.logout() + } catch (error) { + // Swallow error and do nothing + } + } + const showMobileMenu = () => (mobileMenuVisible = true) const hideMobileMenu = () => (mobileMenuVisible = false) @@ -87,7 +96,11 @@ if (!$auth.user?.builder?.global) { $redirect("../") } else { - await organisation.init() + try { + await organisation.init() + } catch (error) { + notifications.error("Error getting org config") + } loaded = true } } @@ -158,7 +171,7 @@ $goto("../apps")}> Close developer mode - Log out + Log out diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index bf783fdb86..b05aa1b659 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -19,7 +19,7 @@ import ChooseIconModal from "components/start/ChooseIconModal.svelte" import { store, automationStore } from "builderStore" - import api, { del, post, get } from "builderStore/api" + import { API } from "api" import { onMount } from "svelte" import { apps, auth, admin, templates } from "stores/portal" import download from "downloadjs" @@ -115,43 +115,29 @@ data.append("templateKey", template.key) // 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: appName, - appId: appJson.instance._id, + appId: createdApp.instance._id, template, fromTemplateMarketplace: true, }) // 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 userResp = await api.post(`/api/users/metadata/self`, { + await API.updateOwnMetadata({ roleId: "BASIC", }) - await userResp.json() 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") } } @@ -199,17 +185,11 @@ return } try { - const response = await del( - `/api/applications/${selectedApp.prodId}?unpublish=1` - ) - if (response.status !== 200) { - const json = await response.json() - throw json.message - } + await API.unpublishApp(selectedApp.prodId) await apps.load() notifications.success("App unpublished successfully") } catch (err) { - notifications.error(`Error unpublishing app: ${err}`) + notifications.error("Error unpublishing app") } } @@ -223,17 +203,13 @@ return } try { - const response = await del(`/api/applications/${selectedApp?.devId}`) - if (response.status !== 200) { - const json = await response.json() - throw json.message - } + await API.deleteApp(selectedApp?.devId) await apps.load() - // get checklist, just in case that was the last app + // Get checklist, just in case that was the last app await admin.init() notifications.success("App deleted successfully") } catch (err) { - notifications.error(`Error deleting app: ${err}`) + notifications.error("Error deleting app") } selectedApp = null appName = null @@ -246,15 +222,11 @@ const releaseLock = async app => { try { - const response = await del(`/api/dev/${app.devId}/lock`) - if (response.status !== 200) { - const json = await response.json() - throw json.message - } + await API.releaseAppLock(app.devId) await apps.load() notifications.success("Lock released successfully") } catch (err) { - notifications.error(`Error releasing lock: ${err}`) + notifications.error("Error releasing lock") } } @@ -272,17 +244,23 @@ } onMount(async () => { - await apps.load() - await templates.load() - if ($templates?.length === 0) { - notifications.error("There was a problem loading quick start templates.") - } - // if the portal is loaded from an external URL with a template param - const initInfo = await auth.getInitInfo() - if (initInfo?.init_template) { - creatingFromTemplate = true - createAppFromTemplateUrl(initInfo.init_template) - return + try { + await apps.load() + await templates.load() + if ($templates?.length === 0) { + notifications.error( + "There was a problem loading quick start templates." + ) + } + // If the portal is loaded from an external URL with a template param + const initInfo = await auth.getInitInfo() + if (initInfo?.init_template) { + creatingFromTemplate = true + createAppFromTemplateUrl(initInfo.init_template) + return + } + } catch (error) { + notifications.error("Error loading apps and templates") } loaded = true }) diff --git a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte index 20d30fdfbb..b001f02fe9 100644 --- a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte @@ -20,9 +20,9 @@ Toggle, } from "@budibase/bbui" import { onMount } from "svelte" - import api from "builderStore/api" + import { API } from "api" import { organisation, admin } from "stores/portal" - import { uuid } from "builderStore/uuid" + import { Helpers } from "@budibase/bbui" import analytics, { Events } from "analytics" const ConfigTypes = { @@ -137,17 +137,6 @@ providers.oidc?.config?.configs[0].clientID && providers.oidc?.config?.configs[0].clientSecret - async function uploadLogo(file) { - let data = new FormData() - data.append("file", file) - const res = await api.post( - `/api/global/configs/upload/logos_oidc/${file.name}`, - data, - {} - ) - return await res.json() - } - const onFileSelected = e => { let fileName = e.target.files[0].name image = e.target.files[0] @@ -156,17 +145,28 @@ } async function save(docs) { - // only if the user has provided an image, upload it. - image && uploadLogo(image) let calls = [] + + // Only if the user has provided an image, upload it + if (image) { + let data = new FormData() + data.append("file", image) + calls.push( + API.uploadOIDCLogo({ + name: image.name, + data, + }) + ) + } + docs.forEach(element => { if (element.type === ConfigTypes.OIDC) { - //Add a UUID here so each config is distinguishable when it arrives at the login page + // Add a UUID here so each config is distinguishable when it arrives at the login page for (let config of element.config.configs) { if (!config.uuid) { - config.uuid = uuid() + config.uuid = Helpers.uuid() } - // callback urls shouldn't be included + // Callback urls shouldn't be included delete config.callbackURL } if (partialOidc) { @@ -175,8 +175,8 @@ `Please fill in all required ${ConfigTypes.OIDC} fields` ) } else { - calls.push(api.post(`/api/global/configs`, element)) - // turn the save button grey when clicked + calls.push(API.saveConfig(element)) + // Turn the save button grey when clicked oidcSaveButtonDisabled = true originalOidcDoc = cloneDeep(providers.oidc) } @@ -189,71 +189,73 @@ `Please fill in all required ${ConfigTypes.Google} fields` ) } else { - calls.push(api.post(`/api/global/configs`, element)) + calls.push(API.saveConfig(element)) googleSaveButtonDisabled = true originalGoogleDoc = cloneDeep(providers.google) } } } }) - calls.length && + + if (calls.length) { Promise.all(calls) - .then(responses => { - return Promise.all( - responses.map(response => { - return response.json() - }) - ) - }) .then(data => { data.forEach(res => { providers[res.type]._rev = res._rev providers[res.type]._id = res._id }) - notifications.success(`Settings saved.`) + notifications.success(`Settings saved`) analytics.captureEvent(Events.SSO.SAVED) }) - .catch(err => { - notifications.error(`Failed to update auth settings. ${err}`) - throw new Error(err.message) + .catch(() => { + notifications.error("Failed to update auth settings") }) + } } onMount(async () => { - await organisation.init() - // fetch the configs for oauth - const googleResponse = await api.get( - `/api/global/configs/${ConfigTypes.Google}` - ) - const googleDoc = await googleResponse.json() + try { + await organisation.init() + } catch (error) { + notifications.error("Error getting org config") + } - if (!googleDoc._id) { + // Fetch Google config + let googleDoc + try { + googleDoc = await API.getConfig(ConfigTypes.Google) + } catch (error) { + notifications.error("Error fetching Google OAuth config") + } + if (!googleDoc?._id) { providers.google = { type: ConfigTypes.Google, config: { activated: true }, } originalGoogleDoc = cloneDeep(googleDoc) } else { - // default activated to true for older configs + // Default activated to true for older configs if (googleDoc.config.activated === undefined) { googleDoc.config.activated = true } originalGoogleDoc = cloneDeep(googleDoc) providers.google = googleDoc } - googleCallbackUrl = providers?.google?.config?.callbackURL - //Get the list of user uploaded logos and push it to the dropdown options. - //This needs to be done before the config call so they're available when the dropdown renders - const res = await api.get(`/api/global/configs/logos_oidc`) - const configSettings = await res.json() - - if (configSettings.config) { - const logoKeys = Object.keys(configSettings.config) - + // Get the list of user uploaded logos and push it to the dropdown options. + // This needs to be done before the config call so they're available when + // the dropdown renders. + let oidcLogos + try { + oidcLogos = await API.getOIDCLogos() + } catch (error) { + notifications.error("Error fetching OIDC logos") + } + if (oidcLogos?.config) { + const logoKeys = Object.keys(oidcLogos.config) logoKeys.map(logoKey => { - const logoUrl = configSettings.config[logoKey] + const logoUrl = oidcLogos.config[logoKey] iconDropdownOptions.unshift({ label: logoKey, value: logoKey, @@ -261,11 +263,15 @@ }) }) } - const oidcResponse = await api.get( - `/api/global/configs/${ConfigTypes.OIDC}` - ) - const oidcDoc = await oidcResponse.json() - if (!oidcDoc._id) { + + // Fetch OIDC config + let oidcDoc + try { + oidcDoc = await API.getConfig(ConfigTypes.OIDC) + } catch (error) { + notifications.error("Error fetching OIDC config") + } + if (!oidcDoc?._id) { providers.oidc = { type: ConfigTypes.OIDC, config: { configs: [{ activated: true }] }, diff --git a/packages/builder/src/pages/builder/portal/manage/email/[template].svelte b/packages/builder/src/pages/builder/portal/manage/email/[template].svelte index cc00f3d798..33ecca2a10 100644 --- a/packages/builder/src/pages/builder/portal/manage/email/[template].svelte +++ b/packages/builder/src/pages/builder/portal/manage/email/[template].svelte @@ -36,9 +36,9 @@ try { // Save your template config await email.templates.save(selectedTemplate) - notifications.success(`Template saved.`) - } catch (err) { - notifications.error(`Failed to update template settings. ${err}`) + notifications.success("Template saved") + } catch (error) { + notifications.error("Failed to update template settings") } } diff --git a/packages/builder/src/pages/builder/portal/manage/email/_layout.svelte b/packages/builder/src/pages/builder/portal/manage/email/_layout.svelte index 410a7d4ff2..e371c2daae 100644 --- a/packages/builder/src/pages/builder/portal/manage/email/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/manage/email/_layout.svelte @@ -1,7 +1,15 @@ diff --git a/packages/builder/src/pages/builder/portal/manage/email/index.svelte b/packages/builder/src/pages/builder/portal/manage/email/index.svelte index 5a78623b81..4ef59d2daa 100644 --- a/packages/builder/src/pages/builder/portal/manage/email/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/email/index.svelte @@ -14,7 +14,7 @@ Checkbox, } from "@budibase/bbui" import { email } from "stores/portal" - import api from "builderStore/api" + import { API } from "api" import { cloneDeep } from "lodash/fp" import analytics, { Events } from "analytics" @@ -54,55 +54,48 @@ delete smtp.config.auth } // Save your SMTP config - const response = await api.post(`/api/global/configs`, smtp) - - if (response.status !== 200) { - const error = await response.text() - let message - try { - message = JSON.parse(error).message - } catch (err) { - message = error - } - notifications.error(`Failed to save email settings, reason: ${message}`) - } else { - const json = await response.json() - smtpConfig._rev = json._rev - smtpConfig._id = json._id - notifications.success(`Settings saved.`) + try { + const savedConfig = await API.saveConfig(smtp) + smtpConfig._rev = savedConfig._rev + smtpConfig._id = savedConfig._id + notifications.success(`Settings saved`) analytics.captureEvent(Events.SMTP.SAVED) + } catch (error) { + notifications.error( + `Failed to save email settings, reason: ${error?.message || "Unknown"}` + ) } } async function fetchSmtp() { loading = true - // fetch the configs for smtp - const smtpResponse = await api.get( - `/api/global/configs/${ConfigTypes.SMTP}` - ) - const smtpDoc = await smtpResponse.json() - - if (!smtpDoc._id) { - smtpConfig = { - type: ConfigTypes.SMTP, - config: { - secure: true, - }, + try { + // Fetch the configs for smtp + const smtpDoc = await API.getConfig(ConfigTypes.SMTP) + if (!smtpDoc._id) { + smtpConfig = { + type: ConfigTypes.SMTP, + config: { + secure: true, + }, + } + } else { + smtpConfig = smtpDoc } - } else { - smtpConfig = smtpDoc - } - loading = false - requireAuth = smtpConfig.config.auth != null - // always attach the auth for the forms purpose - - // this will be removed later if required - if (!smtpDoc.config) { - smtpDoc.config = {} - } - if (!smtpDoc.config.auth) { - smtpConfig.config.auth = { - type: "login", + loading = false + requireAuth = smtpConfig.config.auth != null + // Always attach the auth for the forms purpose - + // this will be removed later if required + if (!smtpDoc.config) { + smtpDoc.config = {} } + if (!smtpDoc.config.auth) { + smtpConfig.config.auth = { + type: "login", + } + } + } catch (error) { + notifications.error("Error fetching SMTP config") } } diff --git a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte index 549d0e4334..a8cb340465 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte @@ -64,31 +64,43 @@ const apps = fetchData(`/api/global/roles`) async function deleteUser() { - const res = await users.delete(userId) - if (res.status === 200) { + try { + await users.delete(userId) notifications.success(`User ${$userFetch?.data?.email} deleted.`) $goto("./") - } else { - notifications.error(res?.message ? res.message : "Failed to delete user.") + } catch (error) { + notifications.error("Error deleting user") } } let toggleDisabled = false async function updateUserFirstName(evt) { - await users.save({ ...$userFetch?.data, firstName: evt.target.value }) - await userFetch.refresh() + try { + await users.save({ ...$userFetch?.data, firstName: evt.target.value }) + await userFetch.refresh() + } catch (error) { + notifications.error("Error updating user") + } } async function updateUserLastName(evt) { - await users.save({ ...$userFetch?.data, lastName: evt.target.value }) - await userFetch.refresh() + try { + await users.save({ ...$userFetch?.data, lastName: evt.target.value }) + await userFetch.refresh() + } catch (error) { + notifications.error("Error updating user") + } } async function toggleFlag(flagName, detail) { toggleDisabled = true - await users.save({ ...$userFetch?.data, [flagName]: { global: detail } }) - await userFetch.refresh() + try { + await users.save({ ...$userFetch?.data, [flagName]: { global: detail } }) + await userFetch.refresh() + } catch (error) { + notifications.error("Error updating user") + } toggleDisabled = false } diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte index 25a69af1c8..0255784a7b 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte @@ -21,12 +21,12 @@ const [email, error, touched] = createValidationStore("", emailValidator) async function createUserFlow() { - const res = await users.invite({ email: $email, builder, admin }) - if (res.status) { - notifications.error(res.message) - } else { + try { + const res = await users.invite({ email: $email, builder, admin }) notifications.success(res.message) analytics.captureEvent(Events.USER.INVITE, { type: selected }) + } catch (error) { + notifications.error("Error inviting user") } } diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte index ff958d542b..29e2d56ed0 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/BasicOnboardingModal.svelte @@ -16,17 +16,17 @@ admin = false async function createUser() { - const res = await users.create({ - email: $email, - password, - builder, - admin, - forceResetPassword: true, - }) - if (res.status) { - notifications.error(res.message) - } else { + try { + await users.create({ + email: $email, + password, + builder, + admin, + forceResetPassword: true, + }) notifications.success("Successfully created user") + } catch (error) { + notifications.error("Error creating user") } } diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/ForceResetPasswordModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/ForceResetPasswordModal.svelte index 6468498df8..a380f0aa65 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/ForceResetPasswordModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/ForceResetPasswordModal.svelte @@ -10,16 +10,16 @@ const password = Math.random().toString(36).substr(2, 20) async function resetPassword() { - const res = await users.save({ - ...user, - password, - forceResetPassword: true, - }) - if (res.status) { - notifications.error(res.message) - } else { - notifications.success("Password reset.") + try { + await users.save({ + ...user, + password, + forceResetPassword: true, + }) + notifications.success("Password reset successfully") dispatch("update") + } catch (error) { + notifications.error("Error resetting password") } } diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte index afa4c84f0e..5a60bfdff8 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte @@ -18,33 +18,31 @@ let selectedRole = user?.roles?.[app?._id] async function updateUserRoles() { - let res - if (selectedRole === NO_ACCESS) { - // remove the user role - const filteredRoles = { ...user.roles } - delete filteredRoles[app?._id] - res = await users.save({ - ...user, - roles: { - ...filteredRoles, - }, - }) - } else { - // add the user role - res = await users.save({ - ...user, - roles: { - ...user.roles, - [app._id]: selectedRole, - }, - }) - } - - if (res.status === 400) { - notifications.error("Failed to update role") - } else { + try { + if (selectedRole === NO_ACCESS) { + // Remove the user role + const filteredRoles = { ...user.roles } + delete filteredRoles[app?._id] + await users.save({ + ...user, + roles: { + ...filteredRoles, + }, + }) + } else { + // Add the user role + await users.save({ + ...user, + roles: { + ...user.roles, + [app._id]: selectedRole, + }, + }) + } notifications.success("Role updated") dispatch("update") + } catch (error) { + notifications.error("Failed to update role") } } diff --git a/packages/builder/src/pages/builder/portal/manage/users/index.svelte b/packages/builder/src/pages/builder/portal/manage/users/index.svelte index 124115a486..61192063cc 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/index.svelte @@ -11,13 +11,13 @@ Label, Layout, Modal, + notifications, } from "@budibase/bbui" import TagsRenderer from "./_components/TagsTableRenderer.svelte" import AddUserModal from "./_components/AddUserModal.svelte" import BasicOnboardingModal from "./_components/BasicOnboardingModal.svelte" import { users } from "stores/portal" - - users.init() + import { onMount } from "svelte" const schema = { email: {}, @@ -47,6 +47,14 @@ createUserModal.hide() basicOnboardingModal.show() } + + onMount(async () => { + try { + await users.init() + } catch (error) { + notifications.error("Error getting user list") + } + }) diff --git a/packages/builder/src/pages/builder/portal/settings/organisation.svelte b/packages/builder/src/pages/builder/portal/settings/organisation.svelte index 6903854922..7094a0af01 100644 --- a/packages/builder/src/pages/builder/portal/settings/organisation.svelte +++ b/packages/builder/src/pages/builder/portal/settings/organisation.svelte @@ -11,7 +11,7 @@ notifications, } from "@budibase/bbui" import { auth, organisation, admin } from "stores/portal" - import { post } from "builderStore/api" + import { API } from "api" import { writable } from "svelte/store" import { redirect } from "@roxi/routify" @@ -32,42 +32,40 @@ let loading = false async function uploadLogo(file) { - let data = new FormData() - data.append("file", file) - const res = await post( - "/api/global/configs/upload/settings/logoUrl", - data, - {} - ) - return await res.json() + try { + let data = new FormData() + data.append("file", file) + await API.uploadLogo(data) + } catch (error) { + notifications.error("Error uploading logo") + } } async function saveConfig() { loading = true - // Upload logo if required - if ($values.logo && !$values.logo.url) { - await uploadLogo($values.logo) - await organisation.init() - } + try { + // Upload logo if required + if ($values.logo && !$values.logo.url) { + await uploadLogo($values.logo) + await organisation.init() + } - const config = { - company: $values.company ?? "", - platformUrl: $values.platformUrl ?? "", - } - // remove logo if required - if (!$values.logo) { - config.logoUrl = "" - } + const config = { + company: $values.company ?? "", + platformUrl: $values.platformUrl ?? "", + } - // Update settings - const res = await organisation.save(config) - if (res.status === 200) { - notifications.success("Settings saved successfully") - } else { - notifications.error(res.message) - } + // Remove logo if required + if (!$values.logo) { + config.logoUrl = "" + } + // Update settings + await organisation.save(config) + } catch (error) { + notifications.error("Error saving org config") + } loading = false } diff --git a/packages/builder/src/pages/builder/portal/settings/update.svelte b/packages/builder/src/pages/builder/portal/settings/update.svelte index 5deb724a7c..d87736144d 100644 --- a/packages/builder/src/pages/builder/portal/settings/update.svelte +++ b/packages/builder/src/pages/builder/portal/settings/update.svelte @@ -9,7 +9,7 @@ notifications, Label, } from "@budibase/bbui" - import api from "builderStore/api" + import { API } from "api" import { auth, admin } from "stores/portal" import { redirect } from "@roxi/routify" @@ -38,8 +38,12 @@ } async function getVersion() { - const response = await api.get("/api/dev/version") - version = await response.text() + try { + version = await API.getBudibaseVersion() + } catch (error) { + notifications.error("Error getting Budibase version") + version = null + } } onMount(() => { diff --git a/packages/builder/src/pages/index.svelte b/packages/builder/src/pages/index.svelte index 477097f726..c6eaba8ff1 100644 --- a/packages/builder/src/pages/index.svelte +++ b/packages/builder/src/pages/index.svelte @@ -2,10 +2,14 @@ import { redirect } from "@roxi/routify" import { auth } from "../stores/portal" import { onMount } from "svelte" + import { notifications } from "@budibase/bbui" - auth.checkQueryString() - - onMount(() => { + onMount(async () => { + try { + await auth.checkQueryString() + } catch (error) { + notifications.error("Error setting org") + } $redirect(`./builder`) }) diff --git a/packages/builder/src/stores/backend/datasources.js b/packages/builder/src/stores/backend/datasources.js index 7810c3a950..2423394c6a 100644 --- a/packages/builder/src/stores/backend/datasources.js +++ b/packages/builder/src/stores/backend/datasources.js @@ -1,6 +1,6 @@ import { writable, get } from "svelte/store" import { queries, tables, views } from "./" -import api from "../../builderStore/api" +import { API } from "api" export const INITIAL_DATASOURCE_VALUES = { list: [], @@ -13,23 +13,20 @@ export function createDatasourcesStore() { const { subscribe, update, set } = store async function updateDatasource(response) { - if (response.status !== 200) { - throw new Error(await response.text()) - } - - const { datasource, error } = await response.json() + const { datasource, error } = response update(state => { const currentIdx = state.list.findIndex(ds => ds._id === datasource._id) - const sources = state.list - if (currentIdx >= 0) { sources.splice(currentIdx, 1, datasource) } else { sources.push(datasource) } - - return { list: sources, selected: datasource._id, schemaError: error } + return { + list: sources, + selected: datasource._id, + schemaError: error, + } }) return datasource } @@ -38,25 +35,25 @@ export function createDatasourcesStore() { subscribe, update, init: async () => { - const response = await api.get(`/api/datasources`) - const json = await response.json() - set({ list: json, selected: null }) + const datasources = await API.getDatasources() + set({ + list: datasources, + selected: null, + }) }, fetch: async () => { - const response = await api.get(`/api/datasources`) - const json = await response.json() + const datasources = await API.getDatasources() // Clear selected if it no longer exists, otherwise keep it const selected = get(store).selected let nextSelected = null - if (selected && json.find(source => source._id === selected)) { + if (selected && datasources.find(source => source._id === selected)) { nextSelected = selected } - update(state => ({ ...state, list: json, selected: nextSelected })) - return json + update(state => ({ ...state, list: datasources, selected: nextSelected })) }, - select: async datasourceId => { + select: datasourceId => { update(state => ({ ...state, selected: datasourceId })) queries.unselect() tables.unselect() @@ -66,37 +63,33 @@ export function createDatasourcesStore() { update(state => ({ ...state, selected: null })) }, updateSchema: async datasource => { - let url = `/api/datasources/${datasource._id}/schema` - - const response = await api.post(url) - return updateDatasource(response) + const response = await API.buildDatasourceSchema(datasource?._id) + return await updateDatasource(response) }, save: async (body, fetchSchema = false) => { let response if (body._id) { - response = await api.put(`/api/datasources/${body._id}`, body) + response = await API.updateDatasource(body) } else { - response = await api.post("/api/datasources", { + response = await API.createDatasource({ datasource: body, fetchSchema, }) } - return updateDatasource(response) }, delete: async datasource => { - const response = await api.delete( - `/api/datasources/${datasource._id}/${datasource._rev}` - ) + await API.deleteDatasource({ + datasourceId: datasource?._id, + datasourceRev: datasource?._rev, + }) update(state => { const sources = state.list.filter( existing => existing._id !== datasource._id ) return { list: sources, selected: null } }) - await queries.fetch() - return response }, removeSchemaError: () => { update(state => { diff --git a/packages/builder/src/stores/backend/flags.js b/packages/builder/src/stores/backend/flags.js index 7e5adcd00f..449d010640 100644 --- a/packages/builder/src/stores/backend/flags.js +++ b/packages/builder/src/stores/backend/flags.js @@ -1,37 +1,27 @@ import { writable } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" export function createFlagsStore() { const { subscribe, set } = writable({}) - return { - subscribe, + const actions = { fetch: async () => { - const { doc, response } = await getFlags() - set(doc) - return response + const flags = await API.getFlags() + set(flags) }, updateFlag: async (flag, value) => { - const response = await api.post("/api/users/flags", { + await API.updateFlag({ flag, value, }) - if (response.status === 200) { - const { doc } = await getFlags() - set(doc) - } - return response + await actions.fetch() }, } -} -async function getFlags() { - const response = await api.get("/api/users/flags") - let doc = {} - if (response.status === 200) { - doc = await response.json() + return { + subscribe, + ...actions, } - return { doc, response } } export const flags = createFlagsStore() diff --git a/packages/builder/src/stores/backend/integrations.js b/packages/builder/src/stores/backend/integrations.js index d1df818248..717b656c72 100644 --- a/packages/builder/src/stores/backend/integrations.js +++ b/packages/builder/src/stores/backend/integrations.js @@ -1,3 +1,16 @@ import { writable } from "svelte/store" +import { API } from "api" -export const integrations = writable({}) +const createIntegrationsStore = () => { + const store = writable(null) + + return { + ...store, + init: async () => { + const integrations = await API.getIntegrations() + store.set(integrations) + }, + } +} + +export const integrations = createIntegrationsStore() diff --git a/packages/builder/src/stores/backend/permissions.js b/packages/builder/src/stores/backend/permissions.js index 29159494ed..aaab406bc9 100644 --- a/packages/builder/src/stores/backend/permissions.js +++ b/packages/builder/src/stores/backend/permissions.js @@ -1,5 +1,5 @@ import { writable } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" export function createPermissionStore() { const { subscribe } = writable([]) @@ -7,14 +7,14 @@ export function createPermissionStore() { return { subscribe, save: async ({ level, role, resource }) => { - const response = await api.post( - `/api/permission/${role}/${resource}/${level}` - ) - return await response.json() + return await API.updatePermissionForResource({ + resourceId: resource, + roleId: role, + level, + }) }, forResource: async resourceId => { - const response = await api.get(`/api/permission/${resourceId}`) - return await response.json() + return await API.getPermissionForResource(resourceId) }, } } diff --git a/packages/builder/src/stores/backend/queries.js b/packages/builder/src/stores/backend/queries.js index 2018933ffc..6e30cb21f8 100644 --- a/packages/builder/src/stores/backend/queries.js +++ b/packages/builder/src/stores/backend/queries.js @@ -1,6 +1,6 @@ import { writable, get } from "svelte/store" import { datasources, integrations, tables, views } from "./" -import api from "builderStore/api" +import { API } from "api" import { duplicateName } from "../../helpers/duplicate" const sortQueries = queryList => { @@ -15,23 +15,26 @@ export function createQueriesStore() { const actions = { init: async () => { - const response = await api.get(`/api/queries`) - const json = await response.json() - set({ list: json, selected: null }) + const queries = await API.getQueries() + set({ + list: queries, + selected: null, + }) }, fetch: async () => { - const response = await api.get(`/api/queries`) - const json = await response.json() - sortQueries(json) - update(state => ({ ...state, list: json })) - return json + const queries = await API.getQueries() + sortQueries(queries) + update(state => ({ + ...state, + list: queries, + })) }, save: async (datasourceId, query) => { const _integrations = get(integrations) const dataSource = get(datasources).list.filter( ds => ds._id === datasourceId ) - // check if readable attribute is found + // Check if readable attribute is found if (dataSource.length !== 0) { const integration = _integrations[dataSource[0].source] const readable = integration.query[query.queryVerb].readable @@ -40,34 +43,28 @@ export function createQueriesStore() { } } query.datasourceId = datasourceId - const response = await api.post(`/api/queries`, query) - if (response.status !== 200) { - throw new Error("Failed saving query.") - } - const json = await response.json() + const savedQuery = await API.saveQuery(query) update(state => { - const currentIdx = state.list.findIndex(query => query._id === json._id) - + const idx = state.list.findIndex(query => query._id === savedQuery._id) const queries = state.list - - if (currentIdx >= 0) { - queries.splice(currentIdx, 1, json) + if (idx >= 0) { + queries.splice(idx, 1, savedQuery) } else { - queries.push(json) + queries.push(savedQuery) } sortQueries(queries) - return { list: queries, selected: json._id } + return { + list: queries, + selected: savedQuery._id, + } }) - return json + return savedQuery }, - import: async body => { - const response = await api.post(`/api/queries/import`, body) - - if (response.status !== 200) { - throw new Error(response.message) - } - - return response.json() + import: async (data, datasourceId) => { + return await API.importQueries({ + datasourceId, + data, + }) }, select: query => { update(state => ({ ...state, selected: query._id })) @@ -79,48 +76,37 @@ export function createQueriesStore() { update(state => ({ ...state, selected: null })) }, preview: async query => { - const response = await api.post("/api/queries/preview", { - fields: query.fields, - queryVerb: query.queryVerb, - transformer: query.transformer, - parameters: query.parameters.reduce( - (acc, next) => ({ - ...acc, - [next.name]: next.default, - }), - {} - ), - datasourceId: query.datasourceId, - queryId: query._id || undefined, + const parameters = query.parameters.reduce( + (acc, next) => ({ + ...acc, + [next.name]: next.default, + }), + {} + ) + const result = await API.previewQuery({ + ...query, + parameters, }) - - if (response.status !== 200) { - const error = await response.text() - throw `Query error: ${error}` - } - - const json = await response.json() // Assume all the fields are strings and create a basic schema from the // unique fields returned by the server const schema = {} - for (let field of json.schemaFields) { + for (let field of result.schemaFields) { schema[field] = "string" } - return { ...json, schema, rows: json.rows || [] } + return { ...result, schema, rows: result.rows || [] } }, delete: async query => { - const response = await api.delete( - `/api/queries/${query._id}/${query._rev}` - ) + await API.deleteQuery({ + queryId: query?._id, + queryRev: query?._rev, + }) update(state => { state.list = state.list.filter(existing => existing._id !== query._id) if (state.selected === query._id) { state.selected = null } - return state }) - return response }, duplicate: async query => { let list = get(store).list diff --git a/packages/builder/src/stores/backend/roles.js b/packages/builder/src/stores/backend/roles.js index 1a1a9c04c5..0c3cdbce5a 100644 --- a/packages/builder/src/stores/backend/roles.js +++ b/packages/builder/src/stores/backend/roles.js @@ -1,30 +1,32 @@ import { writable } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" export function createRolesStore() { const { subscribe, update, set } = writable([]) - return { - subscribe, + const actions = { fetch: async () => { - set(await getRoles()) + const roles = await API.getRoles() + set(roles) }, delete: async role => { - const response = await api.delete(`/api/roles/${role._id}/${role._rev}`) + await API.deleteRole({ + roleId: role?._id, + roleRev: role?._rev, + }) update(state => state.filter(existing => existing._id !== role._id)) - return response }, save: async role => { - const response = await api.post("/api/roles", role) - set(await getRoles()) - return response + const savedRole = await API.saveRole(role) + await actions.fetch() + return savedRole }, } + + return { + subscribe, + ...actions, + } } -async function getRoles() { - const response = await api.get("/api/roles") - return await response.json() -} - export const roles = createRolesStore() diff --git a/packages/builder/src/stores/backend/tables.js b/packages/builder/src/stores/backend/tables.js index 02db48c549..f6d20037cb 100644 --- a/packages/builder/src/stores/backend/tables.js +++ b/packages/builder/src/stores/backend/tables.js @@ -1,7 +1,7 @@ import { get, writable } from "svelte/store" import { datasources, queries, views } from "./" import { cloneDeep } from "lodash/fp" -import api from "builderStore/api" +import { API } from "api" import { SWITCHABLE_TYPES } from "../../constants/backend" export function createTablesStore() { @@ -9,10 +9,11 @@ export function createTablesStore() { const { subscribe, update, set } = store async function fetch() { - const tablesResponse = await api.get(`/api/tables`) - const tables = await tablesResponse.json() - update(state => ({ ...state, list: tables })) - return tables + const tables = await API.getTables() + update(state => ({ + ...state, + list: tables, + })) } async function select(table) { @@ -38,16 +39,16 @@ export function createTablesStore() { const oldTable = get(store).list.filter(t => t._id === table._id)[0] const fieldNames = [] - // update any renamed schema keys to reflect their names + // Update any renamed schema keys to reflect their names for (let key of Object.keys(updatedTable.schema)) { - // if field name has been seen before remove it + // If field name has been seen before remove it if (fieldNames.indexOf(key.toLowerCase()) !== -1) { delete updatedTable.schema[key] continue } const field = updatedTable.schema[key] const oldField = oldTable?.schema[key] - // if the type has changed then revert back to the old field + // If the type has changed then revert back to the old field if ( oldField != null && oldField?.type !== field.type && @@ -55,21 +56,17 @@ export function createTablesStore() { ) { updatedTable.schema[key] = oldField } - // field has been renamed + // Field has been renamed if (field.name && field.name !== key) { updatedTable.schema[field.name] = field updatedTable._rename = { old: key, updated: field.name } delete updatedTable.schema[key] } - // finally record this field has been used + // Finally record this field has been used fieldNames.push(key.toLowerCase()) } - const response = await api.post(`/api/tables`, updatedTable) - if (response.status !== 200) { - throw (await response.json()).message - } - const savedTable = await response.json() + const savedTable = await API.saveTable(updatedTable) await fetch() if (table.type === "external") { await datasources.fetch() @@ -91,21 +88,18 @@ export function createTablesStore() { }, save, init: async () => { - const response = await api.get("/api/tables") - const json = await response.json() + const tables = await API.getTables() set({ - list: json, + list: tables, selected: {}, draft: {}, }) }, delete: async table => { - const response = await api.delete( - `/api/tables/${table._id}/${table._rev}` - ) - if (response.status !== 200) { - throw (await response.json()).message - } + await API.deleteTable({ + tableId: table?._id, + tableRev: table?._rev, + }) update(state => ({ ...state, list: state.list.filter(existing => existing._id !== table._id), @@ -156,12 +150,16 @@ export function createTablesStore() { await promise } }, - deleteField: field => { + deleteField: async field => { + let promise update(state => { delete state.draft.schema[field.name] - save(state.draft) + promise = save(state.draft) return state }) + if (promise) { + await promise + } }, } } diff --git a/packages/builder/src/stores/backend/views.js b/packages/builder/src/stores/backend/views.js index 14c7bf92a4..849a66f671 100644 --- a/packages/builder/src/stores/backend/views.js +++ b/packages/builder/src/stores/backend/views.js @@ -1,6 +1,6 @@ import { writable, get } from "svelte/store" import { tables, datasources, queries } from "./" -import api from "builderStore/api" +import { API } from "api" export function createViewsStore() { const { subscribe, update } = writable({ @@ -11,7 +11,7 @@ export function createViewsStore() { return { subscribe, update, - select: async view => { + select: view => { update(state => ({ ...state, selected: view, @@ -27,16 +27,14 @@ export function createViewsStore() { })) }, delete: async view => { - await api.delete(`/api/views/${view}`) + await API.deleteView(view) await tables.fetch() }, save: async view => { - const response = await api.post(`/api/views`, view) - const json = await response.json() - + const savedView = await API.saveView(view) const viewMeta = { name: view.name, - ...json, + ...savedView, } const viewTable = get(tables).list.find( diff --git a/packages/builder/src/stores/portal/admin.js b/packages/builder/src/stores/portal/admin.js index d98eae8363..dc68c43cc5 100644 --- a/packages/builder/src/stores/portal/admin.js +++ b/packages/builder/src/stores/portal/admin.js @@ -1,5 +1,5 @@ import { writable, get } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" import { auth } from "stores/portal" export function createAdminStore() { @@ -23,64 +23,37 @@ export function createAdminStore() { const admin = writable(DEFAULT_CONFIG) async function init() { - try { - const tenantId = get(auth).tenantId - const response = await api.get( - `/api/global/configs/checklist?tenantId=${tenantId}` - ) - const json = await response.json() - const totalSteps = Object.keys(json).length - const completedSteps = Object.values(json).filter(x => x?.checked).length - - await getEnvironment() - admin.update(store => { - store.loaded = true - store.checklist = json - store.onboardingProgress = (completedSteps / totalSteps) * 100 - return store - }) - } catch (err) { - admin.update(store => { - store.checklist = null - return store - }) - } + const tenantId = get(auth).tenantId + const checklist = await API.getChecklist(tenantId) + const totalSteps = Object.keys(checklist).length + const completedSteps = Object.values(checklist).filter( + x => x?.checked + ).length + await getEnvironment() + admin.update(store => { + store.loaded = true + store.checklist = checklist + store.onboardingProgress = (completedSteps / totalSteps) * 100 + return store + }) } async function checkImportComplete() { - const response = await api.get(`/api/cloud/import/complete`) - if (response.status === 200) { - const json = await response.json() - admin.update(store => { - store.importComplete = json ? json.imported : false - return store - }) - } + const result = await API.checkImportComplete() + admin.update(store => { + store.importComplete = result ? result.imported : false + return store + }) } async function getEnvironment() { - let multiTenancyEnabled = false - let cloud = false - let disableAccountPortal = false - let accountPortalUrl = "" - let isDev = false - try { - const response = await api.get(`/api/system/environment`) - const json = await response.json() - multiTenancyEnabled = json.multiTenancy - cloud = json.cloud - disableAccountPortal = json.disableAccountPortal - accountPortalUrl = json.accountPortalUrl - isDev = json.isDev - } catch (err) { - // just let it stay disabled - } + const environment = await API.getEnvironment() admin.update(store => { - store.multiTenancy = multiTenancyEnabled - store.cloud = cloud - store.disableAccountPortal = disableAccountPortal - store.accountPortalUrl = accountPortalUrl - store.isDev = isDev + store.multiTenancy = environment.multiTenancy + store.cloud = environment.cloud + store.disableAccountPortal = environment.disableAccountPortal + store.accountPortalUrl = environment.accountPortalUrl + store.isDev = environment.isDev return store }) } diff --git a/packages/builder/src/stores/portal/apps.js b/packages/builder/src/stores/portal/apps.js index de944c057d..b8fb8c5670 100644 --- a/packages/builder/src/stores/portal/apps.js +++ b/packages/builder/src/stores/portal/apps.js @@ -1,7 +1,6 @@ import { writable } from "svelte/store" -import { get } from "builderStore/api" import { AppStatus } from "../../constants" -import api from "../../builderStore/api" +import { API } from "api" const extractAppId = id => { const split = id?.split("_") || [] @@ -12,77 +11,67 @@ export function createAppStore() { const store = writable([]) async function load() { - try { - const res = await get(`/api/applications?status=all`) - const json = await res.json() - if (res.ok && Array.isArray(json)) { - // Merge apps into one sensible list - let appMap = {} - let devApps = json.filter(app => app.status === AppStatus.DEV) - let deployedApps = json.filter(app => app.status === AppStatus.DEPLOYED) + const json = await API.getApps() + if (Array.isArray(json)) { + // Merge apps into one sensible list + let appMap = {} + let devApps = json.filter(app => app.status === AppStatus.DEV) + let deployedApps = json.filter(app => app.status === AppStatus.DEPLOYED) - // First append all dev app version - devApps.forEach(app => { - const id = extractAppId(app.appId) - appMap[id] = { - ...app, - devId: app.appId, - devRev: app._rev, - } - }) + // First append all dev app version + devApps.forEach(app => { + const id = extractAppId(app.appId) + appMap[id] = { + ...app, + devId: app.appId, + devRev: app._rev, + } + }) - // Then merge with all prod app versions - deployedApps.forEach(app => { - const id = extractAppId(app.appId) + // Then merge with all prod app versions + deployedApps.forEach(app => { + const id = extractAppId(app.appId) - // Skip any deployed apps which don't have a dev counterpart - if (!appMap[id]) { - return - } + // Skip any deployed apps which don't have a dev counterpart + if (!appMap[id]) { + return + } - appMap[id] = { - ...appMap[id], - ...app, - prodId: app.appId, - prodRev: app._rev, - } - }) + appMap[id] = { + ...appMap[id], + ...app, + prodId: app.appId, + prodRev: app._rev, + } + }) - // Transform into an array and clean up - const apps = Object.values(appMap) - apps.forEach(app => { - app.appId = extractAppId(app.devId) - delete app._id - delete app._rev - }) - store.set(apps) - } else { - store.set([]) - } - return json - } catch (error) { + // Transform into an array and clean up + const apps = Object.values(appMap) + apps.forEach(app => { + app.appId = extractAppId(app.devId) + delete app._id + delete app._rev + }) + store.set(apps) + } else { store.set([]) } } async function update(appId, value) { - console.log({ value }) - const response = await api.put(`/api/applications/${appId}`, { ...value }) - if (response.status === 200) { - store.update(state => { - const updatedAppIndex = state.findIndex( - app => app.instance._id === appId - ) - if (updatedAppIndex !== -1) { - let updatedApp = state[updatedAppIndex] - updatedApp = { ...updatedApp, ...value } - state.apps = state.splice(updatedAppIndex, 1, updatedApp) - } - return state - }) - } else { - throw new Error("Error updating name") - } + await API.saveAppMetadata({ + appId, + metadata: value, + }) + store.update(state => { + const updatedAppIndex = state.findIndex(app => app.instance._id === appId) + if (updatedAppIndex !== -1) { + let updatedApp = state[updatedAppIndex] + updatedApp = { ...updatedApp, ...value } + state.apps = state.splice(updatedAppIndex, 1, updatedApp) + } + return state + }) } return { diff --git a/packages/builder/src/stores/portal/auth.js b/packages/builder/src/stores/portal/auth.js index c4197a89c0..d66e901163 100644 --- a/packages/builder/src/stores/portal/auth.js +++ b/packages/builder/src/stores/portal/auth.js @@ -1,5 +1,5 @@ import { derived, writable, get } from "svelte/store" -import api from "../../builderStore/api" +import { API } from "api" import { admin } from "stores/portal" import analytics from "analytics" @@ -54,18 +54,25 @@ export function createAuthStore() { }) if (user) { - analytics.activate().then(() => { - analytics.identify(user._id, user) - analytics.showChat({ - email: user.email, - created_at: (user.createdAt || Date.now()) / 1000, - name: user.account?.name, - user_id: user._id, - tenant: user.tenantId, - "Company size": user.account?.size, - "Job role": user.account?.profession, + analytics + .activate() + .then(() => { + analytics.identify(user._id, user) + analytics.showChat({ + email: user.email, + created_at: (user.createdAt || Date.now()) / 1000, + name: user.account?.name, + user_id: user._id, + tenant: user.tenantId, + "Company size": user.account?.size, + "Job role": user.account?.profession, + }) + }) + .catch(() => { + // This request may fail due to browser extensions blocking requests + // containing the word analytics, so we don't want to spam users with + // an error here. }) - }) } } @@ -83,7 +90,7 @@ export function createAuthStore() { } async function setInitInfo(info) { - await api.post(`/api/global/auth/init`, info) + await API.setInitInfo(info) auth.update(store => { store.initInfo = info return store @@ -91,7 +98,7 @@ export function createAuthStore() { return info } - async function setPostLogout() { + function setPostLogout() { auth.update(store => { store.postLogout = true return store @@ -99,13 +106,12 @@ export function createAuthStore() { } async function getInitInfo() { - const response = await api.get(`/api/global/auth/init`) - const json = response.json() + const info = await API.getInitInfo() auth.update(store => { - store.initInfo = json + store.initInfo = info return store }) - return json + return info } const actions = { @@ -120,76 +126,51 @@ export function createAuthStore() { await setOrganisation(tenantId) }, getSelf: async () => { - const response = await api.get("/api/global/users/self") - if (response.status !== 200) { + // We need to catch this locally as we never want this to fail, even + // though normally we never want to swallow API errors at the store level. + // We're either logged in or we aren't. + // We also need to always update the loaded flag. + try { + const user = await API.fetchBuilderSelf() + setUser(user) + } catch (error) { setUser(null) - } else { - const json = await response.json() - setUser(json) } }, login: async creds => { const tenantId = get(store).tenantId - const response = await api.post( - `/api/global/auth/${tenantId}/login`, - creds - ) - if (response.status === 200) { - await actions.getSelf() - } else { - const json = await response.json() - throw new Error(json.message ? json.message : "Invalid credentials") - } + await API.logIn({ + username: creds.username, + password: creds.password, + tenantId, + }) + await actions.getSelf() }, logout: async () => { - const response = await api.post(`/api/global/auth/logout`) - if (response.status !== 200) { - throw "Unable to create logout" - } - await response.json() - await setInitInfo({}) setUser(null) setPostLogout() + await API.logOut() + await setInitInfo({}) }, updateSelf: async fields => { const newUser = { ...get(auth).user, ...fields } - const response = await api.post("/api/global/users/self", newUser) - if (response.status === 200) { - setUser(newUser) - } else { - throw "Unable to update user details" - } + await API.updateSelf(newUser) + setUser(newUser) }, forgotPassword: async email => { const tenantId = get(store).tenantId - const response = await api.post(`/api/global/auth/${tenantId}/reset`, { + await API.requestForgotPassword({ + tenantId, email, }) - if (response.status !== 200) { - throw "Unable to send email with reset link" - } - await response.json() }, - resetPassword: async (password, code) => { + resetPassword: async (password, resetCode) => { const tenantId = get(store).tenantId - const response = await api.post( - `/api/global/auth/${tenantId}/reset/update`, - { - password, - resetCode: code, - } - ) - if (response.status !== 200) { - throw "Unable to reset password" - } - await response.json() - }, - createUser: async user => { - const response = await api.post(`/api/global/users`, user) - if (response.status !== 200) { - throw "Unable to create user" - } - await response.json() + await API.resetPassword({ + tenantId, + password, + resetCode, + }) }, } diff --git a/packages/builder/src/stores/portal/email.js b/packages/builder/src/stores/portal/email.js index a015480141..2e222d34c4 100644 --- a/packages/builder/src/stores/portal/email.js +++ b/packages/builder/src/stores/portal/email.js @@ -1,5 +1,5 @@ import { writable } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" export function createEmailStore() { const store = writable({}) @@ -8,14 +8,9 @@ export function createEmailStore() { subscribe: store.subscribe, templates: { fetch: async () => { - // fetch the email template definitions - const response = await api.get(`/api/global/template/definitions`) - const definitions = await response.json() - - // fetch the email templates themselves - const templatesResponse = await api.get(`/api/global/template/email`) - const templates = await templatesResponse.json() - + // Fetch the email template definitions and templates + const definitions = await API.getEmailTemplateDefinitions() + const templates = await API.getEmailTemplates() store.set({ definitions, templates, @@ -23,15 +18,12 @@ export function createEmailStore() { }, save: async template => { // Save your template config - const response = await api.post(`/api/global/template`, template) - const json = await response.json() - if (response.status !== 200) throw new Error(json.message) - template._rev = json._rev - template._id = json._id - + const savedTemplate = await API.saveEmailTemplate(template) + template._rev = savedTemplate._rev + template._id = savedTemplate._id store.update(state => { const currentIdx = state.templates.findIndex( - template => template.purpose === json.purpose + template => template.purpose === savedTemplate.purpose ) state.templates.splice(currentIdx, 1, template) return state diff --git a/packages/builder/src/stores/portal/oidc.js b/packages/builder/src/stores/portal/oidc.js index 3e3a7048ca..3a4b954753 100644 --- a/packages/builder/src/stores/portal/oidc.js +++ b/packages/builder/src/stores/portal/oidc.js @@ -1,5 +1,5 @@ import { writable, get } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" import { auth } from "stores/portal" const OIDC_CONFIG = { @@ -11,26 +11,20 @@ const OIDC_CONFIG = { export function createOidcStore() { const store = writable(OIDC_CONFIG) const { set, subscribe } = store - - async function init() { - const tenantId = get(auth).tenantId - const res = await api.get( - `/api/global/configs/public/oidc?tenantId=${tenantId}` - ) - const json = await res.json() - - if (json.status === 400 || Object.keys(json).length === 0) { - set(OIDC_CONFIG) - } else { - // Just use the first config for now. We will be support multiple logins buttons later on. - set(...json) - } - } - return { subscribe, set, - init, + init: async () => { + const tenantId = get(auth).tenantId + const config = await API.getOIDCConfig(tenantId) + if (Object.keys(config || {}).length) { + // Just use the first config for now. + // We will be support multiple logins buttons later on. + set(...config) + } else { + set(OIDC_CONFIG) + } + }, } } diff --git a/packages/builder/src/stores/portal/organisation.js b/packages/builder/src/stores/portal/organisation.js index 21a110c54a..9709578fa2 100644 --- a/packages/builder/src/stores/portal/organisation.js +++ b/packages/builder/src/stores/portal/organisation.js @@ -1,5 +1,5 @@ import { writable, get } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" import { auth } from "stores/portal" const DEFAULT_CONFIG = { @@ -19,35 +19,23 @@ export function createOrganisationStore() { async function init() { const tenantId = get(auth).tenantId - const res = await api.get(`/api/global/configs/public?tenantId=${tenantId}`) - const json = await res.json() - - if (json.status === 400) { - set(DEFAULT_CONFIG) - } else { - set({ ...DEFAULT_CONFIG, ...json.config, _rev: json._rev }) - } + const tenant = await API.getTenantConfig(tenantId) + set({ ...DEFAULT_CONFIG, ...tenant.config, _rev: tenant._rev }) } async function save(config) { - // delete non-persisted fields + // Delete non-persisted fields const storeConfig = get(store) delete storeConfig.oidc delete storeConfig.google delete storeConfig.oidcCallbackUrl delete storeConfig.googleCallbackUrl - - const res = await api.post("/api/global/configs", { + await API.saveConfig({ type: "settings", config: { ...get(store), ...config }, _rev: get(store)._rev, }) - const json = await res.json() - if (json.status) { - return json - } await init() - return { status: 200 } } return { diff --git a/packages/builder/src/stores/portal/templates.js b/packages/builder/src/stores/portal/templates.js index b82ecd70e2..904e9cfa8e 100644 --- a/packages/builder/src/stores/portal/templates.js +++ b/packages/builder/src/stores/portal/templates.js @@ -1,18 +1,15 @@ import { writable } from "svelte/store" -import api from "builderStore/api" +import { API } from "api" export function templatesStore() { const { subscribe, set } = writable([]) - async function load() { - const response = await api.get("/api/templates?type=app") - const json = await response.json() - set(json) - } - return { subscribe, - load, + load: async () => { + const templates = await API.getAppTemplates() + set(templates) + }, } } diff --git a/packages/builder/src/stores/portal/users.js b/packages/builder/src/stores/portal/users.js index 9a3df120e0..cebf03d4c0 100644 --- a/packages/builder/src/stores/portal/users.js +++ b/packages/builder/src/stores/portal/users.js @@ -1,38 +1,28 @@ import { writable } from "svelte/store" -import api, { post } from "builderStore/api" +import { API } from "api" import { update } from "lodash" export function createUsersStore() { const { subscribe, set } = writable([]) async function init() { - const response = await api.get(`/api/global/users`) - const json = await response.json() - set(json) + const users = await API.getUsers() + set(users) } async function invite({ email, builder, admin }) { - const body = { email, userInfo: {} } - if (admin) { - body.userInfo.admin = { - global: true, - } - } - if (builder) { - body.userInfo.builder = { - global: true, - } - } - const response = await api.post(`/api/global/users/invite`, body) - return await response.json() + await API.inviteUser({ + email, + builder, + admin, + }) } async function acceptInvite(inviteCode, password) { - const response = await api.post("/api/global/users/invite/accept", { + await API.acceptInvite({ inviteCode, password, }) - return await response.json() } async function create({ @@ -56,29 +46,17 @@ export function createUsersStore() { if (admin) { body.admin = { global: true } } - const response = await api.post("/api/global/users", body) + await API.saveUser(body) await init() - return await response.json() } async function del(id) { - const response = await api.delete(`/api/global/users/${id}`) + await API.deleteUser(id) update(users => users.filter(user => user._id !== id)) - const json = await response.json() - return { - ...json, - status: response.status, - } } async function save(data) { - try { - const res = await post(`/api/global/users`, data) - return await res.json() - } catch (error) { - console.log(error) - return error - } + await API.saveUser(data) } return { diff --git a/packages/builder/vite.config.js b/packages/builder/vite.config.js index d66d677555..b68d265bc5 100644 --- a/packages/builder/vite.config.js +++ b/packages/builder/vite.config.js @@ -56,6 +56,10 @@ export default ({ mode }) => { find: "stores", replacement: path.resolve("./src/stores"), }, + { + find: "api", + replacement: path.resolve("./src/api.js"), + }, { find: "constants", replacement: path.resolve("./src/constants"), diff --git a/packages/client/package.json b/packages/client/package.json index 5e3bc9b373..c1cd6aec73 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@budibase/bbui": "^1.0.50-alpha.5", - "@budibase/standard-components": "^0.9.139", + "@budibase/frontend-core": "^1.0.50-alpha.5", "@budibase/string-templates": "^1.0.50-alpha.5", "regexparam": "^1.3.0", "rollup-plugin-polyfill-node": "^0.8.0", diff --git a/packages/client/rollup.config.js b/packages/client/rollup.config.js index bde9d2325f..1aee91df42 100644 --- a/packages/client/rollup.config.js +++ b/packages/client/rollup.config.js @@ -57,10 +57,6 @@ export default { find: "sdk", replacement: path.resolve("./src/sdk"), }, - { - find: "builder", - replacement: path.resolve("../builder"), - }, ], }), svelte({ diff --git a/packages/client/src/api/analytics.js b/packages/client/src/api/analytics.js deleted file mode 100644 index 5a089eaa21..0000000000 --- a/packages/client/src/api/analytics.js +++ /dev/null @@ -1,10 +0,0 @@ -import API from "./api" - -/** - * Notifies that an end user client app has been loaded. - */ -export const pingEndUser = async () => { - return await API.post({ - url: `/api/analytics/ping`, - }) -} diff --git a/packages/client/src/api/api.js b/packages/client/src/api/api.js index 1bb12cca53..591d4a6782 100644 --- a/packages/client/src/api/api.js +++ b/packages/client/src/api/api.js @@ -1,110 +1,50 @@ -import { notificationStore, authStore } from "stores" +import { createAPIClient } from "@budibase/frontend-core" +import { notificationStore, authStore } from "../stores" import { get } from "svelte/store" -import { ApiVersion } from "constants" -/** - * API cache for cached request responses. - */ -let cache = {} +export const API = createAPIClient({ + // Enable caching of cacheable endpoints to speed things up, + enableCaching: true, -/** - * Handler for API errors. - */ -const handleError = error => { - return { error } -} + // Attach client specific headers + attachHeaders: headers => { + // Attach app ID header + headers["x-budibase-app-id"] = window["##BUDIBASE_APP_ID##"] -/** - * Performs an API call to the server. - * App ID header is always correctly set. - */ -const makeApiCall = async ({ method, url, body, json = true }) => { - try { - const requestBody = json ? JSON.stringify(body) : body - const inBuilder = window["##BUDIBASE_IN_BUILDER##"] - const headers = { - Accept: "application/json", - "x-budibase-app-id": window["##BUDIBASE_APP_ID##"], - "x-budibase-api-version": ApiVersion, - ...(json && { "Content-Type": "application/json" }), - ...(!inBuilder && { "x-budibase-type": "client" }), + // Attach client header if not inside the builder preview + if (!window["##BUDIBASE_IN_BUILDER##"]) { + headers["x-budibase-type"] = "client" } - // add csrf token if authenticated + // Add csrf token if authenticated const auth = get(authStore) - if (auth && auth.csrfToken) { + if (auth?.csrfToken) { headers["x-csrf-token"] = auth.csrfToken } + }, - const response = await fetch(url, { - method, - headers, - body: requestBody, - credentials: "same-origin", - }) - switch (response.status) { - case 200: - try { - return await response.json() - } catch (error) { - return null - } - case 401: - notificationStore.actions.error("Invalid credentials") - return handleError(`Invalid credentials`) - case 404: - notificationStore.actions.warning("Not found") - return handleError(`${url}: Not Found`) - case 400: - return handleError(`${url}: Bad Request`) - case 403: - notificationStore.actions.error( - "Your session has expired, or you don't have permission to access that data" - ) - return handleError(`${url}: Forbidden`) - default: - if (response.status >= 200 && response.status < 400) { - return response.json() - } - return handleError(`${url} - ${response.statusText}`) + // Show an error notification for all API failures. + // We could also log these to sentry. + // Or we could check error.status and redirect to login on a 403 etc. + onError: error => { + const { status, method, url, message, handled } = error || {} + + // Log any errors that we haven't manually handled + if (!handled) { + console.error("Unhandled error from API client", error) + return } - } catch (error) { - return handleError(error) - } -} -/** - * Performs an API call to the server and caches the response. - * Future invocation for this URL will return the cached result instead of - * hitting the server again. - */ -const makeCachedApiCall = async params => { - const identifier = params.url - if (!identifier) { - return null - } - if (!cache[identifier]) { - cache[identifier] = makeApiCall(params) - cache[identifier] = await cache[identifier] - } - return await cache[identifier] -} + // Notify all errors + if (message) { + // Don't notify if the URL contains the word analytics as it may be + // blocked by browser extensions + if (!url?.includes("analytics")) { + notificationStore.actions.error(message) + } + } -/** - * Constructs an API call function for a particular HTTP method. - */ -const requestApiCall = method => async params => { - const { external = false, url, cache = false } = params - const fixedUrl = external ? url : `/${url}`.replace("//", "/") - const enrichedParams = { ...params, method, url: fixedUrl } - return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams) -} - -export default { - post: requestApiCall("POST"), - put: requestApiCall("PUT"), - get: requestApiCall("GET"), - patch: requestApiCall("PATCH"), - del: requestApiCall("DELETE"), - error: handleError, -} + // Log all errors to console + console.warn(`[Client] HTTP ${status} on ${method}:${url}\n\t${message}`) + }, +}) diff --git a/packages/client/src/api/app.js b/packages/client/src/api/app.js deleted file mode 100644 index c5ee305cda..0000000000 --- a/packages/client/src/api/app.js +++ /dev/null @@ -1,10 +0,0 @@ -import API from "./api" - -/** - * Fetches screen definition for an app. - */ -export const fetchAppPackage = async appId => { - return await API.get({ - url: `/api/applications/${appId}/appPackage`, - }) -} diff --git a/packages/client/src/api/attachments.js b/packages/client/src/api/attachments.js deleted file mode 100644 index ed9c6fe522..0000000000 --- a/packages/client/src/api/attachments.js +++ /dev/null @@ -1,50 +0,0 @@ -import API from "./api" - -/** - * Uploads an attachment to the server. - */ -export const uploadAttachment = async (data, tableId = "") => { - return await API.post({ - url: `/api/attachments/${tableId}/upload`, - body: data, - json: false, - }) -} - -/** - * Generates a signed URL to upload a file to an external datasource. - */ -export const getSignedDatasourceURL = async (datasourceId, bucket, key) => { - if (!datasourceId) { - return null - } - const res = await API.post({ - url: `/api/attachments/${datasourceId}/url`, - body: { bucket, key }, - }) - if (res.error) { - throw "Could not generate signed upload URL" - } - return res -} - -/** - * Uploads a file to an external datasource. - */ -export const externalUpload = async (datasourceId, bucket, key, data) => { - const { signedUrl, publicUrl } = await getSignedDatasourceURL( - datasourceId, - bucket, - key - ) - const res = await API.put({ - url: signedUrl, - body: data, - json: false, - external: true, - }) - if (res?.error) { - throw "Could not upload file to signed URL" - } - return { publicUrl } -} diff --git a/packages/client/src/api/auth.js b/packages/client/src/api/auth.js deleted file mode 100644 index 9ac09f5571..0000000000 --- a/packages/client/src/api/auth.js +++ /dev/null @@ -1,45 +0,0 @@ -import API from "./api" -import { enrichRows } from "./rows" -import { TableNames } from "../constants" - -/** - * Performs a log in request. - */ -export const logIn = async ({ email, password }) => { - if (!email) { - return API.error("Please enter your email") - } - if (!password) { - return API.error("Please enter your password") - } - return await API.post({ - url: "/api/global/auth", - body: { username: email, password }, - }) -} - -/** - * Logs the user out and invaidates their session. - */ -export const logOut = async () => { - return await API.post({ - url: "/api/global/auth/logout", - }) -} - -/** - * Fetches the currently logged in user object - */ -export const fetchSelf = async () => { - const user = await API.get({ url: "/api/self" }) - if (user && user._id) { - if (user.roleId === "PUBLIC") { - // Don't try to enrich a public user as it will 403 - return user - } else { - return (await enrichRows([user], TableNames.USERS))[0] - } - } else { - return null - } -} diff --git a/packages/client/src/api/automations.js b/packages/client/src/api/automations.js deleted file mode 100644 index cb3e4623ad..0000000000 --- a/packages/client/src/api/automations.js +++ /dev/null @@ -1,16 +0,0 @@ -import { notificationStore } from "stores/notification" -import API from "./api" - -/** - * Executes an automation. Must have "App Action" trigger. - */ -export const triggerAutomation = async (automationId, fields) => { - const res = await API.post({ - url: `/api/automations/${automationId}/trigger`, - body: { fields }, - }) - res.error - ? notificationStore.actions.error("An error has occurred") - : notificationStore.actions.success("Automation triggered") - return res -} diff --git a/packages/client/src/api/index.js b/packages/client/src/api/index.js index d429eb437c..5eb6b2b6f4 100644 --- a/packages/client/src/api/index.js +++ b/packages/client/src/api/index.js @@ -1,11 +1,9 @@ -export * from "./rows" -export * from "./auth" -export * from "./tables" -export * from "./attachments" -export * from "./views" -export * from "./relationships" -export * from "./routes" -export * from "./queries" -export * from "./app" -export * from "./automations" -export * from "./analytics" +import { API } from "./api.js" +import { patchAPI } from "./patches.js" + +// Certain endpoints which return rows need patched so that they transform +// and enrich the row docs, so that they can be correctly handled by the +// client library +patchAPI(API) + +export { API } diff --git a/packages/client/src/api/patches.js b/packages/client/src/api/patches.js new file mode 100644 index 0000000000..faad9c81ec --- /dev/null +++ b/packages/client/src/api/patches.js @@ -0,0 +1,107 @@ +import { Constants } from "@budibase/frontend-core" +import { FieldTypes } from "../constants" + +export const patchAPI = API => { + /** + * Enriches rows which contain certain field types so that they can + * be properly displayed. + * The ability to create these bindings has been removed, but they will still + * exist in client apps to support backwards compatibility. + */ + const enrichRows = async (rows, tableId) => { + if (!Array.isArray(rows)) { + return [] + } + if (rows.length) { + const tables = {} + for (let row of rows) { + // Fall back to passed in tableId if row doesn't have it specified + let rowTableId = row.tableId || tableId + let table = tables[rowTableId] + if (!table) { + // Fetch table schema so we can check column types + table = await API.fetchTableDefinition(rowTableId) + tables[rowTableId] = table + } + const schema = table?.schema + if (schema) { + const keys = Object.keys(schema) + for (let key of keys) { + const type = schema[key].type + if (type === FieldTypes.LINK && Array.isArray(row[key])) { + // Enrich row a string join of relationship fields + row[`${key}_text`] = + row[key] + ?.map(option => option?.primaryDisplay) + .filter(option => !!option) + .join(", ") || "" + } else if (type === "attachment") { + // Enrich row with the first image URL for any attachment fields + let url = null + if (Array.isArray(row[key]) && row[key][0] != null) { + url = row[key][0].url + } + row[`${key}_first`] = url + } + } + } + } + } + return rows + } + + // Enrich rows so they properly handle client bindings + const fetchSelf = API.fetchSelf + API.fetchSelf = async () => { + const user = await fetchSelf() + if (user && user._id) { + if (user.roleId === "PUBLIC") { + // Don't try to enrich a public user as it will 403 + return user + } else { + return (await enrichRows([user], Constants.TableNames.USERS))[0] + } + } else { + return null + } + } + const fetchRelationshipData = API.fetchRelationshipData + API.fetchRelationshipData = async params => { + const tableId = params?.tableId + const rows = await fetchRelationshipData(params) + return await enrichRows(rows, tableId) + } + const fetchTableData = API.fetchTableData + API.fetchTableData = async tableId => { + const rows = await fetchTableData(tableId) + return await enrichRows(rows, tableId) + } + const searchTable = API.searchTable + API.searchTable = async params => { + const tableId = params?.tableId + const output = await searchTable(params) + return { + ...output, + rows: await enrichRows(output?.rows, tableId), + } + } + const fetchViewData = API.fetchViewData + API.fetchViewData = async params => { + const tableId = params?.tableId + const rows = await fetchViewData(params) + return await enrichRows(rows, tableId) + } + + // Wipe any HBS formulae from table definitions, as these interfere with + // handlebars enrichment + const fetchTableDefinition = API.fetchTableDefinition + API.fetchTableDefinition = async tableId => { + const definition = await fetchTableDefinition(tableId) + Object.keys(definition?.schema || {}).forEach(field => { + if (definition.schema[field]?.type === "formula") { + delete definition.schema[field].formula + } + }) + return definition + } +} diff --git a/packages/client/src/api/queries.js b/packages/client/src/api/queries.js deleted file mode 100644 index e8972f657e..0000000000 --- a/packages/client/src/api/queries.js +++ /dev/null @@ -1,34 +0,0 @@ -import { notificationStore, dataSourceStore } from "stores" -import API from "./api" - -/** - * Executes a query against an external data connector. - */ -export const executeQuery = async ({ queryId, pagination, parameters }) => { - const query = await fetchQueryDefinition(queryId) - if (query?.datasourceId == null) { - notificationStore.actions.error("That query couldn't be found") - return - } - const res = await API.post({ - url: `/api/v2/queries/${queryId}`, - body: { - parameters, - pagination, - }, - }) - if (res.error) { - notificationStore.actions.error("An error has occurred") - } else if (!query.readable) { - notificationStore.actions.success("Query executed successfully") - await dataSourceStore.actions.invalidateDataSource(query.datasourceId) - } - return res -} - -/** - * Fetches the definition of an external query. - */ -export const fetchQueryDefinition = async queryId => { - return await API.get({ url: `/api/queries/${queryId}`, cache: true }) -} diff --git a/packages/client/src/api/relationships.js b/packages/client/src/api/relationships.js deleted file mode 100644 index fe92bfd038..0000000000 --- a/packages/client/src/api/relationships.js +++ /dev/null @@ -1,14 +0,0 @@ -import API from "./api" -import { enrichRows } from "./rows" - -/** - * Fetches related rows for a certain field of a certain row. - */ -export const fetchRelationshipData = async ({ tableId, rowId, fieldName }) => { - if (!tableId || !rowId || !fieldName) { - return [] - } - const response = await API.get({ url: `/api/${tableId}/${rowId}/enrich` }) - const rows = response[fieldName] || [] - return await enrichRows(rows, tableId) -} diff --git a/packages/client/src/api/routes.js b/packages/client/src/api/routes.js deleted file mode 100644 index d762461075..0000000000 --- a/packages/client/src/api/routes.js +++ /dev/null @@ -1,10 +0,0 @@ -import API from "./api" - -/** - * Fetches available routes for the client app. - */ -export const fetchRoutes = async () => { - return await API.get({ - url: `/api/routing/client`, - }) -} diff --git a/packages/client/src/api/rows.js b/packages/client/src/api/rows.js deleted file mode 100644 index 2d6df90e83..0000000000 --- a/packages/client/src/api/rows.js +++ /dev/null @@ -1,155 +0,0 @@ -import { notificationStore, dataSourceStore } from "stores" -import API from "./api" -import { fetchTableDefinition } from "./tables" -import { FieldTypes } from "../constants" - -/** - * Fetches data about a certain row in a table. - */ -export const fetchRow = async ({ tableId, rowId }) => { - if (!tableId || !rowId) { - return - } - const row = await API.get({ - url: `/api/${tableId}/rows/${rowId}`, - }) - return (await enrichRows([row], tableId))[0] -} - -/** - * Creates a row in a table. - */ -export const saveRow = async row => { - if (!row?.tableId) { - return - } - const res = await API.post({ - url: `/api/${row.tableId}/rows`, - body: row, - }) - res.error - ? notificationStore.actions.error("An error has occurred") - : notificationStore.actions.success("Row saved") - - // Refresh related datasources - await dataSourceStore.actions.invalidateDataSource(row.tableId) - - return res -} - -/** - * Updates a row in a table. - */ -export const updateRow = async row => { - if (!row?.tableId || !row?._id) { - return - } - const res = await API.patch({ - url: `/api/${row.tableId}/rows`, - body: row, - }) - res.error - ? notificationStore.actions.error("An error has occurred") - : notificationStore.actions.success("Row updated") - - // Refresh related datasources - await dataSourceStore.actions.invalidateDataSource(row.tableId) - - return res -} - -/** - * Deletes a row from a table. - */ -export const deleteRow = async ({ tableId, rowId, revId }) => { - if (!tableId || !rowId || !revId) { - return - } - const res = await API.del({ - url: `/api/${tableId}/rows`, - body: { - _id: rowId, - _rev: revId, - }, - }) - res.error - ? notificationStore.actions.error("An error has occurred") - : notificationStore.actions.success("Row deleted") - - // Refresh related datasources - await dataSourceStore.actions.invalidateDataSource(tableId) - - return res -} - -/** - * Deletes many rows from a table. - */ -export const deleteRows = async ({ tableId, rows }) => { - if (!tableId || !rows) { - return - } - const res = await API.del({ - url: `/api/${tableId}/rows`, - body: { - rows, - }, - }) - res.error - ? notificationStore.actions.error("An error has occurred") - : notificationStore.actions.success(`${rows.length} row(s) deleted`) - - // Refresh related datasources - await dataSourceStore.actions.invalidateDataSource(tableId) - - return res -} - -/** - * Enriches rows which contain certain field types so that they can - * be properly displayed. - * The ability to create these bindings has been removed, but they will still - * exist in client apps to support backwards compatibility. - */ -export const enrichRows = async (rows, tableId) => { - if (!Array.isArray(rows)) { - return [] - } - if (rows.length) { - // map of tables, incase a row being loaded is not from the same table - const tables = {} - for (let row of rows) { - // fallback to passed in tableId if row doesn't have it specified - let rowTableId = row.tableId || tableId - let table = tables[rowTableId] - if (!table) { - // Fetch table schema so we can check column types - table = await fetchTableDefinition(rowTableId) - tables[rowTableId] = table - } - const schema = table?.schema - if (schema) { - const keys = Object.keys(schema) - for (let key of keys) { - const type = schema[key].type - if (type === FieldTypes.LINK && Array.isArray(row[key])) { - // Enrich row a string join of relationship fields - row[`${key}_text`] = - row[key] - ?.map(option => option?.primaryDisplay) - .filter(option => !!option) - .join(", ") || "" - } else if (type === "attachment") { - // Enrich row with the first image URL for any attachment fields - let url = null - if (Array.isArray(row[key]) && row[key][0] != null) { - url = row[key][0].url - } - row[`${key}_first`] = url - } - } - } - } - } - return rows -} diff --git a/packages/client/src/api/tables.js b/packages/client/src/api/tables.js deleted file mode 100644 index 09f77de6ee..0000000000 --- a/packages/client/src/api/tables.js +++ /dev/null @@ -1,63 +0,0 @@ -import API from "./api" -import { enrichRows } from "./rows" - -/** - * Fetches a table definition. - * Since definitions cannot change at runtime, the result is cached. - */ -export const fetchTableDefinition = async tableId => { - const res = await API.get({ url: `/api/tables/${tableId}`, cache: true }) - - // Wipe any HBS formulae, as these interfere with handlebars enrichment - Object.keys(res?.schema || {}).forEach(field => { - if (res.schema[field]?.type === "formula") { - delete res.schema[field].formula - } - }) - - return res -} - -/** - * Fetches all rows from a table. - */ -export const fetchTableData = async tableId => { - const rows = await API.get({ url: `/api/${tableId}/rows` }) - return await enrichRows(rows, tableId) -} - -/** - * Searches a table using Lucene. - */ -export const searchTable = async ({ - tableId, - query, - bookmark, - limit, - sort, - sortOrder, - sortType, - paginate, -}) => { - if (!tableId || !query) { - return { - rows: [], - } - } - const res = await API.post({ - url: `/api/${tableId}/search`, - body: { - query, - bookmark, - limit, - sort, - sortOrder, - sortType, - paginate, - }, - }) - return { - ...res, - rows: await enrichRows(res?.rows, tableId), - } -} diff --git a/packages/client/src/api/views.js b/packages/client/src/api/views.js deleted file mode 100644 index d173e53d53..0000000000 --- a/packages/client/src/api/views.js +++ /dev/null @@ -1,30 +0,0 @@ -import API from "./api" -import { enrichRows } from "./rows" - -/** - * Fetches all rows in a view. - */ -export const fetchViewData = async ({ - name, - field, - groupBy, - calculation, - tableId, -}) => { - const params = new URLSearchParams() - - if (calculation) { - params.set("field", field) - params.set("calculation", calculation) - } - if (groupBy) { - params.set("group", groupBy ? "true" : "false") - } - - const QUERY_VIEW_URL = field - ? `/api/views/${name}?${params}` - : `/api/views/${name}` - - const rows = await API.get({ url: QUERY_VIEW_URL }) - return await enrichRows(rows, tableId) -} diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index 7f5bed210e..5bd5d2d46f 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -2,6 +2,8 @@ import { writable, get } from "svelte/store" import { setContext, onMount } from "svelte" import { Layout, Heading, Body } from "@budibase/bbui" + import ErrorSVG from "@budibase/frontend-core/assets/error.svg" + import { Constants, CookieUtils } from "@budibase/frontend-core" import Component from "./Component.svelte" import SDK from "sdk" import { @@ -24,7 +26,6 @@ import HoverIndicator from "components/preview/HoverIndicator.svelte" import CustomThemeWrapper from "./CustomThemeWrapper.svelte" import DNDHandler from "components/preview/DNDHandler.svelte" - import ErrorSVG from "builder/assets/error.svg" import KeyboardManager from "components/preview/KeyboardManager.svelte" // Provide contexts @@ -63,9 +64,8 @@ } else { // The user is not logged in, redirect them to login const returnUrl = `${window.location.pathname}${window.location.hash}` - // TODO: reuse `Cookies` from builder when frontend-core is added - window.document.cookie = `budibase:returnurl=${returnUrl}; Path=/` - window.location = `/builder/auth/login` + CookieUtils.setCookie(Constants.Cookies.ReturnUrl, returnUrl) + window.location = "/builder/auth/login" } } } diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 8cd1849336..f43c2b30ec 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -9,7 +9,7 @@ import Router from "./Router.svelte" import { enrichProps, propsAreSame } from "utils/componentProps" import { builderStore } from "stores" - import { hashString } from "utils/helpers" + import { Helpers } from "@budibase/bbui" import Manifest from "manifest.json" import { getActiveConditions, reduceConditionActions } from "utils/conditions" import Placeholder from "components/app/Placeholder.svelte" @@ -106,7 +106,7 @@ // Raw settings are all settings excluding internal props and children $: rawSettings = getRawSettings(instance) - $: instanceKey = hashString(JSON.stringify(rawSettings)) + $: instanceKey = Helpers.hashString(JSON.stringify(rawSettings)) // Update and enrich component settings $: updateSettings(rawSettings, instanceKey, settingsDefinition, $context) @@ -118,9 +118,6 @@ // Build up the final settings object to be passed to the component $: cacheSettings(enrichedSettings, nestedSettings, conditionalSettings) - // Render key is used to determine when components need to fully remount - $: renderKey = getRenderKey(id, editing) - // Update component context $: componentStore.set({ id, @@ -276,8 +273,7 @@ // reactive statements as much as possible. const cacheSettings = (enriched, nested, conditional) => { const allSettings = { ...enriched, ...nested, ...conditional } - const mounted = ref?.$$set != null - if (!cachedSettings || !mounted) { + if (!cachedSettings) { cachedSettings = { ...allSettings } initialSettings = cachedSettings } else { @@ -290,51 +286,54 @@ // setting it on initialSettings directly, we avoid a double render. cachedSettings[key] = allSettings[key] - // Programmatically set the prop to avoid svelte reactive statements - // firing inside components. This circumvents the problems caused by - // spreading a props object. - ref.$$set({ [key]: allSettings[key] }) + if (ref?.$$set) { + // Programmatically set the prop to avoid svelte reactive statements + // firing inside components. This circumvents the problems caused by + // spreading a props object. + ref.$$set({ [key]: allSettings[key] }) + } else { + // Sometimes enrichment can occur multiple times before the + // component has mounted and been assigned a ref. + // In these cases, for some reason we need to update the + // initial settings object, even though it is equivalent by + // reference to cached settings. This solves the problem of multiple + // initial enrichments, while also not causing wasted renders for + // any components not affected by this issue. + initialSettings[key] = allSettings[key] + } } }) } } - - // Generates a key used to determine when components need to fully remount. - // Currently only toggling editing requires remounting. - const getRenderKey = (id, editing) => { - return hashString(`${id}-${editing}`) - } -{#key renderKey} - {#if constructor && initialSettings && (visible || inSelectedPath)} - - -
- - {#if children.length} - {#each children as child (child._id)} - - {/each} - {:else if emptyState} - - {:else if isBlock} - - {/if} - -
- {/if} -{/key} +{#if constructor && initialSettings && (visible || inSelectedPath)} + + +
+ + {#if children.length} + {#each children as child (child._id)} + + {/each} + {:else if emptyState} + + {:else if isBlock} + + {/if} + +
+{/if}