This commit is contained in:
Martin McKeaveney 2020-12-12 20:09:04 +00:00
commit 89615e58a7
292 changed files with 11526 additions and 7604 deletions

View File

View File

@ -186,7 +186,7 @@ Or if you are in the builder you can run `yarn cy:test`.
### Other Useful Information
* The contributors are listed in [AUTHORS.md](https://github.com/budibase/server/blob/master/AUTHORS.md) (add yourself).
* The contributors are listed in [AUTHORS.md](https://github.com/Budibase/budibase/blob/master/.github/AUTHORS.md) (add yourself).
* This project uses a modified version of the MPLv2 license, see [LICENSE](https://github.com/budibase/server/blob/master/LICENSE).

View File

@ -116,7 +116,7 @@ You can also follow a quick tutorial on [how to build a CRM with Budibase](https
## ❗ Code of Conduct
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/master/CODE_OF_CONDUCT.md). Please read it.
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/master/.github/CODE_OF_CONDUCT.md). Please read it.
## 🙌 Contributing to Budibase
@ -134,7 +134,7 @@ Budibase is a monorepo managed by lerna. Lerna manages the building and publishi
- [packages/server](https://github.com/Budibase/budibase/tree/master/packages/server) - The budibase server. This Koa app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system.
For more information, see [CONTRIBUTING.md](./CONTRIBUTING.md)
For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/master/.github/CONTRIBUTING.md)
## 📝 License

View File

@ -14,7 +14,7 @@
"prettier-plugin-svelte": "^1.4.0",
"rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0",
"svelte": "^3.28.0"
"svelte": "^3.30.0"
},
"scripts": {
"bootstrap": "lerna bootstrap",
@ -26,7 +26,7 @@
"nuke": "rimraf ~/.budibase && npm run restore",
"clean": "lerna clean",
"kill-port": "kill-port 4001",
"dev": "npm run kill-port && node ./scripts/symlinkDev.js && lerna run --parallel dev:builder --concurrency 1",
"dev": "yarn run kill-port && node ./scripts/symlinkDev.js && lerna run --parallel dev:builder --concurrency 1",
"test": "lerna run test",
"lint": "eslint packages",
"lint:fix": "eslint --fix packages",

View File

@ -60,7 +60,7 @@ context("Create a Table", () => {
})
it("deletes a table", () => {
cy.contains(".nav-item", "dog").get(".actions").invoke("show").click()
cy.get(".actions").first().invoke("show").click()
cy.get("[data-cy=delete-table]").click()
cy.contains("Delete Table").click()
cy.contains("dog").should("not.exist")

View File

@ -9,9 +9,9 @@ context('Create a User', () => {
// https://on.cypress.io/interacting-with-elements
it('should create a user', () => {
cy.createUser('bbuser', 'test', 'POWER_USER')
cy.createUser("bbuser@test.com", "test", "ADMIN")
// Check to make sure user was created!
cy.get("input[disabled]").should('have.value', 'bbuser')
// // Check to make sure user was created!
cy.contains("bbuser").should('be.visible')
})
})

View File

@ -16,6 +16,9 @@ process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
process.env.NODE_ENV = "cypress"
process.env.ENABLE_ANALYTICS = "false"
// Stop info logs polluting test outputs
process.env.LOG_LEVEL = "error"
async function run(dir) {
process.env.BUDIBASE_DIR = resolve(dir)
require("dotenv").config({ path: resolve(dir, ".env") })

View File

@ -44,9 +44,9 @@ Cypress.Commands.add("createApp", name => {
cy.contains("Next").click()
cy.get("input[name=username]")
cy.get("input[name=email]")
.click()
.type("test")
.type("test@test.com")
cy.get("input[name=password]")
.click()
.type("test")
@ -111,25 +111,28 @@ Cypress.Commands.add("addRow", values => {
})
})
Cypress.Commands.add("createUser", (username, password, accessLevel) => {
Cypress.Commands.add("createUser", (email, password, role) => {
// Create User
cy.get(".toprightnav > .settings").click()
cy.contains("Users").click()
cy.get("[name=Name]")
.first()
.type(username)
cy.get("[name=Password]")
.first()
.type(password)
cy.get("select")
.first()
.select(accessLevel)
cy.contains("Create New Row").click()
// Save
cy.get(".inputs")
.contains("Create")
.click()
cy.get(".modal").within(() => {
cy.get("input")
.first()
.type(password)
cy.get("input")
.eq(1)
.type(email)
cy.get("select")
.first()
.select(role)
// Save
cy.get(".buttons")
.contains("Create Row")
.click()
})
})
Cypress.Commands.add("addHeadlineComponent", text => {

View File

@ -63,7 +63,7 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.50.2",
"@budibase/bbui": "^1.52.2",
"@budibase/client": "^0.3.8",
"@budibase/colorpicker": "^1.0.1",
"@budibase/svelte-ag-grid": "^0.0.16",
@ -81,8 +81,8 @@
"shortid": "^2.2.15",
"svelte-loading-spinners": "^0.1.1",
"svelte-portal": "^0.1.0",
"yup": "^0.29.2",
"uuid": "^8.3.1"
"uuid": "^8.3.1",
"yup": "^0.29.2"
},
"devDependencies": {
"@babel/core": "^7.5.5",
@ -90,6 +90,7 @@
"@babel/preset-env": "^7.5.5",
"@babel/runtime": "^7.5.5",
"@rollup/plugin-alias": "^3.0.1",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-json": "^4.0.3",
"@sveltech/routify": "1.7.11",
"@testing-library/jest-dom": "^5.11.0",
@ -104,9 +105,9 @@
"rimraf": "^3.0.2",
"rollup": "^2.11.2",
"rollup-plugin-alias": "^1.5.2",
"rollup-plugin-commonjs": "^10.0.0",
"rollup-plugin-copy": "^3.0.0",
"rollup-plugin-css-only": "^2.1.0",
"rollup-plugin-html": "^0.2.1",
"rollup-plugin-livereload": "^1.0.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
@ -115,7 +116,7 @@
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-url": "^2.2.2",
"start-server-and-test": "^1.11.0",
"svelte": "^3.29.0",
"svelte": "^3.30.0",
"svelte-jester": "^1.0.6"
},
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"

View File

@ -1,7 +1,7 @@
import alias from "@rollup/plugin-alias"
import svelte from "rollup-plugin-svelte"
import resolve from "rollup-plugin-node-resolve"
import commonjs from "rollup-plugin-commonjs"
import commonjs from "@rollup/plugin-commonjs"
import url from "rollup-plugin-url"
import livereload from "rollup-plugin-livereload"
import { terser } from "rollup-plugin-terser"
@ -11,106 +11,12 @@ import copy from "rollup-plugin-copy"
import css from "rollup-plugin-css-only"
import replace from "rollup-plugin-replace"
import json from "@rollup/plugin-json"
import html from "rollup-plugin-html"
import path from "path"
const production = !process.env.ROLLUP_WATCH
const lodash_fp_exports = [
"flow",
"pipe",
"union",
"reduce",
"isUndefined",
"cloneDeep",
"split",
"some",
"map",
"filter",
"isEmpty",
"countBy",
"includes",
"last",
"find",
"constant",
"take",
"first",
"intersection",
"mapValues",
"isNull",
"has",
"isInteger",
"isNumber",
"isString",
"isBoolean",
"isDate",
"isArray",
"isObject",
"clone",
"values",
"keyBy",
"isNaN",
"keys",
"orderBy",
"concat",
"reverse",
"difference",
"merge",
"flatten",
"each",
"pull",
"join",
"defaultCase",
"uniqBy",
"every",
"uniqWith",
"isFunction",
"groupBy",
"differenceBy",
"intersectionBy",
"isEqual",
"max",
"sortBy",
"assign",
"uniq",
"trimChars",
"trimCharsStart",
"isObjectLike",
"flattenDeep",
"indexOf",
"isPlainObject",
"toNumber",
"takeRight",
"toPairs",
"remove",
"findIndex",
"compose",
"get",
"tap",
]
const lodash_exports = [
"flow",
"join",
"replace",
"trim",
"dropRight",
"takeRight",
"head",
"reduce",
"tail",
"startsWith",
"findIndex",
"merge",
"assign",
"each",
"find",
"orderBy",
"union",
]
const outputpath = "../server/builder"
const coreExternal = [
"lodash",
"lodash/fp",
@ -170,10 +76,6 @@ export default {
{ src: "src/index.html", dest: outputpath },
{ src: "src/favicon.png", dest: outputpath },
{ src: "assets", dest: outputpath },
{
src: "node_modules/@budibase/client/dist/budibase-client.esm.mjs",
dest: outputpath,
},
{
src: "node_modules/@budibase/bbui/dist/bbui.css",
dest: outputpath,
@ -224,13 +126,7 @@ export default {
)
},
}),
commonjs({
namedExports: {
"lodash/fp": lodash_fp_exports,
lodash: lodash_exports,
shortid: ["generate"],
},
}),
commonjs(),
url({
limit: 0,
include: ["**/*.woff2", "**/*.png"],
@ -248,5 +144,6 @@ export default {
// instead of npm run dev), minify
production && terser(),
json(),
html(),
],
}

View File

@ -24,7 +24,7 @@ import { cloneDeep, difference } from "lodash/fp"
* @returns {Array.<BindableProperty>}
*/
export default function({ componentInstanceId, screen, components, tables }) {
const walkResult = walk({
const result = walk({
// cloning so we are free to mutate props (e.g. by adding _contexts)
instance: cloneDeep(screen.props),
targetId: componentInstanceId,
@ -33,13 +33,10 @@ export default function({ componentInstanceId, screen, components, tables }) {
})
return [
...walkResult.bindableInstances
.filter(isInstanceInSharedContext(walkResult))
.map(componentInstanceToBindable(walkResult)),
...(walkResult.target?._contexts
.map(contextToBindables(tables, walkResult))
.flat() ?? []),
...result.bindableInstances
.filter(isInstanceInSharedContext(result))
.map(componentInstanceToBindable),
...(result.target?._contexts.map(contextToBindables(tables)).flat() ?? []),
]
}
@ -53,26 +50,18 @@ const isInstanceInSharedContext = walkResult => i =>
// turns a component instance prop into binding expressions
// used by the UI
const componentInstanceToBindable = walkResult => i => {
const lastContext =
i.instance._contexts.length &&
i.instance._contexts[i.instance._contexts.length - 1]
const contextParentPath = lastContext
? getParentPath(walkResult, lastContext)
: ""
const componentInstanceToBindable = i => {
return {
type: "instance",
instance: i.instance,
// how the binding expression persists, and is used in the app at runtime
runtimeBinding: `${contextParentPath}${i.instance._id}.${i.prop}`,
runtimeBinding: `${i.instance._id}`,
// how the binding exressions looks to the user of the builder
readableBinding: `${i.instance._instanceName}`,
}
}
const contextToBindables = (tables, walkResult) => context => {
const contextParentPath = getParentPath(walkResult, context)
const contextToBindables = tables => context => {
const tableId = context.table?.tableId ?? context.table
const table = tables.find(table => table._id === tableId)
let schema =
@ -98,7 +87,7 @@ const contextToBindables = (tables, walkResult) => context => {
fieldSchema,
instance: context.instance,
// how the binding expression persists, and is used in the app at runtime
runtimeBinding: `${contextParentPath}data.${runtimeBoundKey}`,
runtimeBinding: `${context.instance._id}.${runtimeBoundKey}`,
// how the binding expressions looks to the user of the builder
readableBinding: `${context.instance._instanceName}.${table.name}.${key}`,
// table / view info
@ -118,20 +107,6 @@ const contextToBindables = (tables, walkResult) => context => {
)
}
const getParentPath = (walkResult, context) => {
// describes the number of "parent" in the path
// clone array first so original array is not mtated
const contextParentNumber = [...walkResult.target._contexts]
.reverse()
.indexOf(context)
return (
new Array(contextParentNumber).fill("parent").join(".") +
// trailing . if has parents
(contextParentNumber ? "." : "")
)
}
const walk = ({ instance, targetId, components, tables, result }) => {
if (!result) {
result = {

View File

@ -2,6 +2,8 @@ import { walkProps } from "./storeUtils"
import { get_capitalised_name } from "../helpers"
import { get } from "svelte/store"
import { allScreens } from "builderStore"
import { FrontendTypes } from "../constants"
import { currentAsset } from "."
export default function(component, state) {
const capitalised = get_capitalised_name(
@ -19,14 +21,16 @@ export default function(component, state) {
})
}
// check page first
findMatches(state.pages[state.currentPageName].props)
// check layouts first
for (let layout of state.layouts) {
findMatches(layout.props)
}
// if viewing screen, check current screen for duplicate
if (state.currentFrontEndType === "screen") {
findMatches(state.currentPreviewItem.props)
if (state.currentFrontEndType === FrontendTypes.SCREEN) {
findMatches(get(currentAsset).props)
} else {
// viewing master page - need to find against all screens
// viewing a layout - need to find against all screens
for (let screen of get(allScreens)) {
findMatches(screen.props)
}

View File

@ -4,37 +4,74 @@ import { getAutomationStore } from "./store/automation/"
import { getThemeStore } from "./store/theme"
import { derived } from "svelte/store"
import analytics from "analytics"
import { LAYOUT_NAMES } from "../constants"
import { makePropsSafe } from "components/userInterface/assetParsing/createProps"
export const store = getFrontendStore()
export const backendUiStore = getBackendUiStore()
export const automationStore = getAutomationStore()
export const themeStore = getThemeStore()
export const currentAsset = derived(store, $store => {
const layout = $store.layouts
? $store.layouts.find(layout => layout._id === $store.currentAssetId)
: null
if (layout) return layout
const screen = $store.screens
? $store.screens.find(screen => screen._id === $store.currentAssetId)
: null
if (screen) return screen
return null
})
export const selectedComponent = derived(
[store, currentAsset],
([$store, $currentAsset]) => {
if (!$currentAsset || !$store.selectedComponentId) return null
function traverse(node, callback) {
if (node._id === $store.selectedComponentId) return callback(node)
if (node._children) {
node._children.forEach(child => traverse(child, callback))
}
if (node.props) {
traverse(node.props, callback)
}
}
let component
traverse($currentAsset, found => {
const componentIdentifier = found._component ?? found.props._component
const componentDef = componentIdentifier.startsWith("##")
? found
: $store.components[componentIdentifier]
component = makePropsSafe(componentDef, found)
})
return component
}
)
export const currentAssetName = derived(store, () => {
return currentAsset.name
})
// leave this as before for consistency
export const allScreens = derived(store, $store => {
let screens = []
if ($store.pages == null) {
return screens
}
for (let page of Object.values($store.pages)) {
screens = screens.concat(page._screens)
}
return screens
return $store.screens
})
export const currentScreens = derived(store, $store => {
const currentScreens = $store.pages[$store.currentPageName]?._screens
if (currentScreens == null) {
return []
}
return Array.isArray(currentScreens)
? currentScreens
: Object.values(currentScreens)
})
export const selectedPage = derived(store, $store => {
if (!$store.pages) return null
return $store.pages[$store.currentPageName || "main"]
export const mainLayout = derived(store, $store => {
return $store.layouts?.find(
layout => layout._id === LAYOUT_NAMES.MASTER.PRIVATE
)
})
export const initialise = async () => {

View File

@ -12,10 +12,7 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
return boundValue === `{{ ${readableBinding} }}`
})
if (binding) {
result = textWithBindings.replace(
boundValue,
`{{ ${binding.runtimeBinding} }}`
)
result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`)
}
})
return result

View File

@ -4,34 +4,36 @@ import {
createProps,
getBuiltin,
makePropsSafe,
} from "components/userInterface/pagesParsing/createProps"
import { allScreens, backendUiStore, selectedPage } from "builderStore"
import { generate_screen_css } from "../generate_css"
} from "components/userInterface/assetParsing/createProps"
import {
allScreens,
backendUiStore,
currentAsset,
mainLayout,
selectedComponent,
} from "builderStore"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
import { DEFAULT_PAGES_OBJECT } from "../../constants"
import { FrontendTypes } from "../../constants"
import getNewComponentName from "../getNewComponentName"
import analytics from "analytics"
import {
findChildComponentType,
generateNewIdsForComponent,
getComponentDefinition,
getParent,
findParent,
} from "../storeUtils"
const INITIAL_FRONTEND_STATE = {
apps: [],
name: "",
description: "",
pages: DEFAULT_PAGES_OBJECT,
mainUi: {},
unauthenticatedUi: {},
layouts: [],
screens: [],
components: [],
currentPreviewItem: null,
currentComponentInfo: null,
currentFrontEndType: "none",
currentPageName: "",
currentComponentProps: null,
currentAssetId: "",
selectedComponentId: "",
errors: [],
hasAppPackage: false,
libraries: null,
@ -43,52 +45,13 @@ export const getFrontendStore = () => {
const store = writable({ ...INITIAL_FRONTEND_STATE })
store.actions = {
// TODO: REFACTOR
initialise: async pkg => {
const { layouts, screens, application } = pkg
store.update(state => {
state.appId = pkg.application._id
state.appId = application._id
return state
})
const screens = await api.get("/api/screens").then(r => r.json())
const mainScreens = screens.filter(screen =>
screen._id.includes(pkg.pages.main._id)
),
unauthScreens = screens.filter(screen =>
screen._id.includes(pkg.pages.unauthenticated._id)
)
pkg.pages = {
main: {
...pkg.pages.main,
_screens: mainScreens,
},
unauthenticated: {
...pkg.pages.unauthenticated,
_screens: unauthScreens,
},
}
// if the app has just been created
// we need to build the CSS and save
if (pkg.justCreated) {
for (let pageName of ["main", "unauthenticated"]) {
const page = pkg.pages[pageName]
store.actions.screens.regenerateCss(page)
for (let screen of page._screens) {
store.actions.screens.regenerateCss(screen)
}
await api.post(`/api/pages/${page._id}`, {
page: {
componentLibraries: pkg.application.componentLibraries,
...page,
},
screens: page._screens,
})
}
}
pkg.justCreated = false
const components = await fetchComponentLibDefinitions(pkg.application._id)
@ -99,7 +62,8 @@ export const getFrontendStore = () => {
name: pkg.application.name,
description: pkg.application.description,
appId: pkg.application._id,
pages: pkg.pages,
layouts,
screens,
hasAppPackage: true,
builtins: [getBuiltin("##builtin/screenslot")],
appInstance: pkg.application.instance,
@ -107,20 +71,6 @@ export const getFrontendStore = () => {
await backendUiStore.actions.database.select(pkg.application.instance)
},
selectPageOrScreen: type => {
store.update(state => {
state.currentFrontEndType = type
const page = get(selectedPage)
const pageOrScreen = type === "page" ? page : page._screens[0]
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null
state.currentPreviewItem = pageOrScreen
state.currentView = "detail"
return state
})
},
routing: {
fetch: async () => {
const response = await api.get("/api/routing")
@ -133,167 +83,170 @@ export const getFrontendStore = () => {
},
},
screens: {
select: screenId => {
select: async screenId => {
let promise
store.update(state => {
const screen = get(allScreens).find(screen => screen._id === screenId)
state.currentPreviewItem = screen
state.currentFrontEndType = "screen"
if (!screen) return state
state.currentFrontEndType = FrontendTypes.SCREEN
state.currentAssetId = screenId
state.currentView = "detail"
store.actions.screens.regenerateCssForCurrentScreen()
const safeProps = makePropsSafe(
state.components[screen.props._component],
screen.props
)
screen.props = safeProps
state.currentComponentInfo = safeProps
promise = store.actions.screens.regenerateCss(screen)
state.selectedComponentId = screen.props?._id
return state
})
await promise
},
create: async screen => {
let savePromise
screen = await store.actions.screens.save(screen)
store.update(state => {
state.currentPreviewItem = screen
state.currentComponentInfo = screen.props
state.currentFrontEndType = "screen"
if (state.currentPreviewItem) {
store.actions.screens.regenerateCss(state.currentPreviewItem)
}
savePromise = store.actions.screens.save(screen)
state.currentAssetId = screen._id
state.selectedComponentId = screen.props._id
state.currentFrontEndType = FrontendTypes.SCREEN
return state
})
await savePromise
return screen
},
save: async screen => {
const page = get(selectedPage)
const currentPageScreens = page._screens
const creatingNewScreen = screen._id === undefined
const response = await api.post(`/api/screens`, screen)
screen = await response.json()
let savePromise
const response = await api.post(`/api/screens/${page._id}`, screen)
const json = await response.json()
screen._rev = json.rev
screen._id = json.id
const foundScreen = page._screens.findIndex(el => el._id === screen._id)
if (foundScreen !== -1) {
page._screens.splice(foundScreen, 1)
}
page._screens.push(screen)
// TODO: should carry out all server updates to screen in a single call
store.update(state => {
page._screens = currentPageScreens
const foundScreen = state.screens.findIndex(
el => el._id === screen._id
)
if (foundScreen !== -1) {
state.screens.splice(foundScreen, 1)
}
state.screens.push(screen)
if (creatingNewScreen) {
state.currentPreviewItem = screen
const safeProps = makePropsSafe(
state.components[screen.props._component],
screen.props
)
state.currentComponentInfo = safeProps
state.selectedComponentId = safeProps._id
screen.props = safeProps
}
savePromise = store.actions.pages.save()
return state
})
if (savePromise) await savePromise
return screen
},
regenerateCss: screen => {
screen._css = generate_screen_css([screen.props])
regenerateCss: async asset => {
const response = await api.post("/api/css/generate", asset)
asset._css = (await response.json())?.css
},
regenerateCssForCurrentScreen: () => {
const { currentPreviewItem } = get(store)
if (currentPreviewItem) {
store.actions.screens.regenerateCss(currentPreviewItem)
regenerateCssForCurrentScreen: async () => {
const asset = get(currentAsset)
if (asset) {
await store.actions.screens.regenerateCss(asset)
}
},
delete: async screens => {
let deletePromise
const screensToDelete = Array.isArray(screens) ? screens : [screens]
const screenDeletePromises = []
store.update(state => {
const currentPage = get(selectedPage)
for (let screenToDelete of screensToDelete) {
// Remove screen from current page as well
// TODO: Should be done server side
currentPage._screens = currentPage._screens.filter(
scr => scr._id !== screenToDelete._id
state.screens = state.screens.filter(
screen => screen._id !== screenToDelete._id
)
deletePromise = api.delete(
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
screenDeletePromises.push(
api.delete(
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
)
)
if (screenToDelete._id === state.currentAssetId) {
state.currentAssetId = ""
}
}
return state
})
await deletePromise
await Promise.all(screenDeletePromises)
},
},
preview: {
saveSelected: async () => {
const state = get(store)
if (state.currentFrontEndType !== "page") {
await store.actions.screens.save(state.currentPreviewItem)
const selectedAsset = get(currentAsset)
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
await store.actions.screens.save(selectedAsset)
} else {
await store.actions.layouts.save(selectedAsset)
}
await store.actions.pages.save()
},
},
pages: {
select: pageName => {
layouts: {
select: async layoutId => {
store.update(state => {
const currentPage = state.pages[pageName]
const layout = store.actions.layouts.find(layoutId)
state.currentFrontEndType = "page"
state.currentFrontEndType = FrontendTypes.LAYOUT
state.currentView = "detail"
state.currentPageName = pageName
// This is the root of many problems.
// Uncaught (in promise) TypeError: Cannot read property '_component' of undefined
// it appears that the currentPage sometimes has _props instead of props
// why
const safeProps = makePropsSafe(
state.components[currentPage.props._component],
currentPage.props
)
state.currentComponentInfo = safeProps
currentPage.props = safeProps
state.currentPreviewItem = state.pages[pageName]
store.actions.screens.regenerateCssForCurrentScreen()
for (let screen of get(allScreens)) {
screen._css = generate_screen_css([screen.props])
}
state.currentAssetId = layout._id
state.selectedComponentId = layout.props?._id
return state
})
},
save: async page => {
const storeContents = get(store)
const pageName = storeContents.currentPageName || "main"
const pageToSave = page || storeContents.pages[pageName]
let cssPromises = []
cssPromises.push(store.actions.screens.regenerateCssForCurrentScreen())
// TODO: revisit. This sends down a very weird payload
const response = await api.post(`/api/pages/${pageToSave._id}`, {
page: {
componentLibraries: storeContents.pages.componentLibraries,
...pageToSave,
},
screens: pageToSave._screens,
})
for (let screen of get(allScreens)) {
cssPromises.push(store.actions.screens.regenerateCss(screen))
}
await Promise.all(cssPromises)
},
save: async layout => {
const layoutToSave = cloneDeep(layout)
delete layoutToSave._css
const response = await api.post(`/api/layouts`, layoutToSave)
const json = await response.json()
if (!json.ok) throw new Error("Error updating page")
store.update(state => {
const layoutIdx = state.layouts.findIndex(
stateLayout => stateLayout._id === json._id
)
if (layoutIdx >= 0) {
// update existing layout
state.layouts.splice(layoutIdx, 1, json)
} else {
// save new layout
state.layouts.push(json)
}
state.currentAssetId = json._id
return state
})
},
find: layoutId => {
if (!layoutId) {
return get(mainLayout)
}
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)
}
store.update(state => {
state.pages[pageName]._rev = json.rev
state.layouts = state.layouts.filter(
layout => layout._id !== layoutToDelete._id
)
return state
})
},
@ -301,17 +254,19 @@ export const getFrontendStore = () => {
components: {
select: component => {
store.update(state => {
const componentDef = component._component.startsWith("##")
? component
: state.components[component._component]
state.currentComponentInfo = makePropsSafe(componentDef, component)
state.selectedComponentId = component._id
state.currentView = "component"
return state
})
},
create: (componentToAdd, presetProps) => {
const selectedAsset = get(currentAsset)
store.update(state => {
function findSlot(component_array) {
if (!component_array) {
return false
}
for (let component of component_array) {
if (component._component === "##builtin/screenslot") {
return true
@ -324,7 +279,7 @@ export const getFrontendStore = () => {
if (
componentToAdd.startsWith("##") &&
findSlot(state.pages[state.currentPageName].props._children)
findSlot(selectedAsset?.props._children)
) {
return state
}
@ -340,29 +295,34 @@ export const getFrontendStore = () => {
_instanceName: instanceName,
})
const currentComponent =
state.components[state.currentComponentInfo._component]
const selected = get(selectedComponent)
const targetParent = currentComponent.children
? state.currentComponentInfo
: getParent(
state.currentPreviewItem.props,
state.currentComponentInfo
)
const currentComponentDefinition =
state.components[selected._component]
// Don't continue if there's no parent
if (!targetParent) {
return state
const allowsChildren = currentComponentDefinition.children
// Determine where to put the new component.
let targetParent
if (allowsChildren) {
// Child of the selected component
targetParent = selected
} else {
// Sibling of selected component
targetParent = findParent(selectedAsset.props, selected)
}
targetParent._children = targetParent._children.concat(
newComponent.props
)
// Don't continue if there's no parent
if (!targetParent) return state
// Push the new component
targetParent._children.push(newComponent.props)
store.actions.preview.saveSelected()
state.currentView = "component"
state.currentComponentInfo = newComponent.props
state.selectedComponentId = newComponent.props._id
analytics.captureEvent("Added Component", {
name: newComponent.props._component,
})
@ -370,14 +330,12 @@ export const getFrontendStore = () => {
})
},
copy: (component, cut = false) => {
const selectedAsset = get(currentAsset)
store.update(state => {
state.componentToPaste = cloneDeep(component)
state.componentToPaste.isCut = cut
if (cut) {
const parent = getParent(
state.currentPreviewItem.props,
component._id
)
const parent = findParent(selectedAsset.props, component._id)
parent._children = parent._children.filter(
child => child._id !== component._id
)
@ -387,7 +345,9 @@ export const getFrontendStore = () => {
return state
})
},
paste: (targetComponent, mode) => {
paste: async (targetComponent, mode) => {
const selectedAsset = get(currentAsset)
let promises = []
store.update(state => {
if (!state.componentToPaste) return state
@ -406,54 +366,56 @@ export const getFrontendStore = () => {
return state
}
const parent = getParent(
state.currentPreviewItem.props,
targetComponent
)
const parent = findParent(selectedAsset.props, targetComponent)
const targetIndex = parent._children.indexOf(targetComponent)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
store.actions.screens.regenerateCssForCurrentScreen()
store.actions.preview.saveSelected()
promises.push(store.actions.screens.regenerateCssForCurrentScreen())
promises.push(store.actions.preview.saveSelected())
store.actions.components.select(componentToPaste)
return state
})
await Promise.all(promises)
},
updateStyle: (type, name, value) => {
store.update(state => {
if (!state.currentComponentInfo._styles) {
state.currentComponentInfo._styles = {}
}
state.currentComponentInfo._styles[type][name] = value
updateStyle: async (type, name, value) => {
let promises = []
const selected = get(selectedComponent)
store.actions.screens.regenerateCssForCurrentScreen()
store.update(state => {
if (!selected._styles) {
selected._styles = {}
}
selected._styles[type][name] = value
promises.push(store.actions.screens.regenerateCssForCurrentScreen())
// save without messing with the store
store.actions.preview.saveSelected()
promises.push(store.actions.preview.saveSelected())
return state
})
await Promise.all(promises)
},
updateProp: (name, value) => {
store.update(state => {
let current_component = state.currentComponentInfo
let current_component = get(selectedComponent)
current_component[name] = value
state.currentComponentInfo = current_component
state.selectedComponentId = current_component._id
store.actions.preview.saveSelected()
return state
})
},
findRoute: component => {
// Gets all the components to needed to construct a path.
const tempStore = get(store)
const selectedAsset = get(currentAsset)
let pathComponents = []
let parent = component
let root = false
while (!root) {
parent = getParent(tempStore.currentPreviewItem.props, parent)
parent = findParent(selectedAsset.props, parent)
if (!parent) {
root = true
} else {
@ -461,7 +423,7 @@ export const getFrontendStore = () => {
}
}
// Remove root entry since it's the screen or page layout.
// Remove root entry since it's the screen or layout.
// Reverse array since we need the correct order of the IDs
const reversedComponents = pathComponents.reverse().slice(1)
@ -476,12 +438,13 @@ export const getFrontendStore = () => {
},
links: {
save: async (url, title) => {
let savePromise
let promises = []
const layout = get(mainLayout)
store.update(state => {
// Try to extract a nav component from the master screen
// Try to extract a nav component from the master layout
const nav = findChildComponentType(
state.pages.main,
"@budibase/standard-components/Navigation"
layout,
"@budibase/standard-components/navigation"
)
if (nav) {
let newLink
@ -513,18 +476,18 @@ export const getFrontendStore = () => {
}).props
}
// Save page and regenerate all CSS because otherwise weird things happen
// Save layout and regenerate all CSS because otherwise weird things happen
nav._children = [...nav._children, newLink]
state.currentPageName = "main"
store.actions.screens.regenerateCss(state.pages.main)
for (let screen of state.pages.main._screens) {
store.actions.screens.regenerateCss(screen)
state.currentAssetId = layout._id
promises.push(store.actions.screens.regenerateCss(layout))
for (let screen of get(allScreens)) {
promises.push(store.actions.screens.regenerateCss(screen))
}
savePromise = store.actions.pages.save()
promises.push(store.actions.layouts.save(layout))
}
return state
})
await savePromise
await Promise.all(promises)
},
},
},

View File

@ -18,7 +18,7 @@ export default function(tables) {
})
}
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new`)
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
function generateTitleContainer(table) {

View File

@ -14,9 +14,6 @@ export class Component extends BaseStructure {
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "",
_instanceName: "",
_children: [],

View File

@ -4,6 +4,7 @@ export class Screen extends BaseStructure {
constructor() {
super(true)
this._json = {
layoutId: "layout_private_master",
props: {
_id: "",
_component: "",
@ -18,7 +19,7 @@ export class Screen extends BaseStructure {
},
routing: {
route: "",
accessLevelId: "",
roleId: "BASIC",
},
name: "screen-id",
}

View File

@ -1,15 +1,21 @@
import { getBuiltin } from "components/userInterface/pagesParsing/createProps"
import { getBuiltin } from "components/userInterface/assetParsing/createProps"
import { uuid } from "./uuid"
import getNewComponentName from "./getNewComponentName"
export const getParent = (rootProps, child) => {
/**
* Find the parent component of the passed in child.
* @param {Object} rootProps - props to search for the parent in
* @param {String|Object} child - id of the child or the child itself to find the parent of
*/
export const findParent = (rootProps, child) => {
let parent
walkProps(rootProps, (p, breakWalk) => {
walkProps(rootProps, (props, breakWalk) => {
if (
p._children &&
(p._children.includes(child) || p._children.some(c => c._id === child))
props._children &&
(props._children.includes(child) ||
props._children.some(c => c._id === child))
) {
parent = p
parent = props
breakWalk()
}
})

View File

@ -62,6 +62,8 @@
</Select>
{:else if value.customType === 'password'}
<Input type="password" extraThin bind:value={block.inputs[key]} />
{:else if value.customType === 'email'}
<Input type="email" extraThin bind:value={block.inputs[key]} />
{:else if value.customType === 'table'}
<TableSelector bind:value={block.inputs[key]} />
{:else if value.customType === 'row'}

View File

@ -1,14 +1,26 @@
<script>
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
import {
Input,
Select,
Label,
DatePicker,
Toggle,
RichText,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../helpers"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
export let meta
export let creating
export let value = meta.type === "boolean" ? false : ""
$: type = meta.type
$: label = capitalise(meta.name)
$: editingUser =
!creating && $backendUiStore.selectedTable?._id === TableNames.USERS
</script>
{#if type === 'options'}
@ -29,6 +41,17 @@
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
{:else if type === 'link'}
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
{:else if type === 'longform'}
<div>
<Label extraSmall grey>{label}</Label>
<RichText bind:value />
</div>
{:else}
<Input thin {label} data-cy="{meta.name}-input" {type} bind:value />
<Input
thin
{label}
data-cy="{meta.name}-input"
{type}
bind:value
disabled={editingUser} />
{/if}

View File

@ -7,8 +7,8 @@ export async function createUser(user) {
}
export async function saveRow(row, tableId) {
const SAVE_ROWS_URL = `/api/${tableId}/rows`
const response = await api.post(SAVE_ROWS_URL, row)
const SAVE_ROW_URL = `/api/${tableId}/rows`
const response = await api.post(SAVE_ROW_URL, row)
return await response.json()
}

View File

@ -2,6 +2,7 @@
import { Input, Button, TextButton, Select, Toggle } from "@budibase/bbui"
import { cloneDeep } from "lodash/fp"
import { backendUiStore } from "builderStore"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
import { FIELDS } from "constants/backend"
import { notifier } from "builderStore/store/notifications"
import ValuesList from "components/common/ValuesList.svelte"
@ -30,6 +31,9 @@
table => table._id !== $backendUiStore.draftTable._id
)
$: required = !!field?.constraints?.presence || primaryDisplay
$: uneditable =
$backendUiStore.selectedTable?._id === TableNames.USERS &&
UNEDITABLE_USER_FIELDS.includes(field.name)
async function saveColumn() {
backendUiStore.update(state => {
@ -87,7 +91,7 @@
</script>
<div class="actions" class:hidden={deletion}>
<Input label="Name" thin bind:value={field.name} />
<Input label="Name" thin bind:value={field.name} disabled={uneditable} />
<Select
disabled={originalName}
@ -101,7 +105,7 @@
{/each}
</Select>
{#if field.type !== 'link'}
{#if field.type !== 'link' && !uneditable}
<Toggle
checked={required}
on:change={onChangeRequired}
@ -157,7 +161,7 @@
bind:value={field.fieldName} />
{/if}
<footer class="create-column-options">
{#if originalName}
{#if !uneditable && originalName}
<TextButton text on:click={confirmDelete}>Delete Column</TextButton>
{/if}
<Button secondary on:click={onClosed}>Cancel</Button>

View File

@ -1,5 +1,6 @@
<script>
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import { notifier } from "builderStore/store/notifications"
import RowFieldControl from "../RowFieldControl.svelte"
import * as api from "../api"
@ -21,9 +22,10 @@
{ ...row, tableId: table._id },
table._id
)
if (rowResponse.errors) {
errors = Object.keys(rowResponse.errors)
.map(k => ({ dataPath: k, message: rowResponse.errors[k] }))
errors = Object.entries(rowResponse.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
// Prevent modal closing if there were errors
return false
@ -38,9 +40,15 @@
confirmText={creating ? 'Create Row' : 'Save Row'}
onConfirm={saveRow}>
<ErrorsBox {errors} />
{#if creating && table._id === TableNames.USERS}
<RowFieldControl
{creating}
meta={{ name: 'password', type: 'password' }}
bind:value={row.password} />
{/if}
{#each tableSchema as [key, meta]}
<div>
<RowFieldControl {meta} bind:value={row[key]} />
<RowFieldControl {meta} bind:value={row[key]} {creating} />
</div>
{/each}
</ModalContent>

View File

@ -1,6 +1,7 @@
<script>
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import ListItem from "./ListItem.svelte"
import CreateTableModal from "./modals/CreateTableModal.svelte"
import EditTablePopover from "./popovers/EditTablePopover.svelte"
@ -43,7 +44,7 @@
{#each $backendUiStore.tables as table, idx}
<NavItem
border={idx > 0}
icon={table.integration?.type ? 'ri-database-2-line' : 'ri-table-line'}
icon={table.integration?.type ? 'ri-database-2-line' : `ri-${table._id === TableNames.USERS ? 'user' : 'table'}-line`}
text={table.name}
selected={selectedView === `all_${table._id}`}
on:click={() => selectTable(table)}>

View File

@ -54,14 +54,13 @@
const screens = screenTemplates($store, [table])
.filter(template => defaultScreens.includes(template.id))
.map(template => template.create())
store.actions.pages.select("main")
for (let screen of screens) {
// Record the table that created this screen so we can link it later
screen.autoTableId = table._id
await store.actions.screens.create(screen)
}
// Create autolink to newly created list page
// Create autolink to newly created list screen
const listScreen = screens.find(screen =>
screen.props._instanceName.endsWith("List")
)

View File

@ -1,5 +1,5 @@
<script>
import { General, Users, DangerZone, APIKeys } from "./tabs"
import { General, DangerZone, APIKeys } from "./tabs"
import { Switcher, ModalContent } from "@budibase/bbui"
const tabs = [
@ -8,11 +8,6 @@
key: "GENERAL",
component: General,
},
{
title: "Users",
key: "USERS",
component: Users,
},
{
title: "API Keys",
key: "API_KEYS",

View File

@ -1,43 +0,0 @@
<script>
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
import { Input, Select, Button } from "@budibase/bbui"
export let user
let editMode = false
</script>
<div class="inputs">
<Input
disabled
thin
bind:value={user.username}
name="Name"
placeholder="Username" />
<Select disabled={!editMode} bind:value={user.accessLevelId} thin secondary>
<option value="">Choose an option</option>
<option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>
</Select>
{#if editMode}
<Button
blue
on:click={() => {
dispatch('save', user)
editMode = false
}}>
Save
</Button>
{:else}
<Button secondary on:click={() => (editMode = true)}>Edit</Button>
{/if}
</div>
<style>
.inputs {
display: grid;
justify-items: stretch;
grid-gap: var(--spacing-m);
grid-template-columns: 1fr 1fr 140px;
}
</style>

View File

@ -1,114 +0,0 @@
<script>
import { Input, Select, Button, Label } from "@budibase/bbui"
import UserRow from "../UserRow.svelte"
import { store, backendUiStore } from "builderStore"
import api from "builderStore/api"
// import * as api from "../api"
let username = ""
let password = ""
let accessLevelId = "ADMIN"
$: valid = username && password && accessLevelId
$: appId = $store.appId
// Create user!
async function createUser() {
if (valid) {
const user = { name: username, username, password, accessLevelId }
const response = await api.post(`/api/users`, user)
const json = await response.json()
backendUiStore.actions.users.create(json)
fetchUsersPromise = fetchUsers()
}
}
// Update user!
async function updateUser(event) {
let data = event.detail
delete data.password
const response = await api.put(`/api/users`, data)
const users = await response.json()
backendUiStore.update(state => {
state.users = users
return state
})
fetchUsersPromise = fetchUsers()
}
// Get users
async function fetchUsers() {
const response = await api.get(`/api/users`)
const users = await response.json()
backendUiStore.update(state => {
state.users = users
return state
})
return users
}
let fetchUsersPromise = fetchUsers()
</script>
<div class="container">
<div>
<Label extraSmall grey>Create New User</Label>
<div class="inputs">
<Input thin bind:value={username} name="Name" placeholder="Username" />
<Input
thin
type="password"
bind:value={password}
name="Password"
placeholder="Password" />
<Select secondary bind:value={accessLevelId} thin>
<option value="">Choose an option</option>
<option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>
</Select>
<Button on:click={createUser} primary>Create</Button>
</div>
</div>
<div>
<Label extraSmall grey>Current Users</Label>
{#await fetchUsersPromise}
Loading...
{:then users}
<ul>
{#each users as user}
<li>
<UserRow {user} on:save={updateUser} />
</li>
{:else}
<li>No Users found</li>
{/each}
</ul>
{:catch err}
Something went wrong when trying to fetch users. Please refresh (CMD + R /
CTRL + R) the page and try again.
{/await}
</div>
</div>
<style>
.container {
display: grid;
grid-gap: var(--spacing-xl);
}
.inputs {
display: grid;
justify-items: stretch;
grid-gap: var(--spacing-m);
grid-template-columns: 1fr 1fr 1fr 140px;
}
ul {
list-style: none;
padding: 0;
display: grid;
grid-gap: var(--spacing-m);
margin: 0;
}
</style>

View File

@ -1,6 +1,5 @@
export { default as General } from "./General.svelte"
export { default as Integrations } from "./Integrations.svelte"
export { default as Permissions } from "./Permissions.svelte"
export { default as Users } from "./Users.svelte"
export { default as APIKeys } from "./APIKeys.svelte"
export { default as DangerZone } from "./DangerZone.svelte"

View File

@ -52,13 +52,13 @@
applicationName: string().required("Your application must have a name."),
},
{
username: string().required("Your application needs a first user."),
email: string()
.email()
.required("Your application needs a first user."),
password: string().required(
"Please enter a password for your first user."
),
accessLevelId: string().required(
"You need to select an access level for your user."
),
roleId: string().required("You need to select a role for your user."),
},
]
@ -79,9 +79,7 @@
if (hasKey) {
validationSchemas.shift()
validationSchemas = validationSchemas
steps.shift()
steps = steps
}
// Handles form navigation
@ -155,19 +153,17 @@
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
backendUiStore.actions.reset()
pkg.justCreated = true
await store.actions.initialise(pkg)
automationStore.actions.fetch()
await automationStore.actions.fetch()
} else {
throw new Error(pkg)
}
// Create user
const user = {
name: $createAppStore.values.username,
username: $createAppStore.values.username,
email: $createAppStore.values.email,
password: $createAppStore.values.password,
accessLevelId: $createAppStore.values.accessLevelId,
roleId: $createAppStore.values.roleId,
}
const userResp = await api.post(`/api/users`, user)
const json = await userResp.json()

View File

@ -2,18 +2,18 @@
import { Input, Select } from "@budibase/bbui"
export let validationErrors
let blurred = { username: false, password: false }
let blurred = { email: false, password: false }
</script>
<h2>Create your first User</h2>
<div class="container">
<Input
on:input={() => (blurred.username = true)}
label="Username"
name="username"
placeholder="Username"
type="name"
error={blurred.username && validationErrors.username} />
on:input={() => (blurred.email = true)}
label="Email"
name="email"
placeholder="Email"
type="email"
error={blurred.email && validationErrors.email} />
<Input
on:input={() => (blurred.password = true)}
label="Password"
@ -21,7 +21,7 @@
placeholder="Password"
type="password"
error={blurred.password && validationErrors.password} />
<Select label="Access Level" secondary name="accessLevelId">
<Select label="Role" secondary name="roleId">
<option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>
</Select>

View File

@ -1,146 +1,79 @@
<script>
import { store, backendUiStore } from "builderStore"
import { map, join } from "lodash/fp"
import { onMount } from "svelte"
import { store, currentAsset } from "builderStore"
import iframeTemplate from "./iframeTemplate"
import { pipe } from "../../../helpers"
import { Screen } from "../../../builderStore/store/screenTemplates/utils/Screen"
import { Component } from "../../../builderStore/store/screenTemplates/utils/Component"
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
import { FrontendTypes } from "../../../constants"
let iframe
let styles = ""
let layout
let screen
function transform_component(comp) {
const props = comp.props || comp
if (props && props._children && props._children.length) {
props._children = props._children.map(transform_component)
}
return props
}
const getComponentTypeName = component => {
let [componentName] = component._component.match(/[a-z]*$/)
return componentName || "element"
}
const headingStyle = {
width: "500px",
padding: "8px",
}
const textStyle = {
...headingStyle,
"max-width": "",
"text-align": "left",
}
const heading = new Component("@budibase/standard-components/heading")
.normalStyle(headingStyle)
.type("h1")
.text("Screen Slot")
.instanceName("Heading")
const textScreenDisplay = new Component("@budibase/standard-components/text")
.normalStyle(textStyle)
.instanceName("Text")
.type("none")
.text(
"The screens that you create will be displayed inside this box. This box is just a placeholder, to show you the position of screens."
)
const container = new Component("@budibase/standard-components/container")
.normalStyle({
display: "flex",
"flex-direction": "column",
"align-items": "center",
flex: "1 1 auto",
})
.type("div")
.instanceName("Container")
.addChild(heading)
.addChild(textScreenDisplay)
// Create screen slot placeholder for use when a page is selected rather
// than a screen
const screenPlaceholder = new Screen()
.name("Screen Placeholder")
.route("*")
.component("@budibase/standard-components/container")
.mainType("div")
.component("@budibase/standard-components/screenslotplaceholder")
.instanceName("Content Placeholder")
.normalStyle({
flex: "1 1 auto",
})
.addChild(container)
.json()
// TODO: this ID is attached to how the screen slot is rendered, confusing, would be better a type etc
screenPlaceholder.props._id = "screenslot-placeholder"
$: hasComponent = !!$store.currentPreviewItem
// Extract data to pass to the iframe
$: {
styles = ""
// Apply the CSS from the currently selected page and its screens
const currentPage = $store.pages[$store.currentPageName]
styles += currentPage._css
for (let screen of currentPage._screens) {
styles += screen._css
if ($store.currentFrontEndType === FrontendTypes.LAYOUT) {
layout = $currentAsset
screen = screenPlaceholder
} else {
screen = $currentAsset
layout = $store.layouts.find(layout => layout._id === screen?.layoutId)
}
styles = styles
}
$: selectedComponentId = $store.selectedComponentId ?? ""
$: previewData = {
layout,
screen,
selectedComponentId,
}
$: stylesheetLinks = pipe($store.pages.stylesheets, [
map(s => `<link rel="stylesheet" href="${s}"/>`),
join("\n"),
])
// Saving pages and screens to the DB causes them to have _revs.
// These revisions change every time a save happens and causes
// these reactive statements to fire, even though the actual
// definition hasn't changed.
// By deleting all _rev properties we can avoid this and increase
// performance.
$: json = JSON.stringify(previewData)
$: strippedJson = json.replaceAll(/"_rev":\s*"[^"]+"/g, `"_rev":""`)
$: screensExist =
$store.currentPreviewItem._screens &&
$store.currentPreviewItem._screens.length > 0
$: frontendDefinition = {
appId: $store.appId,
libraries: $store.libraries,
page: $store.pages[$store.currentPageName],
screens: [
$store.currentFrontEndType === "page"
? screenPlaceholder
: $store.currentPreviewItem,
],
// Update the iframe with the builder info to render the correct preview
const refreshContent = message => {
if (iframe) {
iframe.contentWindow.postMessage(message)
}
}
$: selectedComponentType = getComponentTypeName($store.currentComponentInfo)
// Refresh the preview when required
$: refreshContent(strippedJson)
$: selectedComponentId = $store.currentComponentInfo
? $store.currentComponentInfo._id
: ""
const refreshContent = () => {
iframe.contentWindow.postMessage(
JSON.stringify({
styles,
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition,
appId: $store.appId,
instanceId: $backendUiStore.selectedDatabase._id,
})
// Initialise the app when mounted
onMount(() => {
iframe.contentWindow.addEventListener(
"bb-ready",
() => {
refreshContent(strippedJson)
},
{
once: true,
}
)
}
$: if (iframe)
iframe.contentWindow.addEventListener("bb-ready", refreshContent, {
once: true,
})
$: if (iframe && frontendDefinition) {
refreshContent()
}
})
</script>
<div class="component-container">
{#if hasComponent && $store.currentPreviewItem}
<iframe
style="height: 100%; width: 100%"
title="componentPreview"
bind:this={iframe}
srcdoc={iframeTemplate} />
{/if}
<iframe
style="height: 100%; width: 100%"
title="componentPreview"
bind:this={iframe}
srcdoc={iframeTemplate} />
</div>
<style>
@ -152,7 +85,6 @@
margin: auto;
height: 100%;
}
.component-container iframe {
border: 0;
left: 0;

View File

@ -0,0 +1,52 @@
<html>
<head>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<style>
body, html {
height: 100% !important;
font-family: Inter, sans-serif !important;
margin: 0 !important;
}
*, *:before, *:after {
box-sizing: border-box;
}
</style>
<script src='/assets/budibase-client.js'></script>
<script>
function receiveMessage(event) {
if (!event.data) {
return
}
// Extract data from message
const { selectedComponentId, layout, screen } = JSON.parse(event.data)
// Set some flags so the app knows we're in the builder
window["##BUDIBASE_IN_BUILDER##"] = true
window["##BUDIBASE_PREVIEW_LAYOUT##"] = layout
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
// Initialise app
if (window.loadBudibase) {
loadBudibase()
}
}
// Ignore clicks
["click", "mousedown"].forEach(type => {
document.addEventListener(type, function(e) {
e.preventDefault()
e.stopPropagation()
return false
}, true)
})
window.addEventListener("message", receiveMessage)
window.dispatchEvent(new Event("bb-ready"))
</script>
</head>
<body>
</body>
</html>

View File

@ -1,77 +1 @@
export default `<html>
<head>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
<style>
body, html {
height: 100%!important;
font-family: Inter !important;
margin: 0px!important;
}
*, *:before, *:after {
box-sizing: border-box;
}
.container-screenslot-placeholder {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
text-align: center;
border-style: dashed !important;
border-width: 1px;
color: #000000;
background-color: rgba(0, 0, 0, 0.05);
flex: 1 1 auto;
}
.container-screenslot-placeholder span {
display: block;
margin-bottom: 10px;
}
</style>
<script src='/assets/budibase-client.js'></script>
<script>
function receiveMessage(event) {
if (!event.data) return
const data = JSON.parse(event.data)
try {
if (styles) document.head.removeChild(styles)
} catch(_) { }
try {
if (selectedComponentStyle) document.head.removeChild(selectedComponentStyle)
} catch(_) { }
selectedComponentStyle = document.createElement('style');
document.head.appendChild(selectedComponentStyle)
var selectedCss = '.' + data.selectedComponentType + '-' + data.selectedComponentId + '{ border: 2px solid #0055ff; }'
selectedComponentStyle.appendChild(document.createTextNode(selectedCss))
styles = document.createElement('style')
document.head.appendChild(styles)
styles.appendChild(document.createTextNode(data.styles))
window["##BUDIBASE_FRONTEND_DEFINITION##"] = data.frontendDefinition;
if (window.loadBudibase) {
loadBudibase({ window, localStorage })
}
}
let styles
let selectedComponentStyle
document.addEventListener("click", function(e) {
e.preventDefault()
e.stopPropagation()
return false;
}, true)
window.addEventListener('message', receiveMessage)
window.dispatchEvent(new Event('bb-ready'))
</script>
</head>
<body>
</body>
</html>`
export { default } from "./iframeTemplate.html"

View File

@ -1,10 +1,11 @@
<script>
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { get } from "svelte/store"
import { store, currentAsset } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last } from "lodash/fp"
import { getParent } from "builderStore/storeUtils"
import { findParent } from "builderStore/storeUtils"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
@ -15,7 +16,8 @@
let anchor
$: noChildrenAllowed =
!component || !getComponentDefinition($store, component._component).children
!component ||
!getComponentDefinition($store, component._component)?.children
$: noPaste = !$store.componentToPaste
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
@ -27,12 +29,13 @@
const selectComponent = component => {
store.actions.components.select(component)
const path = store.actions.components.findRoute(component)
$goto(`./:page/:screen/${path}`)
$goto(`./${$store.currentFrontEndType}/${path}`)
}
const moveUpComponent = () => {
store.update(state => {
const parent = getParent(state.currentPreviewItem.props, component)
const asset = get(currentAsset)
const parent = findParent(asset.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
@ -42,7 +45,7 @@
newChildren.splice(currentIndex - 1, 0, component)
parent._children = newChildren
}
state.currentComponentInfo = component
state.selectedComponentId = component._id
store.actions.preview.saveSelected()
return state
@ -51,7 +54,8 @@
const moveDownComponent = () => {
store.update(state => {
const parent = getParent(state.currentPreviewItem.props, component)
const asset = get(currentAsset)
const parent = findParent(asset.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
@ -61,7 +65,7 @@
newChildren.splice(currentIndex + 1, 0, component)
parent._children = newChildren
}
state.currentComponentInfo = component
state.selectedComponentId = component._id
store.actions.preview.saveSelected()
return state
@ -75,10 +79,11 @@
const deleteComponent = () => {
store.update(state => {
const parent = getParent(state.currentPreviewItem.props, component)
const asset = get(currentAsset)
const parent = findParent(asset.props, component)
if (parent) {
parent._children = parent._children.filter(c => c !== component)
parent._children = parent._children.filter(child => child !== component)
selectComponent(parent)
}

View File

@ -1,6 +1,6 @@
<script>
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { store, currentAsset } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils"
import { DropEffect, DropPosition } from "./dragDropStore"
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
@ -10,7 +10,6 @@
export let currentComponent
export let onSelect = () => {}
export let level = 0
export let dragDropStore
const isScreenslot = name => name === "##builtin/screenslot"
@ -23,7 +22,7 @@
const path = store.actions.components.findRoute(component)
// Go to correct URL
$goto(`./:page/:screen/${path}`)
$goto(`./${$store.currentAssetId}/${path}`)
}
const dragstart = component => e => {
@ -73,7 +72,7 @@
text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
withArrow
indentLevel={level + 3}
selected={currentComponent === component}>
selected={$store.selectedComponentId === component._id}>
<ComponentDropdownMenu {component} />
</NavItem>

View File

@ -0,0 +1,77 @@
<script>
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { DropdownMenu, Modal, ModalContent, Input } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
import { cloneDeep } from "lodash/fp"
export let layout
let confirmDeleteDialog
let editLayoutNameModal
let dropdown
let anchor
let name = layout.name
const deleteLayout = async () => {
try {
await store.actions.layouts.delete(layout)
notifier.success(`Layout ${layout.name} deleted successfully.`)
} catch (err) {
notifier.danger(`Error deleting layout: ${err.message}`)
}
}
const saveLayout = async () => {
try {
const layoutToSave = cloneDeep(layout)
layoutToSave.name = name
await store.actions.layouts.save(layoutToSave)
notifier.success(`Layout saved successfully.`)
} catch (err) {
notifier.danger(`Error saving layout: ${err.message}`)
}
}
</script>
<div bind:this={anchor} on:click|stopPropagation>
<div class="icon" on:click={() => dropdown.show()}>
<i class="ri-more-line" />
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
<DropdownContainer>
<DropdownItem
icon="ri-pencil-line"
title="Edit"
on:click={() => editLayoutNameModal.show()} />
<DropdownItem
icon="ri-delete-bin-line"
title="Delete"
on:click={() => confirmDeleteDialog.show()} />
</DropdownContainer>
</DropdownMenu>
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Deletion"
body={'Are you sure you wish to delete this layout?'}
okText="Delete Layout"
onOk={deleteLayout} />
<Modal bind:this={editLayoutNameModal}>
<ModalContent
title="Edit Layout Name"
confirmText="Save"
onConfirm={saveLayout}
disabled={!name}>
<Input thin type="text" label="Name" bind:value={name} />
</ModalContent>
</Modal>
<style>
.icon i {
font-size: 16px;
}
</style>

View File

@ -1,25 +1,32 @@
<script>
import { writable } from "svelte/store"
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { store, selectedComponent, currentAsset } from "builderStore"
import instantiateStore from "./dragDropStore"
import ComponentsTree from "./ComponentTree.svelte"
import ComponentTree from "./ComponentTree.svelte"
import NavItem from "components/common/NavItem.svelte"
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
const ROUTE_NAME_MAP = {
"/": {
BASIC: "Home",
PUBLIC: "Login",
},
}
const dragDropStore = instantiateStore()
export let route
export let path
export let indent
$: selectedScreen = $store.currentPreviewItem
$: selectedScreen = $currentAsset
const changeScreen = screenId => {
// select the route
store.actions.screens.select(screenId)
$goto(`./:page/${screenId}`)
$goto(`./${screenId}`)
}
</script>
@ -30,21 +37,21 @@
withArrow={route.subpaths} />
{#each Object.entries(route.subpaths) as [url, subpath]}
{#each Object.values(subpath.screens) as screenId}
{#each Object.entries(subpath.screens) as [role, screenId]}
<NavItem
icon="ri-artboard-2-line"
indentLevel={indent || 1}
selected={$store.currentPreviewItem._id === screenId}
opened={$store.currentPreviewItem._id === screenId}
text={url === '/' ? 'Home' : url}
selected={$store.currentAssetId === screenId}
opened={$store.currentAssetId === screenId}
text={ROUTE_NAME_MAP[url]?.[role] || url}
withArrow={route.subpaths}
on:click={() => changeScreen(screenId)}>
<ScreenDropdownMenu screen={screenId} />
<ScreenDropdownMenu {screenId} />
</NavItem>
{#if selectedScreen?._id === screenId}
<ComponentsTree
<ComponentTree
components={selectedScreen.props._children}
currentComponent={$store.currentComponentInfo}
currentComponent={$selectedComponent}
{dragDropStore} />
{/if}
{/each}

View File

@ -3,28 +3,27 @@
import { store, allScreens } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownMenu, Modal, ModalContent } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let screen
export let screenId
let confirmDeleteDialog
let dropdown
let anchor
$: screen = $allScreens.find(screen => screen._id === screenId)
const deleteScreen = () => {
const screenToDelete = $allScreens.find(scr => scr._id === screen)
store.actions.screens.delete(screenToDelete)
store.actions.routing.fetch()
// update the page if required
store.update(state => {
if (state.currentPreviewItem._id === screen) {
store.actions.pages.select($store.currentPageName)
notifier.success(`Screen ${screenToDelete.name} deleted successfully.`)
$goto(`./:page/page-layout`)
}
return state
})
try {
store.actions.screens.delete(screen)
store.actions.routing.fetch()
confirmDeleteDialog.hide()
$goto("../")
notifier.success("Deleted screen successfully.")
} catch (err) {
notifier.danger("Error deleting screen")
}
}
</script>

View File

@ -48,7 +48,7 @@ export default function() {
if (mousePosition > 0.4 && mousePosition < 0.8) {
state.dropPosition = DropPosition.INSIDE
}
return
return state
}
// bottom half

View File

@ -5,7 +5,7 @@
</script>
<div class="root">
{#each Object.keys($store.routes) as path}
{#each Object.keys($store.routes || {}) as path}
<PathTree {path} route={$store.routes[path]} />
{/each}
</div>

View File

@ -1,5 +1,7 @@
<script>
import { store } from "builderStore"
import { get } from "svelte/store"
import { store, selectedComponent, currentAsset } from "builderStore"
import { FrontendTypes } from "constants"
import panelStructure from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte"
@ -14,12 +16,12 @@
$: componentInstance =
$store.currentView !== "component"
? { ...$store.currentPreviewItem, ...$store.currentComponentInfo }
: $store.currentComponentInfo
? { ...$currentAsset, ...$selectedComponent }
: $selectedComponent
$: componentDefinition = $store.components[componentInstance._component]
$: componentPropDefinition =
flattenedPanel.find(
//use for getting controls for each component property
// use for getting controls for each component property
c => c._component === componentInstance._component
) || {}
@ -31,7 +33,7 @@
$: isComponentOrScreen =
$store.currentView === "component" ||
$store.currentFrontEndType === "screen"
$store.currentFrontEndType === FrontendTypes.SCREEN
$: isNotScreenslot = componentInstance._component !== "##builtin/screenslot"
$: displayName =
@ -58,16 +60,20 @@
return components
}
function setPageOrScreenProp(name, value) {
function setAssetProps(name, value) {
const selectedAsset = get(currentAsset)
store.update(state => {
if (name === "_instanceName" && state.currentFrontEndType === "screen") {
state.currentPreviewItem.props[name] = value
if (
name === "_instanceName" &&
state.currentFrontEndType === FrontendTypes.SCREEN
) {
selectedAsset.props._instanceName = value
} else {
state.currentPreviewItem[name] = value
selectedAsset[name] = value
}
store.actions.preview.saveSelected()
return state
})
store.actions.preview.saveSelected()
}
function getProps(obj, keys) {
@ -94,21 +100,12 @@
{panelDefinition}
displayNameField={displayName}
onChange={store.actions.components.updateProp}
onScreenPropChange={setPageOrScreenProp}
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
onScreenPropChange={setAssetProps}
assetInstance={$store.currentView !== 'component' && $currentAsset} />
{/if}
</div>
<style>
.title > div:nth-child(1) {
grid-column-start: name;
color: var(--ink);
}
.title > div:nth-child(2) {
grid-column-start: actions;
}
.component-props-container {
flex: 1 1 auto;
min-height: 0;

View File

@ -1,6 +1,6 @@
<script>
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { goto, url } from "@sveltech/routify"
import { store, currentAssetName, selectedComponent } from "builderStore"
import components from "./temporaryPanelStructure.js"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
@ -26,8 +26,8 @@
const onComponentChosen = component => {
store.actions.components.create(component._component, component.presetProps)
const path = store.actions.components.findRoute($store.currentComponentInfo)
$goto(`./:page/:screen/${path}`)
const path = store.actions.components.findRoute($selectedComponent)
$goto(`./${$store.currentAssetId}/${path}`)
close()
}
</script>
@ -52,7 +52,7 @@
align="left">
<DropdownContainer>
{#each categories[selectedIndex].children as item}
{#if !item.showOnPages || item.showOnPages.includes($store.currentPageName)}
{#if !item.showOnAsset || item.showOnAsset.includes($currentAssetName)}
<DropdownItem
icon={item.icon}
title={item.name}

View File

@ -1,5 +1,6 @@
<script>
import { store, allScreens } from "builderStore"
import { FrontendTypes } from "constants"
import ComponentPropertiesPanel from "./ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "./ComponentSelectionList.svelte"
@ -18,7 +19,7 @@
</script>
<div class="root">
{#if $store.currentFrontEndType === 'page' || $allScreens.length}
{#if $store.currentFrontEndType === FrontendTypes.LAYOUT || $allScreens.length}
<div class="switcher">
<button
class:selected={selected === COMPONENT_SELECTION_TAB}

View File

@ -12,8 +12,6 @@
let propGroup = null
let currentGroup
const getProperties = name => panelDefinition[name]
function onChange(category) {
selectedCategory = category
}
@ -38,7 +36,7 @@
{#each propertyGroupNames as groupName}
<PropertyGroup
name={groupName}
properties={getProperties(groupName)}
properties={panelDefinition[groupName]}
styleCategory={selectedCategory}
{onStyleChanged}
{componentDefinition}
@ -64,9 +62,6 @@
gap: var(--spacing-l);
}
.design-view-state-categories {
}
.positioned-wrapper {
position: relative;
display: flex;

View File

@ -1,11 +1,11 @@
<script>
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import { EVENT_TYPE_MEMBER_NAME } from "../../../../../client/src/state/eventHandlers"
import actionTypes from "./actions"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
const eventTypeKey = "##eventHandlerType"
export let event
@ -18,8 +18,7 @@
$: actions = event || []
$: selectedActionComponent =
selectedAction &&
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_MEMBER_NAME])
.component
actionTypes.find(t => t.name === selectedAction[eventTypeKey]).component
const updateEventHandler = (updatedHandler, index) => {
actions[index] = updatedHandler
@ -33,7 +32,7 @@
const addAction = actionType => () => {
const newAction = {
parameters: {},
[EVENT_TYPE_MEMBER_NAME]: actionType.name,
[eventTypeKey]: actionType.name,
}
actions.push(newAction)
selectedAction = newAction
@ -79,7 +78,7 @@
{#each actions as action, index}
<div class="action-container">
<div class="action-header" on:click={selectAction(action)}>
<Body small lh>{index + 1}. {action[EVENT_TYPE_MEMBER_NAME]}</Body>
<Body small lh>{index + 1}. {action[eventTypeKey]}</Body>
<div class="row-expander" class:rotate={action !== selectedAction}>
<ArrowDownIcon />
</div>

View File

@ -1,127 +0,0 @@
<script>
import { keys, map, includes, filter } from "lodash/fp"
import EventEditorModal from "./EventEditorModal.svelte"
import { Modal } from "@budibase/bbui"
export const EVENT_TYPE = "event"
export let component
let events = []
let selectedEvent = null
let modal
$: {
events = Object.keys(component)
// TODO: use real events
.filter(propName => ["onChange", "onClick", "onLoad"].includes(propName))
.map(propName => ({
name: propName,
handlers: component[propName] || [],
}))
}
const openModal = event => {
selectedEvent = event
modal.show()
}
</script>
<button class="newevent" on:click={() => openModal()}>
<i class="icon ri-add-circle-fill" />
Create New Event
</button>
<div class="root">
<form on:submit|preventDefault class="form-root">
{#each events as event, index}
{#if event.handlers.length > 0}
<div
class:selected={selectedEvent && selectedEvent.index === index}
class="handler-container budibase__nav-item"
on:click={() => openModal({ ...event, index })}>
<span class="event-name">{event.name}</span>
<span class="edit-text">EDIT</span>
</div>
{/if}
{/each}
</form>
</div>
<Modal bind:this={modal} width="600px">
<EventEditorModal eventOptions={events} event={selectedEvent} />
</Modal>
<style>
.root {
font-size: 10pt;
width: 100%;
}
.newevent {
cursor: pointer;
border: 1px solid var(--grey-4);
border-radius: 3px;
width: 100%;
padding: 8px 16px;
margin: 0px 0px 12px 0px;
display: flex;
justify-content: center;
align-items: center;
background: var(--background);
color: var(--ink);
font-size: 14px;
font-weight: 500;
transition: all 2ms;
}
.newevent:hover {
background: var(--grey-1);
}
.icon {
color: var(--ink);
font-size: 16px;
margin-right: 4px;
}
.form-root {
display: flex;
flex-wrap: wrap;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
}
.handler-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
border: 2px solid var(--grey-1);
height: 80px;
width: 100%;
}
.event-name {
margin-top: 5px;
font-weight: bold;
font-size: 16px;
color: rgba(22, 48, 87, 0.6);
align-self: end;
}
.edit-text {
font-family: Arial, Helvetica, sans-serif;
font-weight: bold;
align-self: end;
justify-self: end;
font-size: 10px;
color: rgba(35, 65, 105, 0.4);
}
.selected {
color: var(--blue);
background: var(--grey-1) !important;
}
</style>

View File

@ -1,53 +0,0 @@
<script>
import { Input, DataList, Select } from "@budibase/bbui"
import { automationStore, allScreens } from "builderStore"
export let parameter
let isOpen = false
const capitalize = s => {
if (typeof s !== "string") return ""
return s.charAt(0).toUpperCase() + s.slice(1)
}
</script>
<div class="handler-option">
{#if parameter.name === 'automation'}<span>{parameter.name}</span>{/if}
{#if parameter.name === 'automation'}
<Select on:change bind:value={parameter.value}>
<option value="" />
{#each $automationStore.automations.filter(wf => wf.live) as automation}
<option value={automation._id}>{automation.name}</option>
{/each}
</Select>
{:else if parameter.name === 'url'}
<DataList on:change bind:value={parameter.value}>
<option value="" />
{#each $allScreens as screen}
<option value={screen.routing.route}>
{screen.props._instanceName}
</option>
{/each}
</DataList>
{:else}
<Input
name={parameter.name}
label={capitalize(parameter.name)}
on:change
value={parameter.value} />
{/if}
</div>
<style>
.handler-option {
display: flex;
flex-direction: column;
}
span {
font-size: 18px;
margin-bottom: 10px;
font-weight: 500;
}
</style>

View File

@ -1,15 +1,15 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import SaveFields from "./SaveFields.svelte"
export let parameters
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $store.currentPreviewItem,
screen: $currentAsset,
tables: $backendUiStore.tables,
})

View File

@ -1,6 +1,6 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
export let parameters
@ -8,9 +8,9 @@
let idFields
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $store.currentPreviewItem,
screen: $currentAsset,
tables: $backendUiStore.tables,
})

View File

@ -1,7 +1,7 @@
<script>
// accepts an array of field names, and outputs an object of { FieldName: value }
import { DataList, Label, TextButton, Spacer, Select } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
import {
@ -32,9 +32,9 @@
}))
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $store.currentPreviewItem,
screen: $currentAsset,
tables: $backendUiStore.tables,
})

View File

@ -1,6 +1,6 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import SaveFields from "./SaveFields.svelte"
import {
@ -16,9 +16,9 @@
let schemaFields
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $store.currentPreviewItem,
screen: $currentAsset,
tables: $backendUiStore.tables,
})

View File

@ -1,6 +1,6 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import SaveFields from "./SaveFields.svelte"
import {
@ -11,9 +11,9 @@
export let parameters
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $store.currentPreviewItem,
screen: $currentAsset,
tables: $backendUiStore.tables,
})

View File

@ -1,9 +1,8 @@
<script>
import { buildStyle } from "../../helpers.js"
export let value = ""
export let text = ""
export let icon = ""
export let onClick = value => {}
export let onClick = () => {}
export let selected = false
$: useIcon = !!icon

View File

@ -1,6 +1,7 @@
<script>
import { onMount } from "svelte"
import FlatButton from "./FlatButton.svelte"
export let buttonProps = []
export let isMultiSelect = false
export let value = []

View File

@ -1,16 +1,33 @@
<script>
import { onMount } from "svelte"
import { store, currentScreens } from "builderStore"
import api from "builderStore/api"
import { goto, params, url } from "@sveltech/routify"
import { store, currentAsset, selectedComponent } from "builderStore"
import { FrontendTypes } from "constants"
import ComponentNavigationTree from "components/userInterface/ComponentNavigationTree/index.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
import Layout from "components/userInterface/Layout.svelte"
import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
import { Modal } from "@budibase/bbui"
import NewLayoutModal from "components/userInterface/NewLayoutModal.svelte"
import { Modal, Switcher } from "@budibase/bbui"
const tabs = [
{
title: "Screens",
key: "screen",
},
{
title: "Layouts",
key: "layout",
},
]
let modal
let routes = {}
let tab = $params.assetType
function navigate({ detail }) {
if (!detail) return
$goto(`../${detail.heading.key}`)
}
onMount(() => {
store.actions.routing.fetch()
@ -18,32 +35,48 @@
</script>
<div class="title">
<h1>Screens</h1>
<i on:click={modal.show} data-cy="new-screen" class="ri-add-circle-fill" />
<Switcher headings={tabs} bind:value={tab} on:change={navigate}>
{#if tab === FrontendTypes.SCREEN}
<i
on:click={modal.show}
data-cy="new-screen"
class="ri-add-circle-fill" />
{#if $currentAsset}
<div class="nav-items-container">
<ComponentNavigationTree />
</div>
{/if}
<Modal bind:this={modal}>
<NewScreenModal />
</Modal>
{:else if tab === FrontendTypes.LAYOUT}
<i
on:click={modal.show}
data-cy="new-layout"
class="ri-add-circle-fill" />
{#each $store.layouts as layout (layout._id)}
<Layout {layout} />
{/each}
<Modal bind:this={modal}>
<NewLayoutModal />
</Modal>
{/if}
</Switcher>
</div>
<PagesList />
<div class="nav-items-container">
<PageLayout layout={$store.pages[$store.currentPageName]} />
<ComponentNavigationTree />
</div>
<Modal bind:this={modal}>
<NewScreenModal />
</Modal>
<style>
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.title h1 {
font-size: var(--font-size-m);
font-weight: 500;
margin: 0;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
position: relative;
}
.title i {
font-size: 20px;
position: absolute;
top: 0;
right: 0;
}
.title i:hover {
cursor: pointer;

View File

@ -0,0 +1,41 @@
<script>
import { goto } from "@sveltech/routify"
import { FrontendTypes } from "constants"
import ComponentTree from "./ComponentNavigationTree/ComponentTree.svelte"
import LayoutDropdownMenu from "./ComponentNavigationTree/LayoutDropdownMenu.svelte"
import initDragDropStore from "./ComponentNavigationTree/dragDropStore"
import NavItem from "components/common/NavItem.svelte"
import { last } from "lodash/fp"
import { store, currentAsset, selectedComponent } from "builderStore"
import { writable } from "svelte/store"
export let layout
let confirmDeleteDialog
let componentToDelete = ""
const dragDropStore = initDragDropStore()
const selectLayout = () => {
store.actions.layouts.select(layout._id)
$goto(`./${layout._id}`)
}
</script>
<NavItem
border={false}
icon="ri-layout-3-line"
text={layout.name}
withArrow
selected={$store.currentAssetId === layout._id}
opened={$store.currentAssetId === layout._id}
on:click={selectLayout}>
<LayoutDropdownMenu {layout} />
</NavItem>
{#if $store.currentAssetId === layout._id && layout.props?._children}
<ComponentTree
components={layout.props._children}
currentComponent={$selectedComponent}
{dragDropStore} />
{/if}

View File

@ -0,0 +1,13 @@
<script>
import { store, currentAsset } from "builderStore"
import { Select } from "@budibase/bbui"
export let value
</script>
<Select bind:value extraThin secondary on:change>
<option value="">Choose an option</option>
{#each $store.layouts as layout}
<option value={layout._id}>{layout.name}</option>
{/each}
</Select>

View File

@ -0,0 +1,26 @@
<script>
import { goto } from "@sveltech/routify"
import api from "builderStore/api"
import { notifier } from "builderStore/store/notifications"
import { store, backendUiStore, allScreens } from "builderStore"
import { Input, ModalContent } from "@budibase/bbui"
import analytics from "analytics"
const CONTAINER = "@budibase/standard-components/container"
let name = ""
async function save() {
try {
await store.actions.layouts.save({ name })
$goto(`./${$store.currentAssetId}`)
notifier.success(`Layout ${name} created successfully`)
} catch (err) {
notifier.danger(`Error creating layout ${name}.`)
}
}
</script>
<ModalContent title="Create Layout" confirmText="Create" onConfirm={save}>
<Input thin label="Name" bind:value={name} />
</ModalContent>

View File

@ -70,9 +70,9 @@
draftScreen.props._instanceName = name
draftScreen.props._component = baseComponent
// TODO: need to fix this up correctly
draftScreen.routing = { route, accessLevelId: "ADMIN" }
draftScreen.routing = { route, roleId: "ADMIN" }
await store.actions.screens.create(draftScreen)
const createdScreen = await store.actions.screens.create(draftScreen)
if (createLink) {
await store.actions.components.links.save(route, name)
}
@ -85,7 +85,7 @@
})
}
$goto(`./:page/${name}`)
$goto(`./screen/${createdScreen._id}`)
}
const routeNameExists = route => {

View File

@ -1,44 +0,0 @@
<script>
import { goto } from "@sveltech/routify"
import ComponentTree from "./ComponentNavigationTree/ComponentTree.svelte"
import NavItem from "components/common/NavItem.svelte"
import { last } from "lodash/fp"
import { store } from "builderStore"
import { writable } from "svelte/store"
export let layout
let confirmDeleteDialog
let componentToDelete = ""
const dragDropStore = writable({})
const lastPartOfName = c =>
c && last(c.name ? c.name.split("/") : c._component.split("/"))
$: _layout = {
component: layout,
title: lastPartOfName(layout),
}
const setCurrentScreenToLayout = () => {
store.actions.selectPageOrScreen("page")
$goto("./:page/page-layout")
}
</script>
<NavItem
border={false}
icon="ri-layout-3-line"
text="Master Screen"
withArrow
selected={$store.currentComponentInfo?._id === _layout.component.props._id}
opened={$store.currentPreviewItem?.name === _layout.title}
on:click={setCurrentScreenToLayout} />
{#if $store.currentPreviewItem?.name === _layout.title && _layout.component.props._children}
<ComponentTree
components={_layout.component.props._children}
currentComponent={$store.currentComponentInfo}
{dragDropStore} />
{/if}

View File

@ -1,3 +0,0 @@
<script>
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
</script>

View File

@ -1,65 +0,0 @@
<script>
import { params, goto } from "@sveltech/routify"
import { store } from "builderStore"
const getPage = (state, name) => {
const props = state.pages[name]
return { name, props }
}
const pages = [
{
title: "Private",
id: "main",
},
{
title: "Public",
id: "unauthenticated",
},
]
if (!$store.currentPageName)
store.actions.pages.select($params.page ? $params.page : "main")
const changePage = id => {
store.actions.pages.select(id)
$goto(`./${id}/page-layout`)
}
</script>
<div class="root">
{#each pages as { title, id }}
<button class:active={id === $params.page} on:click={() => changePage(id)}>
{title}
</button>
{/each}
</div>
<style>
.root {
display: flex;
flex-direction: row;
}
button {
cursor: pointer;
padding: 0 var(--spacing-m);
height: 32px;
text-align: center;
background: var(--background);
color: var(--grey-7);
border-radius: 5px;
font-size: var(--font-size-xs);
font-weight: 500;
transition: all 0.3s;
text-rendering: optimizeLegibility;
border: none !important;
outline: none;
font-family: var(--font-sans);
}
.active {
background: var(--grey-2);
color: var(--ink);
}
</style>

View File

@ -1,7 +1,7 @@
<script>
import { Icon } from "@budibase/bbui"
import Input from "./PropertyPanelControls/Input.svelte"
import { store, backendUiStore } from "builderStore"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import {
readableToRuntimeBinding,
@ -22,22 +22,20 @@
export let onChange = () => {}
let temporaryBindableValue = value
let bindableProperties = []
let anchor
let dropdown
function handleClose() {
handleChange(key, temporaryBindableValue)
}
let bindableProperties = []
let anchor
let dropdown
function getBindableProperties() {
// Get all bindableProperties
bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $store.currentPreviewItem,
screen: $currentAsset,
tables: $backendUiStore.tables,
})
}
@ -77,7 +75,7 @@
: temp
}
//Incase the component has a different value key name
// Incase the component has a different value key name
const handlevalueKey = value =>
props.valueKey ? { [props.valueKey]: safeValue() } : { value: safeValue() }
</script>
@ -94,7 +92,7 @@
{...props}
name={key} />
</div>
{#if bindable && control === Input && !key.startsWith('_')}
{#if bindable && !key.startsWith('_') && control === Input}
<div
class="icon"
data-cy={`${key}-binding-button`}

View File

@ -1,7 +1,7 @@
<script>
import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store, allScreens, backendUiStore } from "builderStore"
import { store, allScreens, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
const dispatch = createEventDispatcher()
@ -27,9 +27,9 @@
]
const bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $store.currentPreviewItem,
screen: $currentAsset,
tables: $backendUiStore.tables,
})

View File

@ -1,10 +1,11 @@
<script>
import { isEmpty } from "lodash/fp"
import { FrontendTypes } from "constants"
import PropertyControl from "./PropertyControl.svelte"
import LayoutSelect from "./LayoutSelect.svelte"
import Input from "./PropertyPanelControls/Input.svelte"
import { goto } from "@sveltech/routify"
import { excludeProps } from "./propertyCategories.js"
import { store, allScreens } from "builderStore"
import { store, allScreens, currentAsset } from "builderStore"
import { walkProps } from "builderStore/storeUtils"
export let panelDefinition = []
@ -13,13 +14,13 @@
export let onChange = () => {}
export let onScreenPropChange = () => {}
export let displayNameField = false
export let screenOrPageInstance
export let assetInstance
let pageScreenProps = ["title", "favicon", "description", "route"]
let assetProps = ["title", "description", "route", "layoutId"]
let duplicateName = false
const propExistsOnComponentDef = prop =>
pageScreenProps.includes(prop) || prop in componentDefinition.props
assetProps.includes(prop) || prop in componentDefinition.props
function handleChange(key, data) {
data.target ? onChange(key, data.target.value) : onChange(key, data)
@ -28,12 +29,10 @@
const screenDefinition = [
{ key: "description", label: "Description", control: Input },
{ key: "route", label: "Route", control: Input },
{ key: "layoutId", label: "Layout", control: LayoutSelect },
]
const pageDefinition = [
{ key: "title", label: "Title", control: Input },
{ key: "favicon", label: "Favicon", control: Input },
]
const layoutDefinition = [{ key: "title", label: "Title", control: Input }]
const canRenderControl = (key, dependsOn) => {
let test = !isEmpty(componentInstance[dependsOn])
@ -44,8 +43,8 @@
)
}
$: isPage = screenOrPageInstance && screenOrPageInstance.favicon
$: screenOrPageDefinition = isPage ? pageDefinition : screenDefinition
$: isLayout = assetInstance && assetInstance.favicon
$: assetDefinition = isLayout ? layoutDefinition : screenDefinition
const isDuplicateName = name => {
let duplicate = false
@ -58,15 +57,15 @@
}
})
}
// check page first
lookForDuplicate($store.pages[$store.currentPageName].props)
if (duplicate) return true
// check against layouts
for (let layout of $store.layouts) {
lookForDuplicate(layout.props)
}
// if viewing screen, check current screen for duplicate
if ($store.currentFrontEndType === "screen") {
lookForDuplicate($store.currentPreviewItem.props)
if ($store.currentFrontEndType === FrontendTypes.SCREEN) {
lookForDuplicate($currentAsset.props)
} else {
// viewing master page - need to dedupe against all screens
// need to dedupe against all screens
for (let screen of $allScreens) {
lookForDuplicate(screen.props)
}
@ -86,14 +85,14 @@
</script>
<div class="settings-view-container">
{#if screenOrPageInstance}
{#each screenOrPageDefinition as def}
{#if assetInstance}
{#each assetDefinition as def}
<PropertyControl
bindable={false}
control={def.control}
label={def.label}
key={def.key}
value={screenOrPageInstance[def.key]}
value={assetInstance[def.key]}
onChange={onScreenPropChange}
props={{ ...excludeProps(def, ['control', 'label']) }} />
{/each}

View File

@ -1,7 +1,7 @@
<script>
import { Button, Icon, DropdownMenu, Spacer, Heading } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store, backendUiStore } from "builderStore"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "../../builderStore/fetchBindableProperties"
const dispatch = createEventDispatcher()
@ -32,21 +32,24 @@
}, [])
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $store.currentPreviewItem,
screen: $currentAsset,
tables: $backendUiStore.tables,
})
$: links = bindableProperties
.filter(x => x.fieldSchema?.type === "link")
.map(property => ({
label: property.readableBinding,
fieldName: property.fieldSchema.name,
name: `all_${property.fieldSchema.tableId}`,
tableId: property.fieldSchema.tableId,
type: "link",
}))
.map(property => {
return {
providerId: property.instance._id,
label: property.readableBinding,
fieldName: property.fieldSchema.name,
name: `all_${property.fieldSchema.tableId}`,
tableId: property.fieldSchema.tableId,
type: "link",
}
})
</script>
<div

View File

@ -25,7 +25,6 @@ export const createProps = (componentDefinition, derivedFromProps) => {
_id: uuid(),
_component: componentDefinition._component,
_styles: { normal: {}, hover: {}, active: {}, selected: {} },
_code: "",
}
const errors = []
@ -96,6 +95,3 @@ const parsePropDef = propDef => {
return cloneDeep(propDef.default)
}
export const arrayElementComponentName = (parentComponentName, arrayPropName) =>
`${parentComponentName}:${arrayPropName}`

View File

@ -1,60 +0,0 @@
import { isPlainObject, isArray, cloneDeep } from "lodash/fp"
import { getExactComponent } from "./searchComponents"
export const rename = (pages, screens, oldname, newname) => {
pages = cloneDeep(pages)
screens = cloneDeep(screens)
const changedScreens = []
const existingWithNewName = getExactComponent(screens, newname, true)
if (existingWithNewName)
return {
components: screens,
pages,
error: "Component by that name already exists",
}
const traverseProps = props => {
let hasEdited = false
if (props._component && props._component === oldname) {
props._component = newname
hasEdited = true
}
for (let propName in props) {
const prop = props[propName]
if (isPlainObject(prop) && prop._component) {
hasEdited = traverseProps(prop) || hasEdited
}
if (isArray(prop)) {
for (let element of prop) {
hasEdited = traverseProps(element) || hasEdited
}
}
}
return hasEdited
}
for (let screen of screens) {
let hasEdited = false
if (screen.props.instanceName === oldname) {
screen.props.instanceName = newname
hasEdited = true
}
hasEdited = traverseProps(screen.props) || hasEdited
if (hasEdited && screen.props.instanceName !== newname)
changedScreens.push(screen.props.instanceName)
}
for (let pageName in pages) {
const page = pages[pageName]
if (page.appBody === oldname) {
page.appBody = newname
}
}
return { screens, pages, changedScreens }
}

View File

@ -310,15 +310,15 @@ export default {
},
{
label: "Link Color",
key: "color",
control: Input,
placeholder: "Link Color",
key: "linkColor",
control: Colorpicker,
defaultValue: "#000",
},
{
label: "Hover Color",
key: "linkHoverColor",
control: Input,
placeholder: "Hover Color",
control: Colorpicker,
defaultValue: "#222",
},
{
label: "Image Height",
@ -385,15 +385,15 @@ export default {
},
{
label: "Link Color",
key: "color",
control: Input,
placeholder: "Link Color",
key: "linkColor",
control: Colorpicker,
defaultValue: "#000",
},
{
label: "Hover Color",
key: "linkHoverColor",
control: Input,
placeholder: "Hover Color",
control: Colorpicker,
defaultValue: "#222",
},
{
label: "Card Width",
@ -427,6 +427,36 @@ export default {
],
},
},
{
_component: "@budibase/standard-components/cardstat",
name: "Stat",
description: "A card component for displaying numbers.",
icon: "ri-dual-sim-2-line",
children: [],
properties: {
design: { ...all },
settings: [
{
label: "Title",
key: "title",
control: Input,
placeholder: "Total Revenue",
},
{
label: "Value",
key: "value",
control: Input,
placeholder: "$1,981,983",
},
{
label: "Label",
key: "label",
control: Input,
placeholder: "Stripe",
},
],
},
},
],
},
{
@ -1167,7 +1197,7 @@ export default {
_component: "##builtin/screenslot",
name: "Screen Slot",
description:
"This component is a placeholder for the rendering of a screen within a page.",
"This component is a placeholder for the rendering of a screen within a layout.",
icon: "ri-crop-2-line",
properties: { design: { ...all } },
commonProps: {},
@ -1175,7 +1205,7 @@ export default {
},
{
name: "Nav Bar",
_component: "@budibase/standard-components/Navigation",
_component: "@budibase/standard-components/navigation",
description:
"A component for handling the navigation within your app.",
icon: "ri-navigation-line",
@ -1192,7 +1222,7 @@ export default {
"A component that automatically generates a login screen for your app.",
icon: "ri-login-box-line",
children: [],
showOnPages: ["unauthenticated"],
showOnAsset: ["login-screen"],
properties: {
design: { ...all },
settings: [

View File

@ -9,6 +9,16 @@ export const FIELDS = {
presence: false,
},
},
LONGFORM: {
name: "Long Form Text",
icon: "ri-file-text-line",
type: "longform",
constraints: {
type: "string",
length: {},
presence: false,
},
},
OPTIONS: {
name: "Options",
icon: "ri-list-check-2",

View File

@ -1,16 +1,20 @@
export const DEFAULT_PAGES_OBJECT = {
main: {
props: {
_component: "@budibase/standard-components/container",
},
_screens: {},
},
unauthenticated: {
props: {
_component: "@budibase/standard-components/container",
},
_screens: {},
},
componentLibraries: [],
stylesheets: [],
export const TableNames = {
USERS: "ta_users",
}
export const FrontendTypes = {
PAGE: "page",
SCREEN: "screen",
LAYOUT: "layout",
NONE: "none",
}
// fields on the user table that cannot be edited
export const UNEDITABLE_USER_FIELDS = ["email", "password", "roleId"]
export const LAYOUT_NAMES = {
MASTER: {
PRIVATE: "layout_private_master",
PUBLIC: "layout_private_master",
},
}

View File

@ -68,16 +68,11 @@
<div class="toprightnav">
<ThemeEditor />
<FeedbackNavLink />
<div class="topnavitemright">
<a target="_blank" href="https://docs.budibase.com">
<i class="ri-question-line" />
</a>
</div>
<div class="topnavitemright">
<a
target="_blank"
href="https://github.com/Budibase/budibase/discussions">
<i class="ri-discuss-line" />
<i class="ri-question-line" />
</a>
</div>
<SettingsLink />

View File

@ -1,4 +1,6 @@
<script>
import { store } from "builderStore"
import { params } from "@sveltech/routify"
store.actions.pages.select($params.page)
store.actions.layouts.select($params.layout)
</script>

View File

@ -2,7 +2,7 @@
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
</script>
<!-- routify:options index=1 -->
<!-- routify:options index=0 -->
<div class="root">
<div class="nav">
<TableNavigator />

View File

@ -0,0 +1,63 @@
<script>
import { params, leftover, goto } from "@sveltech/routify"
import { FrontendTypes } from "constants"
import { store, allScreens } from "builderStore"
// Get any leftover params not caught by Routifys params store.
const componentIds = $leftover.split("/").filter(id => id !== "")
const currentAssetId = decodeURI($params.asset)
let assetList
let actions
// Determine screens or layouts based on the URL
if ($params.assetType === FrontendTypes.SCREEN) {
assetList = $allScreens
actions = store.actions.screens
} else {
assetList = $store.layouts
actions = store.actions.layouts
}
// select the screen or layout in the UI
actions.select(currentAssetId)
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it.
if ($leftover) {
// Get the correct screen children.
const assetChildren = assetList.find(
asset =>
asset._id === $params.asset ||
asset._id === decodeURIComponent($params.asset)
).props._children
findComponent(componentIds, assetChildren)
}
// }
// Find Component with ID and continue
function findComponent(ids, children) {
// Setup stuff
let componentToSelect
let currentChildren = children
// Loop through each ID
ids.forEach(id => {
// Find ID
const component = currentChildren.find(child => child._id === id)
// If it does not exist, ignore (use last valid route)
if (!component) return
componentToSelect = component
// Update childrens array to selected components children
currentChildren = componentToSelect._children
})
// Select Component!
if (componentToSelect) store.actions.components.select(componentToSelect)
}
</script>
<slot />

View File

@ -1,6 +1,7 @@
<script>
import { store, backendUiStore } from "builderStore"
import { onMount } from "svelte"
import { FrontendTypes } from "constants"
import CurrentItemPreview from "components/userInterface/AppPreview"
import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte"
@ -26,8 +27,6 @@
const settings = () => {
settingsView.show()
}
const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script>
<!-- routify:options index=1 -->
@ -37,7 +36,7 @@
</div>
<div class="preview-pane">
{#if $store.currentPageName && $store.currentPageName.length > 0}
{#if $store.currentAssetId && $store.currentAssetId.length > 0}
<ComponentSelectionList />
<div class="preview-content">
<CurrentItemPreview />
@ -45,7 +44,7 @@
{/if}
</div>
{#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'}
{#if $store.currentFrontEndType === FrontendTypes.SCREEN || $store.currentFrontEndType === FrontendTypes.LAYOUT}
<div class="components-pane">
<ComponentPropertiesPanel />
</div>

View File

@ -0,0 +1,17 @@
<script>
import { store, allScreens } from "builderStore"
import { FrontendTypes } from "constants"
import { goto, params } from "@sveltech/routify"
// Go to first layout
if ($params.assetType === FrontendTypes.LAYOUT) {
$goto(`../${$store.layouts[0]?._id}`)
}
// Go to first screen
if ($params.assetType === FrontendTypes.SCREEN) {
$goto(`../${$allScreens[0]?._id}`)
}
</script>
<!-- routify:options index=false -->

View File

@ -1,69 +0,0 @@
<script>
import { onMount } from "svelte"
import { params, leftover, goto } from "@sveltech/routify"
import { store, allScreens } from "builderStore"
// Get any leftover params not caught by Routifys params store.
const componentIds = $leftover.split("/").filter(id => id !== "")
// It's a screen, set it to that screen
if ($params.screen !== "page-layout") {
const currentScreenName = decodeURI($params.screen)
const validScreen =
$allScreens.findIndex(screen => screen._id === currentScreenName) !== -1
if (!validScreen) {
// Go to main layout if URL set to invalid screen
store.actions.pages.select("main")
$goto("../../main")
} else {
// Otherwise proceed to set screen
store.actions.screens.select(currentScreenName)
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it.
if ($leftover) {
// Get the correct screen children.
const screenChildren = $store.pages[$params.page]._screens.find(
screen =>
screen._id === $params.screen ||
screen._id === decodeURIComponent($params.screen)
).props._children
findComponent(componentIds, screenChildren)
}
}
} else {
// It's a page, so set the screentype to page.
store.actions.selectPageOrScreen("page")
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it.
if ($leftover) {
findComponent(componentIds, $store.pages[$params.page].props._children)
}
}
// Find Component with ID and continue
function findComponent(ids, children) {
// Setup stuff
let componentToSelect
let currentChildren = children
// Loop through each ID
ids.forEach(id => {
// Find ID
const component = currentChildren.find(child => child._id === id)
// If it does not exist, ignore (use last valid route)
if (!component) return
componentToSelect = component
// Update childrens array to selected components children
currentChildren = componentToSelect._children
})
// Select Component!
if (componentToSelect) store.actions.components.select(componentToSelect)
}
</script>
<slot />

View File

@ -1,8 +0,0 @@
<script>
import { params } from "@sveltech/routify"
import { store } from "builderStore"
store.actions.pages.select($params.page)
</script>
<slot />

View File

@ -1,4 +0,0 @@
<script>
import { goto } from "@sveltech/routify"
$goto("../page-layout")
</script>

View File

@ -1,6 +1,8 @@
<script>
import { goto } from "@sveltech/routify"
$goto("../main")
import { FrontendTypes } from "constants"
$goto(`../${FrontendTypes.SCREEN}`)
</script>
<!-- routify:options index=false -->
<!-- routify:options index=1 -->

View File

@ -1,4 +1,4 @@
import { createProps } from "../src/components/userInterface/pagesParsing/createProps"
import { createProps } from "../src/components/userInterface/assetParsing/createProps"
import { keys, some } from "lodash/fp"
import { stripStandardProps } from "./testData"
@ -158,8 +158,6 @@ describe("createDefaultProps", () => {
const comp = getcomponent()
comp.props.fieldName = { type: "string", default: 1 }
const { props } = createProps(comp)
expect(props._code).toBeDefined()
expect(props._styles).toBeDefined()
expect(props._code).toBeDefined()
})
})

View File

@ -11,7 +11,7 @@ describe("fetch bindable properties", () => {
)
expect(componentBinding).toBeDefined()
expect(componentBinding.type).toBe("instance")
expect(componentBinding.runtimeBinding).toBe("search-input-id.value")
expect(componentBinding.runtimeBinding).toBe("search-input-id")
})
it("should not return bindable components when not in their context", () => {
@ -37,20 +37,22 @@ describe("fetch bindable properties", () => {
expect(contextBindings.length).toBe(4)
const namebinding = contextBindings.find(
b => b.runtimeBinding === "data.name"
b => b.runtimeBinding === "list-id.name"
)
expect(namebinding).toBeDefined()
expect(namebinding.readableBinding).toBe("list-name.Test Table.name")
const descriptionbinding = contextBindings.find(
b => b.runtimeBinding === "data.description"
b => b.runtimeBinding === "list-id.description"
)
expect(descriptionbinding).toBeDefined()
expect(descriptionbinding.readableBinding).toBe(
"list-name.Test Table.description"
)
const idbinding = contextBindings.find(b => b.runtimeBinding === "data._id")
const idbinding = contextBindings.find(
b => b.runtimeBinding === "list-id._id"
)
expect(idbinding).toBeDefined()
expect(idbinding.readableBinding).toBe("list-name.Test Table._id")
})
@ -65,13 +67,13 @@ describe("fetch bindable properties", () => {
expect(contextBindings.length).toBe(8)
const namebinding_parent = contextBindings.find(
b => b.runtimeBinding === "parent.data.name"
b => b.runtimeBinding === "list-id.name"
)
expect(namebinding_parent).toBeDefined()
expect(namebinding_parent.readableBinding).toBe("list-name.Test Table.name")
const descriptionbinding_parent = contextBindings.find(
b => b.runtimeBinding === "parent.data.description"
b => b.runtimeBinding === "list-id.description"
)
expect(descriptionbinding_parent).toBeDefined()
expect(descriptionbinding_parent.readableBinding).toBe(
@ -79,7 +81,7 @@ describe("fetch bindable properties", () => {
)
const namebinding_own = contextBindings.find(
b => b.runtimeBinding === "data.name"
b => b.runtimeBinding === "child-list-id.name"
)
expect(namebinding_own).toBeDefined()
expect(namebinding_own.readableBinding).toBe(
@ -87,7 +89,7 @@ describe("fetch bindable properties", () => {
)
const descriptionbinding_own = contextBindings.find(
b => b.runtimeBinding === "data.description"
b => b.runtimeBinding === "child-list-id.description"
)
expect(descriptionbinding_own).toBeDefined()
expect(descriptionbinding_own.readableBinding).toBe(
@ -104,7 +106,7 @@ describe("fetch bindable properties", () => {
r => r.instance._id === "list-item-input-id" && r.type === "instance"
)
expect(componentBinding).toBeDefined()
expect(componentBinding.runtimeBinding).toBe("list-item-input-id.value")
expect(componentBinding.runtimeBinding).toBe("list-item-input-id")
})
it("should not return components from child context", () => {

View File

@ -1,51 +0,0 @@
import {
generate_css,
generate_screen_css,
} from "../src/builderStore/generate_css.js"
describe("generate_css", () => {
test("Check how array styles are output", () => {
expect(generate_css({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0px 10px 0px 15px;")
})
test("Check handling of an array with empty string values", () => {
expect(generate_css({ padding: ["", "", "", ""] })).toBe("")
})
test("Check handling of an empty array", () => {
expect(generate_css({ margin: [] })).toBe("")
})
test("Check handling of valid font property", () => {
expect(generate_css({ "font-size": "10px" })).toBe("font-size: 10px;")
})
})
describe("generate_screen_css", () => {
const normalComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: { "font-size": "16px" }, hover: {}, active: {}, selected: {} } }
test("Test generation of normal css styles", () => {
expect(generate_screen_css([normalComponent])).toBe(".header-123-456 {\nfont-size: 16px;\n}")
})
const hoverComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {"font-size": "16px"}, active: {}, selected: {} } }
test("Test generation of hover css styles", () => {
expect(generate_screen_css([hoverComponent])).toBe(".header-123-456:hover {\nfont-size: 16px;\n}")
})
const selectedComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: { "font-size": "16px" } } }
test("Test generation of selection css styles", () => {
expect(generate_screen_css([selectedComponent])).toBe(".header-123-456::selection {\nfont-size: 16px;\n}")
})
const emptyComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: {} } }
test.only("Testing handling of empty component styles", () => {
expect(generate_screen_css([emptyComponent])).toBe("")
})
})

View File

@ -2,7 +2,7 @@ import {
searchAllComponents,
getExactComponent,
getAncestorProps,
} from "../src/components/userInterface/pagesParsing/searchComponents"
} from "../src/components/userInterface/assetParsing/searchComponents"
import { componentsAndScreens } from "./testData"

View File

@ -106,7 +106,6 @@ export const componentsAndScreens = () => ({
})
export const stripStandardProps = props => {
delete props._code
delete props._id
delete props._styles
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,5 @@
.DS_Store
node_modules
package-lock.json
yarn.lock
release/
dist/

View File

@ -1,13 +0,0 @@
module.exports = {
presets: ["@babel/preset-env"],
sourceMaps: "inline",
retainLines: true,
plugins: [
[
"@babel/plugin-transform-runtime",
{
regenerator: true,
},
],
],
}

View File

@ -3,59 +3,30 @@
"version": "0.3.8",
"license": "MPL-2.0",
"main": "dist/budibase-client.js",
"module": "dist/budibase-client.esm.mjs",
"module": "dist/budibase-client.js",
"scripts": {
"build": "rollup -c",
"test": "jest",
"dev:builder": "rollup -cw"
},
"jest": {
"globals": {
"GLOBALS": {
"client": "web"
}
},
"testURL": "http://test.com",
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js",
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
},
"moduleFileExtensions": [
"js",
"svelte"
],
"moduleDirectories": [
"node_modules"
],
"transform": {
"^.+js$": "babel-jest",
"^.+.svelte$": "svelte-jester"
},
"transformIgnorePatterns": [
"/node_modules/(?!svelte).+\\.js$"
]
},
"dependencies": {
"deep-equal": "^2.0.1",
"mustache": "^4.0.1",
"regexparam": "^1.3.0"
"regexparam": "^1.3.0",
"svelte-spa-router": "^3.0.5"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/runtime": "^7.5.5",
"babel-jest": "^24.8.0",
"@budibase/standard-components": "^0.3.8",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-node-resolve": "^10.0.0",
"fs-extra": "^8.1.0",
"jest": "^24.8.0",
"jsdom": "^16.0.1",
"rollup": "^1.12.0",
"rollup-plugin-commonjs": "^10.0.0",
"rollup": "^2.33.2",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-svelte": "^6.1.1",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^4.0.4",
"svelte": "^3.29.7",
"svelte": "^3.30.0",
"svelte-jester": "^1.0.6"
},
"gitHead": "e4e053cb6ff9a0ddc7115b44ccaa24b8ec41fb9a"

View File

@ -1,31 +1,30 @@
import resolve from "rollup-plugin-node-resolve"
import commonjs from "rollup-plugin-commonjs"
import commonjs from "@rollup/plugin-commonjs"
import resolve from "@rollup/plugin-node-resolve"
import builtins from "rollup-plugin-node-builtins"
import nodeglobals from "rollup-plugin-node-globals"
import svelte from "rollup-plugin-svelte"
const production = !process.env.ROLLUP_WATCH
export default {
input: "src/index.js",
output: [
{
sourcemap: true,
format: "iife",
name: "app",
file: `./dist/budibase-client.js`,
},
{
file: "dist/budibase-client.esm.mjs",
format: "esm",
sourcemap: "inline",
file: `./dist/budibase-client.js`,
},
],
plugins: [
svelte({
dev: !production,
}),
resolve({
preferBuiltins: true,
browser: true,
dedupe: ["svelte", "svelte/internal"],
}),
commonjs(),
builtins(),
nodeglobals(),
],
watch: {
clearScreen: false,

View File

@ -0,0 +1,96 @@
import { getAppId } from "../utils/getAppId"
/**
* API cache for cached request responses.
*/
let cache = {}
/**
* Makes a fully formatted URL based on the SDK configuration.
*/
const makeFullURL = path => {
return `/${path}`.replace("//", "/")
}
/**
* Handler for API errors.
*/
const handleError = error => {
return { error }
}
/**
* 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
let headers = {
Accept: "application/json",
"Content-Type": "application/json",
"x-budibase-app-id": getAppId(),
}
if (!window["##BUDIBASE_IN_BUILDER##"]) {
headers["x-budibase-type"] = "client"
}
const response = await fetch(url, {
method,
headers,
body: requestBody,
credentials: "same-origin",
})
switch (response.status) {
case 200:
return response.json()
case 404:
return handleError(`${url}: Not Found`)
case 400:
return handleError(`${url}: Bad Request`)
case 403:
return handleError(`${url}: Forbidden`)
default:
if (response.status >= 200 && response.status < 400) {
return response.json()
}
return handleError(`${url} - ${response.statusText}`)
}
} 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]
}
/**
* Constructs an API call function for a particular HTTP method.
*/
const requestApiCall = method => async params => {
const { url, cache = false } = params
const fullURL = makeFullURL(url)
const enrichedParams = { ...params, method, url: fullURL }
return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams)
}
export default {
post: requestApiCall("POST"),
get: requestApiCall("GET"),
patch: requestApiCall("PATCH"),
del: requestApiCall("DELETE"),
error: handleError,
}

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