merge
This commit is contained in:
commit
89615e58a7
|
@ -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).
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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") })
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -14,9 +14,6 @@ export class Component extends BaseStructure {
|
|||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
_code: "",
|
||||
className: "",
|
||||
onLoad: [],
|
||||
type: "",
|
||||
_instanceName: "",
|
||||
_children: [],
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)}>
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ export default function() {
|
|||
if (mousePosition > 0.4 && mousePosition < 0.8) {
|
||||
state.dropPosition = DropPosition.INSIDE
|
||||
}
|
||||
return
|
||||
return state
|
||||
}
|
||||
|
||||
// bottom half
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import FlatButton from "./FlatButton.svelte"
|
||||
|
||||
export let buttonProps = []
|
||||
export let isMultiSelect = false
|
||||
export let value = []
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 => {
|
||||
|
|
|
@ -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}
|
|
@ -1,3 +0,0 @@
|
|||
<script>
|
||||
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
|
||||
</script>
|
|
@ -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>
|
|
@ -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`}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}`
|
|
@ -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 }
|
||||
}
|
|
@ -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: [
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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 />
|
|
@ -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>
|
|
@ -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 -->
|
|
@ -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 />
|
|
@ -1,8 +0,0 @@
|
|||
<script>
|
||||
import { params } from "@sveltech/routify"
|
||||
import { store } from "builderStore"
|
||||
|
||||
store.actions.pages.select($params.page)
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -1,4 +0,0 @@
|
|||
<script>
|
||||
import { goto } from "@sveltech/routify"
|
||||
$goto("../page-layout")
|
||||
</script>
|
|
@ -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 -->
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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("")
|
||||
})
|
||||
})
|
|
@ -2,7 +2,7 @@ import {
|
|||
searchAllComponents,
|
||||
getExactComponent,
|
||||
getAncestorProps,
|
||||
} from "../src/components/userInterface/pagesParsing/searchComponents"
|
||||
} from "../src/components/userInterface/assetParsing/searchComponents"
|
||||
import { componentsAndScreens } from "./testData"
|
||||
|
||||
|
||||
|
|
|
@ -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
|
@ -1,6 +1,5 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
release/
|
||||
dist/
|
|
@ -1,13 +0,0 @@
|
|||
module.exports = {
|
||||
presets: ["@babel/preset-env"],
|
||||
sourceMaps: "inline",
|
||||
retainLines: true,
|
||||
plugins: [
|
||||
[
|
||||
"@babel/plugin-transform-runtime",
|
||||
{
|
||||
regenerator: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue