Merge pull request #869 from Budibase/feature/backend-routing

Feature/backend routing
This commit is contained in:
Martin McKeaveney 2020-11-20 11:52:00 +00:00 committed by GitHub
commit 1c0966ffc4
86 changed files with 1720 additions and 1937 deletions

View File

@ -7,17 +7,7 @@ context('Screen Tests', () => {
cy.navigateToFrontend()
})
it('Should successful create a screen', () => {
it('Should successfully create a screen', () => {
cy.createScreen("test Screen", "/test")
})
it('Should rename a screen', () => {
cy.get(".components-pane").within(() => {
cy.contains("Settings").click()
cy.get("input[name=_instanceName]").clear().type("About Us").blur()
})
cy.get('.nav-items-container').within(() => {
cy.contains("About Us").should('exist')
})
})
})

View File

@ -166,6 +166,6 @@ Cypress.Commands.add("createScreen", (screenName, route) => {
cy.contains("Create Screen").click()
})
cy.get(".nav-items-container").within(() => {
cy.contains(screenName).should("exist")
cy.contains(route).should("exist")
})
})

View File

@ -81,7 +81,8 @@
"shortid": "^2.2.15",
"svelte-loading-spinners": "^0.1.1",
"svelte-portal": "^0.1.0",
"yup": "^0.29.2"
"yup": "^0.29.2",
"uuid": "^8.3.1"
},
"devDependencies": {
"@babel/core": "^7.5.5",

View File

@ -31,6 +31,12 @@ export const currentScreens = derived(store, $store => {
: Object.values(currentScreens)
})
export const selectedPage = derived(store, $store => {
if (!$store.pages) return null
return $store.pages[$store.currentPageName || "main"]
})
export const initialise = async () => {
try {
await analytics.activate()

View File

@ -5,8 +5,7 @@ import {
getBuiltin,
makePropsSafe,
} from "components/userInterface/pagesParsing/createProps"
import { getExactComponent } from "components/userInterface/pagesParsing/searchComponents"
import { allScreens, backendUiStore } from "builderStore"
import { allScreens, backendUiStore, selectedPage } from "builderStore"
import { generate_screen_css } from "../generate_css"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
@ -37,6 +36,7 @@ const INITIAL_FRONTEND_STATE = {
hasAppPackage: false,
libraries: null,
appId: "",
routes: {},
}
export const getFrontendStore = () => {
@ -111,10 +111,9 @@ export const getFrontendStore = () => {
store.update(state => {
state.currentFrontEndType = type
const pageOrScreen =
type === "page"
? state.pages[state.currentPageName]
: state.pages[state.currentPageName]._screens[0]
const page = get(selectedPage)
const pageOrScreen = type === "page" ? page : page._screens[0]
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null
state.currentPreviewItem = pageOrScreen
@ -122,10 +121,21 @@ export const getFrontendStore = () => {
return state
})
},
screens: {
select: screenName => {
routing: {
fetch: async () => {
const response = await api.get("/api/routing")
const json = await response.json()
store.update(state => {
const screen = getExactComponent(get(allScreens), screenName, true)
state.routes = json.routes
return state
})
},
},
screens: {
select: screenId => {
store.update(state => {
const screen = get(allScreens).find(screen => screen._id === screenId)
state.currentPreviewItem = screen
state.currentFrontEndType = "screen"
state.currentView = "detail"
@ -158,32 +168,25 @@ export const getFrontendStore = () => {
await savePromise
},
save: async screen => {
const storeContents = get(store)
const pageName = storeContents.currentPageName || "main"
const currentPage = storeContents.pages[pageName]
const currentPageScreens = currentPage._screens
const page = get(selectedPage)
const currentPageScreens = page._screens
const creatingNewScreen = screen._id === undefined
let savePromise
const response = await api.post(
`/api/screens/${currentPage._id}`,
screen
)
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 = currentPageScreens.findIndex(
el => el._id === screen._id
)
const foundScreen = page._screens.findIndex(el => el._id === screen._id)
if (foundScreen !== -1) {
currentPageScreens.splice(foundScreen, 1)
page._screens.splice(foundScreen, 1)
}
currentPageScreens.push(screen)
page._screens.push(screen)
// TODO: should carry out all server updates to screen in a single call
store.update(state => {
state.pages[pageName]._screens = currentPageScreens
page._screens = currentPageScreens
if (creatingNewScreen) {
state.currentPreviewItem = screen
@ -209,21 +212,21 @@ export const getFrontendStore = () => {
store.actions.screens.regenerateCss(currentPreviewItem)
}
},
delete: async (screensToDelete, pageName) => {
delete: async screens => {
let deletePromise
const screensToDelete = Array.isArray(screens) ? screens : [screens]
store.update(state => {
if (pageName == null) {
pageName = state.pages.main.name
}
for (let screenToDelete of Array.isArray(screensToDelete)
? screensToDelete
: [screensToDelete]) {
const currentPage = get(selectedPage)
for (let screenToDelete of screensToDelete) {
// Remove screen from current page as well
// TODO: Should be done server side
state.pages[pageName]._screens = state.pages[
pageName
]._screens.filter(scr => scr._id !== screenToDelete._id)
currentPage._screens = currentPage._screens.filter(
scr => scr._id !== screenToDelete._id
)
deletePromise = api.delete(
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
)
@ -309,14 +312,13 @@ export const getFrontendStore = () => {
create: (componentToAdd, presetProps) => {
store.update(state => {
function findSlot(component_array) {
for (let i = 0; i < component_array.length; i += 1) {
if (component_array[i]._component === "##builtin/screenslot") {
for (let component of component_array) {
if (component._component === "##builtin/screenslot") {
return true
}
if (component_array[i]._children) findSlot(component_array[i])
if (component._children) findSlot(component)
}
return false
}
@ -377,7 +379,7 @@ export const getFrontendStore = () => {
component._id
)
parent._children = parent._children.filter(
c => c._id !== component._id
child => child._id !== component._id
)
store.actions.components.select(parent)
}

View File

@ -1,22 +1,13 @@
import { Screen } from "./utils/Screen"
export default {
name: `Create from scratch`,
create: () => createScreen(),
}
const createScreen = () => ({
props: {
_id: "",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
type: "div",
_children: [],
_instanceName: "",
},
route: "",
name: "screen-id",
})
const createScreen = () => {
return new Screen()
.mainType("div")
.component("@budibase/standard-components/container")
.json()
}

View File

@ -1,22 +1,13 @@
import { Screen } from "./utils/Screen"
export default {
name: `New Row (Empty)`,
create: () => createScreen(),
}
const createScreen = () => ({
props: {
_id: "",
_component: "@budibase/standard-components/newrow",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [],
_instanceName: "",
table: "",
},
route: "",
name: "screen-id",
})
const createScreen = () => {
return new Screen()
.component("@budibase/standard-components/newrow")
.table("")
.json()
}

View File

@ -1,22 +1,13 @@
import { Screen } from "./utils/Screen"
export default {
name: `Row Detail (Empty)`,
create: () => createScreen(),
}
const createScreen = () => ({
props: {
_id: "",
_component: "@budibase/standard-components/rowdetail",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [],
_instanceName: "",
table: "",
},
route: "",
name: "screen-id",
})
const createScreen = () => {
return new Screen()
.component("@budibase/standard-components/rowdetail")
.table("")
.json()
}

View File

@ -24,7 +24,7 @@ const createTemplateOverride = (frontendState, create) => () => {
}
screen.props._id = uuid()
screen.name = screen.props._id
screen.route = screen.route.toLowerCase()
screen.routing.route = screen.routing.route.toLowerCase()
return screen
}

View File

@ -1,5 +1,12 @@
import sanitizeUrl from "./sanitizeUrl"
import { rowListUrl } from "./rowListScreen"
import sanitizeUrl from "./utils/sanitizeUrl"
import { Component } from "./utils/Component"
import { Screen } from "./utils/Screen"
import {
makeBreadcrumbContainer,
makeMainContainer,
makeTitleContainer,
makeSaveButton,
} from "./utils/commonComponents"
export default function(tables) {
return tables.map(table => {
@ -14,242 +21,26 @@ export default function(tables) {
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new`)
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
const createScreen = table => ({
props: {
_id: "c683c4ca8ffc849c6bdd3b7d637fbbf3c",
_component: "@budibase/standard-components/newrow",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
table: table._id,
_children: [
{
_id: "ccad6cc135c7947a7ba9c631f655d6e0f",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
width: "700px",
padding: "0px",
background: "white",
"border-radius": "0.5rem",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
margin: "auto",
"margin-top": "20px",
"padding-top": "48px",
"padding-bottom": "48px",
"padding-right": "48px",
"padding-left": "48px",
"margin-bottom": "20px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Container",
_children: [
{
_id: "c6e91622ba7984f468f70bf4bf5120246",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
"font-size": "14px",
color: "#757575",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Breadcrumbs",
_children: [
{
_id: "caa33353c252c4931b2a51b48a559a7fc",
_component: "@budibase/standard-components/link",
_styles: {
normal: {
color: "#757575",
"text-transform": "capitalize",
},
hover: {
color: "#4285f4",
},
active: {},
selected: {},
},
_code: "",
url: `/${table.name.toLowerCase()}`,
openInNewTab: false,
text: table.name,
color: "",
hoverColor: "",
underline: false,
fontSize: "",
fontFamily: "initial",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Back Link",
_children: [],
},
{
_id: "c6e218170201040e7a74e2c8304fe1860",
_component: "@budibase/standard-components/text",
_styles: {
normal: {
"margin-right": "4px",
"margin-left": "4px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
text: ">",
type: "none",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Arrow",
_children: [],
},
{
_id: "c799da1fa3a84442e947cc9199518f64c",
_component: "@budibase/standard-components/text",
_styles: {
normal: {
color: "#000000",
},
hover: {},
active: {},
selected: {},
},
_code: "",
text: "New",
type: "none",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Identifier",
_children: [],
},
],
},
{
_id: "cbd1637cd1e274287a3c28ef0bf235d08",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
display: "flex",
"flex-direction": "row",
"justify-content": "space-between",
"align-items": "center",
"margin-top": "32px",
"margin-bottom": "32px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Title Container",
_children: [
{
_id: "c98d3675d04114558bbf28661c5ccfb8e",
_component: "@budibase/standard-components/heading",
_styles: {
normal: {
margin: "0px",
"margin-bottom": "0px",
"margin-right": "0px",
"margin-top": "0px",
"margin-left": "0px",
flex: "1 1 auto",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
text: "New Row",
type: "h3",
_instanceName: "Title",
_children: [],
},
{
_id: "cae402bd3c6a44618a8341bf7ab9ab086",
_component: "@budibase/standard-components/button",
_styles: {
normal: {
background: "#000000",
"border-width": "0",
"border-style": "None",
color: "#fff",
"font-family": "Inter",
"font-weight": "500",
"font-size": "14px",
"margin-left": "16px",
},
hover: {
background: "#4285f4",
},
active: {},
selected: {},
},
_code: "",
text: "Save",
className: "",
disabled: false,
onClick: [
{
parameters: {
contextPath: "data",
tableId: table._id,
},
"##eventHandlerType": "Save Row",
},
{
parameters: {
url: rowListUrl(table),
},
"##eventHandlerType": "Navigate To",
},
],
_instanceName: "Save Button",
_children: [],
},
],
},
{
_id: "c5e6c98d7363640f9ad3a7d19c8c10f67",
_component: "@budibase/standard-components/dataformwide",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Form",
_children: [],
},
],
},
],
_instanceName: `${table.name} - New`,
_code: "",
},
route: newRowUrl(table),
name: "",
})
function generateTitleContainer(table) {
return makeTitleContainer("New Row").addChild(makeSaveButton(table))
}
const createScreen = table => {
const dataform = new Component(
"@budibase/standard-components/dataformwide"
).instanceName("Form")
const container = makeMainContainer()
.addChild(makeBreadcrumbContainer(table.name, "New"))
.addChild(generateTitleContainer(table))
.addChild(dataform)
return new Screen()
.component("@budibase/standard-components/newrow")
.table(table._id)
.route(newRowUrl(table))
.instanceName(`${table.name} - New`)
.name("")
.addChild(container)
.json()
}

View File

@ -1,5 +1,13 @@
import sanitizeUrl from "./sanitizeUrl"
import sanitizeUrl from "./utils/sanitizeUrl"
import { rowListUrl } from "./rowListScreen"
import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component"
import {
makeMainContainer,
makeBreadcrumbContainer,
makeTitleContainer,
makeSaveButton,
} from "./utils/commonComponents"
export default function(tables) {
return tables.map(table => {
@ -17,288 +25,78 @@ export default function(tables) {
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`)
const createScreen = (table, heading) => ({
props: {
_id: "c683c4ca8ffc849c6bdd3b7d637fbbf3c",
_component: "@budibase/standard-components/rowdetail",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
table: table._id,
_children: [
{
_id: "ccad6cc135c7947a7ba9c631f655d6e0f",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
width: "700px",
padding: "0px",
background: "white",
"border-radius": "0.5rem",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
margin: "auto",
"margin-top": "20px",
"padding-top": "48px",
"padding-bottom": "48px",
"padding-right": "48px",
"padding-left": "48px",
"margin-bottom": "20px",
function generateTitleContainer(table, title) {
// have to override style for this, its missing margin
const saveButton = makeSaveButton(table).normalStyle({
background: "#000000",
"border-width": "0",
"border-style": "None",
color: "#fff",
"font-family": "Inter",
"font-weight": "500",
"font-size": "14px",
})
const deleteButton = new Component("@budibase/standard-components/button")
.normalStyle({
background: "transparent",
"border-width": "0",
"border-style": "None",
color: "#9e9e9e",
"font-family": "Inter",
"font-weight": "500",
"font-size": "14px",
"margin-right": "8px",
"margin-left": "16px",
})
.hoverStyle({
background: "transparent",
color: "#4285f4",
})
.text("Delete")
.customProps({
className: "",
disabled: false,
onClick: [
{
parameters: {
rowId: "{{ data._id }}",
revId: "{{ data._rev }}",
tableId: table._id,
},
hover: {},
active: {},
selected: {},
"##eventHandlerType": "Delete Row",
},
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Container",
_children: [
{
_id: "c6e91622ba7984f468f70bf4bf5120246",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
"font-size": "14px",
color: "#757575",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Breadcrumbs",
_children: [
{
_id: "caa33353c252c4931b2a51b48a559a7fc",
_component: "@budibase/standard-components/link",
_styles: {
normal: {
color: "#757575",
"text-transform": "capitalize",
},
hover: {
color: "#4285f4",
},
active: {},
selected: {},
},
_code: "",
url: `/${table.name.toLowerCase()}`,
openInNewTab: false,
text: table.name,
color: "",
hoverColor: "",
underline: false,
fontSize: "",
fontFamily: "initial",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Back Link",
_children: [],
},
{
_id: "c6e218170201040e7a74e2c8304fe1860",
_component: "@budibase/standard-components/text",
_styles: {
normal: {
"margin-right": "4px",
"margin-left": "4px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
text: ">",
type: "none",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Arrow",
_children: [],
},
{
_id: "c799da1fa3a84442e947cc9199518f64c",
_component: "@budibase/standard-components/text",
_styles: {
normal: {
color: "#000000",
"text-transform": "capitalize",
},
hover: {},
active: {},
selected: {},
},
_code: "",
text: heading || "Edit",
type: "none",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Identifier",
_children: [],
},
],
{
parameters: {
url: rowListUrl(table),
},
{
_id: "cbd1637cd1e274287a3c28ef0bf235d08",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
display: "flex",
"flex-direction": "row",
"justify-content": "space-between",
"align-items": "center",
"margin-top": "32px",
"margin-bottom": "32px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Title Container",
_children: [
{
_id: "c98d3675d04114558bbf28661c5ccfb8e",
_component: "@budibase/standard-components/heading",
_styles: {
normal: {
margin: "0px",
"margin-bottom": "0px",
"margin-right": "0px",
"margin-top": "0px",
"margin-left": "0px",
flex: "1 1 auto",
"text-transform": "capitalize",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
text: heading || "Edit Row",
type: "h3",
_instanceName: "Title",
_children: [],
},
{
_id: "c0a162cfb7d1c4bcfa8d24c290ccd1fd6",
_component: "@budibase/standard-components/button",
_styles: {
normal: {
background: "transparent",
"border-width": "0",
"border-style": "None",
color: "#9e9e9e",
"font-family": "Inter",
"font-weight": "500",
"font-size": "14px",
"margin-right": "8px",
"margin-left": "16px",
},
hover: {
background: "transparent",
color: "#4285f4",
},
active: {},
selected: {},
},
_code: "",
text: "Delete",
className: "",
disabled: false,
onClick: [
{
parameters: {
rowId: "{{ data._id }}",
revId: "{{ data._rev }}",
tableId: table._id,
},
"##eventHandlerType": "Delete Row",
},
{
parameters: {
url: rowListUrl(table),
},
"##eventHandlerType": "Navigate To",
},
],
_instanceName: "Delete Button",
_children: [],
},
{
_id: "cae402bd3c6a44618a8341bf7ab9ab086",
_component: "@budibase/standard-components/button",
_styles: {
normal: {
background: "#000000",
"border-width": "0",
"border-style": "None",
color: "#fff",
"font-family": "Inter",
"font-weight": "500",
"font-size": "14px",
},
hover: {
background: "#4285f4",
},
active: {},
selected: {},
},
_code: "",
text: "Save",
className: "",
disabled: false,
onClick: [
{
parameters: {
contextPath: "data",
tableId: table._id,
},
"##eventHandlerType": "Save Row",
},
{
parameters: {
url: rowListUrl(table),
},
"##eventHandlerType": "Navigate To",
},
],
_instanceName: "Save Button",
_children: [],
},
],
},
{
_id: "c5e6c98d7363640f9ad3a7d19c8c10f67",
_component: "@budibase/standard-components/dataformwide",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Form",
_children: [],
},
],
},
],
_instanceName: `${table.name} - Detail`,
_code: "",
},
route: rowDetailUrl(table),
name: "",
})
"##eventHandlerType": "Navigate To",
},
],
})
.instanceName("Delete Button")
return makeTitleContainer(title)
.addChild(deleteButton)
.addChild(saveButton)
}
const createScreen = (table, heading) => {
const dataform = new Component(
"@budibase/standard-components/dataformwide"
).instanceName("Form")
const container = makeMainContainer()
.addChild(makeBreadcrumbContainer(table.name, heading || "Edit"))
.addChild(generateTitleContainer(table, heading || "Edit Row"))
.addChild(dataform)
return new Screen()
.component("@budibase/standard-components/rowdetail")
.table(table._id)
.instanceName(`${table.name} - Detail`)
.route(rowDetailUrl(table))
.name("")
.addChild(container)
.json()
}

View File

@ -1,5 +1,7 @@
import sanitizeUrl from "./sanitizeUrl"
import sanitizeUrl from "./utils/sanitizeUrl"
import { newRowUrl } from "./newRowScreen"
import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component"
export default function(tables) {
return tables.map(table => {
@ -14,159 +16,103 @@ export default function(tables) {
export const ROW_LIST_TEMPLATE = "ROW_LIST_TEMPLATE"
export const rowListUrl = table => sanitizeUrl(`/${table.name}`)
const createScreen = table => ({
props: {
_id: "c7365379815e4457dbe703a886c2da43b",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
type: "div",
_children: [
{
_id: "cf51241fc063d4d87be032dd509fe0244",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
background: "white",
"border-radius": "0.5rem",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
margin: "auto",
"margin-top": "20px",
"border-width": "2px",
"border-color": "rgba(0, 0, 0, 0.1)",
"border-style": "None",
"padding-top": "48px",
"padding-bottom": "48px",
"padding-right": "48px",
"padding-left": "48px",
"margin-bottom": "20px",
function generateTitleContainer(table) {
const newButton = new Component("@budibase/standard-components/button")
.normalStyle({
background: "#000000",
"border-width": "0",
"border-style": "None",
color: "#fff",
"font-family": "Inter",
"font-weight": "500",
"font-size": "14px",
})
.hoverStyle({
background: "#4285f4",
})
.text("Create New")
.customProps({
className: "",
disabled: false,
onClick: [
{
parameters: {
url: newRowUrl(table),
},
hover: {},
active: {},
selected: {},
"##eventHandlerType": "Navigate To",
},
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Container",
_children: [
{
_id: "c73294c301fd145aabe9bbbbd96a150ac",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
display: "flex",
"flex-direction": "row",
"justify-content": "space-between",
"align-items": "center",
"margin-bottom": "32px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Title Container",
_children: [
{
_id: "c2b77901df95a4d1ca7204c58300bc94b",
_component: "@budibase/standard-components/heading",
_styles: {
normal: {
margin: "0px",
flex: "1 1 auto",
"text-transform": "capitalize",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
text: table.name,
type: "h3",
_instanceName: "Title",
_children: [],
},
{
_id: "c12a82d77baf24ca9922ea0af7cd4f723",
_component: "@budibase/standard-components/button",
_styles: {
normal: {
background: "#000000",
"border-width": "0",
"border-style": "None",
color: "#fff",
"font-family": "Inter",
"font-weight": "500",
"font-size": "14px",
},
hover: {
background: "#4285f4",
},
active: {},
selected: {},
},
_code: "",
text: "Create New",
className: "",
disabled: false,
onClick: [
{
parameters: {
url: newRowUrl(table),
},
"##eventHandlerType": "Navigate To",
},
],
_instanceName: "New Button",
_children: [],
},
],
},
{
_id: "ca686a2ed89c943e6bafb63fa66a3ead3",
_component: "@budibase/standard-components/datagrid",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
datasource: {
label: table.name,
name: `all_${table._id}`,
tableId: table._id,
type: "table",
},
editable: false,
theme: "alpine",
height: "540",
pagination: true,
_instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610",
_instanceName: "Grid",
_children: [],
detailUrl: `${table.name.toLowerCase()}/:id`,
},
],
],
})
.instanceName("New Button")
const heading = new Component("@budibase/standard-components/heading")
.normalStyle({
margin: "0px",
flex: "1 1 auto",
"text-transform": "capitalize",
})
.type("h3")
.instanceName("Title")
.text(table.name)
return new Component("@budibase/standard-components/container")
.type("div")
.normalStyle({
display: "flex",
"flex-direction": "row",
"justify-content": "space-between",
"align-items": "center",
"margin-bottom": "32px",
})
.instanceName("Title Container")
.addChild(heading)
.addChild(newButton)
}
const createScreen = table => {
const datagrid = new Component("@budibase/standard-components/datagrid")
.customProps({
datasource: {
label: table.name,
name: `all_${table._id}`,
tableId: table._id,
type: "table",
},
],
_instanceName: `${table.name} - List`,
_code: "",
className: "",
onLoad: [],
},
route: rowListUrl(table),
name: "",
})
editable: false,
theme: "alpine",
height: "540",
pagination: true,
detailUrl: `${table.name.toLowerCase()}/:id`,
})
.instanceName("Grid")
const mainContainer = new Component("@budibase/standard-components/container")
.normalStyle({
background: "white",
"border-radius": "0.5rem",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
margin: "auto",
"margin-top": "20px",
"border-width": "2px",
"border-color": "rgba(0, 0, 0, 0.1)",
"border-style": "None",
"padding-top": "48px",
"padding-bottom": "48px",
"padding-right": "48px",
"padding-left": "48px",
"margin-bottom": "20px",
})
.type("div")
.instanceName("Container")
.addChild(generateTitleContainer(table))
.addChild(datagrid)
return new Screen()
.component("@budibase/standard-components/container")
.mainType("div")
.route(rowListUrl(table))
.instanceName(`${table.name} - List`)
.name("")
.addChild(mainContainer)
.json()
}

View File

@ -0,0 +1,35 @@
import { cloneDeep } from "lodash/fp"
export class BaseStructure {
constructor(isScreen) {
this._isScreen = isScreen
this._children = []
this._json = {}
}
addChild(child) {
this._children.push(child)
return this
}
customProps(props) {
for (let key of Object.keys(props)) {
this._json[key] = props[key]
}
return this
}
json() {
const structure = cloneDeep(this._json)
if (this._children.length !== 0) {
for (let child of this._children) {
if (this._isScreen) {
structure.props._children.push(child.json())
} else {
structure._children.push(child.json())
}
}
}
return structure
}
}

View File

@ -0,0 +1,51 @@
import { v4 } from "uuid"
import { BaseStructure } from "./BaseStructure"
export class Component extends BaseStructure {
constructor(name) {
super(false)
this._children = []
this._json = {
_id: v4(),
_component: name,
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "",
_instanceName: "",
_children: [],
}
}
type(type) {
this._json.type = type
return this
}
normalStyle(styling) {
this._json._styles.normal = styling
return this
}
hoverStyle(styling) {
this._json._styles.hover = styling
return this
}
text(text) {
this._json.text = text
return this
}
// TODO: do we need this
instanceName(name) {
this._json._instanceName = name
return this
}
}

View File

@ -0,0 +1,61 @@
import { BaseStructure } from "./BaseStructure"
export class Screen extends BaseStructure {
constructor() {
super(true)
this._json = {
props: {
_id: "",
_component: "",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [],
_instanceName: "",
},
routing: {
route: "",
accessLevelId: "",
},
name: "screen-id",
}
}
normalStyle(styling) {
this._json.props._styles.normal = styling
return this
}
component(name) {
this._json.props._component = name
return this
}
table(tableName) {
this._json.props.table = tableName
return this
}
mainType(type) {
this._json.type = type
return this
}
route(route) {
this._json.routing.route = route
return this
}
name(name) {
this._json.name = name
return this
}
instanceName(name) {
this._json.props._instanceName = name
return this
}
}

View File

@ -0,0 +1,145 @@
import { Component } from "./Component"
import { rowListUrl } from "../rowListScreen"
export function makeLinkComponent(tableName) {
return new Component("@budibase/standard-components/link")
.normalStyle({
color: "#757575",
"text-transform": "capitalize",
})
.hoverStyle({
color: "#4285f4",
})
.text(tableName)
.customProps({
url: `/${tableName.toLowerCase()}`,
openInNewTab: false,
color: "",
hoverColor: "",
underline: false,
fontSize: "",
fontFamily: "initial",
})
}
export function makeMainContainer() {
return new Component("@budibase/standard-components/container")
.type("div")
.normalStyle({
width: "700px",
padding: "0px",
background: "white",
"border-radius": "0.5rem",
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
margin: "auto",
"margin-top": "20px",
"padding-top": "48px",
"padding-bottom": "48px",
"padding-right": "48px",
"padding-left": "48px",
"margin-bottom": "20px",
})
.instanceName("Container")
}
export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
const link = makeLinkComponent(tableName).instanceName("Back Link")
const arrowText = new Component("@budibase/standard-components/text")
.type("none")
.normalStyle({
"margin-right": "4px",
"margin-left": "4px",
})
.text(">")
.instanceName("Arrow")
const textStyling = {
color: "#000000",
}
if (capitalise) {
textStyling["text-transform"] = "capitalize"
}
const identifierText = new Component("@budibase/standard-components/text")
.type("none")
.normalStyle(textStyling)
.text(text)
.instanceName("Identifier")
return new Component("@budibase/standard-components/container")
.type("div")
.normalStyle({
"font-size": "14px",
color: "#757575",
})
.instanceName("Breadcrumbs")
.addChild(link)
.addChild(arrowText)
.addChild(identifierText)
}
export function makeSaveButton(table) {
return new Component("@budibase/standard-components/button")
.normalStyle({
background: "#000000",
"border-width": "0",
"border-style": "None",
color: "#fff",
"font-family": "Inter",
"font-weight": "500",
"font-size": "14px",
"margin-left": "16px",
})
.hoverStyle({
background: "#4285f4",
})
.text("Save")
.customProps({
className: "",
disabled: false,
onClick: [
{
parameters: {
contextPath: "data",
tableId: table._id,
},
"##eventHandlerType": "Save Row",
},
{
parameters: {
url: rowListUrl(table),
},
"##eventHandlerType": "Navigate To",
},
],
})
.instanceName("Save Button")
}
export function makeTitleContainer(title) {
const heading = new Component("@budibase/standard-components/heading")
.normalStyle({
margin: "0px",
"margin-bottom": "0px",
"margin-right": "0px",
"margin-top": "0px",
"margin-left": "0px",
flex: "1 1 auto",
})
.type("h3")
.instanceName("Title")
.text(title)
return new Component("@budibase/standard-components/container")
.type("div")
.normalStyle({
display: "flex",
"flex-direction": "row",
"justify-content": "space-between",
"align-items": "center",
"margin-top": "32px",
"margin-bottom": "32px",
})
.instanceName("Title Container")
.addChild(heading)
}

View File

@ -59,10 +59,13 @@
}
// Create autolink to newly created list page
const listPage = screens.find(screen =>
const listScreen = screens.find(screen =>
screen.props._instanceName.endsWith("List")
)
await store.actions.components.links.save(listPage.route, table.name)
await store.actions.components.links.save(
listScreen.routing.route,
table.name
)
// Navigate to new table
$goto(`./table/${table._id}`)

View File

@ -9,7 +9,7 @@
async function deleteApp() {
loading = true
const id = $params.application
await del(`/api/${id}`)
await del(`/api/applications/${id}`)
loading = false
$goto("/")
}

View File

@ -149,7 +149,9 @@
})
// Select Correct Application/DB in prep for creating user
const applicationPkg = await get(`/api/${appJson._id}/appPackage`)
const applicationPkg = await get(
`/api/applications/${appJson._id}/appPackage`
)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
backendUiStore.actions.reset()

View File

@ -3,6 +3,8 @@
import { map, join } from "lodash/fp"
import iframeTemplate from "./iframeTemplate"
import { pipe } from "../../../helpers"
import { Screen } from "../../../builderStore/store/screenTemplates/utils/Screen"
import { Component } from "../../../builderStore/store/screenTemplates/utils/Component"
let iframe
let styles = ""
@ -21,114 +23,52 @@
return componentName || "element"
}
const screenPlaceholder = {
name: "Screen Placeholder",
route: "*",
props: {
_id: "screenslot-placeholder",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
flex: "1 1 auto",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_children: [
{
_id: "51a1b494-0fa4-49c3-90cc-c2a6c7a3f888",
_component: "@budibase/standard-components/container",
_styles: {
normal: {
display: "flex",
"flex-direction": "column",
"align-items": "center",
flex: "1 1 auto",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
onLoad: [],
type: "div",
_instanceId: "inst_40d9036_4c81114e2bf145ab8721978c66e09a10",
_instanceName: "Container",
_children: [
{
_id: "90a52cd0-f215-46c1-b29b-e28f9e7edf72",
_component: "@budibase/standard-components/heading",
_styles: {
normal: {
width: "500px",
padding: "8px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
className: "",
text: "Screen Slot",
type: "h1",
_instanceId: "inst_40d9036_4c81114e2bf145ab8721978c66e09a10",
_instanceName: "Heading",
_children: [],
},
{
_id: "71a3da65-72c6-4c43-8c6a-49871c07b77d",
_component: "@budibase/standard-components/text",
_styles: {
normal: {
"max-width": "",
"text-align": "left",
width: "500px",
padding: "8px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
text:
"The screens that you create will be displayed inside this box.",
type: "none",
_instanceId: "inst_40d9036_4c81114e2bf145ab8721978c66e09a10",
_instanceName: "Text",
},
{
_id: "8af80374-460d-497b-a5d8-7dd2ec4a7bbc",
_component: "@budibase/standard-components/text",
_styles: {
normal: {
"max-width": "",
"text-align": "left",
width: "500px",
padding: "8px",
},
hover: {},
active: {},
selected: {},
},
_code: "",
text:
"This box is just a placeholder, to show you the position of screens.",
type: "none",
_instanceId: "inst_40d9036_4c81114e2bf145ab8721978c66e09a10",
_instanceName: "Text",
},
],
},
],
_instanceName: "Content Placeholder",
},
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)
const screenPlaceholder = new Screen()
.name("Screen Placeholder")
.route("*")
.component("@budibase/standard-components/container")
.mainType("div")
.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

View File

@ -0,0 +1,113 @@
<script>
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils"
import { DropEffect, DropPosition } from "./dragDropStore"
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
import NavItem from "components/common/NavItem.svelte"
export let components = []
export let currentComponent
export let onSelect = () => {}
export let level = 0
export let dragDropStore
const isScreenslot = name => name === "##builtin/screenslot"
const selectComponent = component => {
// Set current component
store.actions.components.select(component)
// Get ID path
const path = store.actions.components.findRoute(component)
// Go to correct URL
$goto(`./:page/:screen/${path}`)
}
const dragstart = component => e => {
e.dataTransfer.dropEffect = DropEffect.MOVE
dragDropStore.actions.dragstart(component)
}
const dragover = (component, index) => e => {
const canHaveChildrenButIsEmpty =
getComponentDefinition($store, component._component).children &&
component._children.length === 0
e.dataTransfer.dropEffect = DropEffect.COPY
// how far down the mouse pointer is on the drop target
const mousePosition = e.offsetY / e.currentTarget.offsetHeight
dragDropStore.actions.dragover({
component,
index,
canHaveChildrenButIsEmpty,
mousePosition,
})
return false
}
</script>
<ul>
{#each components as component, index (component._id)}
<li on:click|stopPropagation={() => selectComponent(component)}>
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
<div
on:drop={dragDropStore.actions.drop}
ondragover="return false"
ondragenter="return false"
class="drop-item"
style="margin-left: {(level + 1) * 16}px" />
{/if}
<NavItem
draggable
on:dragend={dragDropStore.actions.reset}
on:dragstart={dragstart(component)}
on:dragover={dragover(component, index)}
on:drop={dragDropStore.actions.drop}
text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
withArrow
indentLevel={level + 3}
selected={currentComponent === component}>
<ComponentDropdownMenu {component} />
</NavItem>
{#if component._children}
<svelte:self
components={component._children}
{currentComponent}
{onSelect}
{dragDropStore}
level={level + 1} />
{/if}
{#if $dragDropStore?.targetComponent === component && ($dragDropStore.dropPosition === DropPosition.INSIDE || $dragDropStore.dropPosition === DropPosition.BELOW)}
<div
on:drop={dragDropStore.actions.drop}
ondragover="return false"
ondragenter="return false"
class="drop-item"
style="margin-left: {(level + ($dragDropStore.dropPosition === DropPosition.INSIDE ? 3 : 1)) * 16}px" />
{/if}
</li>
{/each}
</ul>
<style>
ul {
list-style: none;
padding-left: 0;
margin: 0;
}
.drop-item {
border-radius: var(--border-radius-m);
height: 32px;
background: var(--grey-3);
}
</style>

View File

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

View File

@ -1,6 +1,6 @@
<script>
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { store, allScreens } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { DropdownMenu } from "@budibase/bbui"
@ -13,12 +13,14 @@
let anchor
const deleteScreen = () => {
store.actions.screens.delete(screen, $store.currentPageName)
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.name === screen.name) {
if (state.currentPreviewItem._id === screen) {
store.actions.pages.select($store.currentPageName)
notifier.success(`Screen ${screen.name} deleted successfully.`)
notifier.success(`Screen ${screenToDelete.name} deleted successfully.`)
$goto(`./:page/page-layout`)
}
return state
@ -42,7 +44,7 @@
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Deletion"
body={`Are you sure you wish to delete the screen '${screen.props._instanceName}' ?`}
body={'Are you sure you wish to delete this screen?'}
okText="Delete Screen"
onOk={deleteScreen} />

View File

@ -0,0 +1,92 @@
import { writable } from "svelte/store"
import { store as frontendStore } from "builderStore"
export const DropEffect = {
MOVE: "move",
COPY: "copy",
}
export const DropPosition = {
ABOVE: "above",
BELOW: "below",
INSIDE: "inside",
}
export default function() {
const store = writable({})
store.actions = {
dragstart: component => {
store.update(state => {
state.dragged = component
return state
})
},
dragover: ({
component,
index,
canHaveChildrenButIsEmpty,
mousePosition,
}) => {
store.update(state => {
state.targetComponent = component
// only allow dropping inside when container is empty
// if container has children, drag over them
if (canHaveChildrenButIsEmpty && index === 0) {
// hovered above center of target
if (mousePosition < 0.4) {
state.dropPosition = DropPosition.ABOVE
}
// hovered around bottom of target
if (mousePosition > 0.8) {
state.dropPosition = DropPosition.BELOW
}
// hovered in center of target
if (mousePosition > 0.4 && mousePosition < 0.8) {
state.dropPosition = DropPosition.INSIDE
}
return
}
// bottom half
if (mousePosition > 0.5) {
state.dropPosition = DropPosition.BELOW
} else {
state.dropPosition = canHaveChildrenButIsEmpty
? DropPosition.INSIDE
: DropPosition.ABOVE
}
return state
})
},
reset: () => {
store.update(state => {
state.dropPosition = ""
state.targetComponent = null
state.dragged = null
return state
})
},
drop: () => {
store.update(state => {
if (state.targetComponent !== state.dragged) {
frontendStore.actions.components.copy(state.dragged, true)
frontendStore.actions.components.paste(
state.targetComponent,
state.dropPosition
)
}
store.actions.reset()
return state
})
},
}
return store
}

View File

@ -0,0 +1,11 @@
<script>
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import PathTree from "./PathTree.svelte"
</script>
<div class="root">
{#each Object.keys($store.routes) as path}
<PathTree {path} route={$store.routes[path]} />
{/each}
</div>

View File

@ -1,61 +0,0 @@
<script>
import { goto } from "@sveltech/routify"
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import { trimCharsStart, trimChars } from "lodash/fp"
import { pipe } from "../../helpers"
import { store } from "builderStore"
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
import { writable } from "svelte/store"
import NavItem from "components/common/NavItem.svelte"
export let screens = []
$: sortedScreens = screens.sort((s1, s2) => {
const name1 = s1.props._instanceName?.toLowerCase() ?? ""
const name2 = s2.props._instanceName?.toLowerCase() ?? ""
return name1 > name2 ? 1 : -1
})
/*
Using a store here seems odd....
have a look in the <ComponentsHierarchyChildren /> code file to find out why.
I have commented the dragDropStore parameter
*/
const dragDropStore = writable({})
let confirmDeleteDialog
let componentToDelete = ""
const normalizedName = name =>
pipe(name, [
trimCharsStart("./"),
trimCharsStart("~/"),
trimCharsStart("../"),
trimChars(" "),
])
const changeScreen = screen => {
store.actions.screens.select(screen.props._instanceName)
$goto(`./:page/${screen.props._instanceName}`)
}
</script>
<div class="root">
{#each sortedScreens as screen}
<NavItem
icon="ri-artboard-2-line"
text={screen.props._instanceName}
withArrow={screen.props._children.length}
selected={$store.currentComponentInfo._id === screen.props._id}
opened={$store.currentPreviewItem.name === screen.props._id}
on:click={() => changeScreen(screen)}>
<ScreenDropdownMenu {screen} />
</NavItem>
{#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children}
<ComponentsHierarchyChildren
components={screen.props._children}
currentComponent={$store.currentComponentInfo}
{dragDropStore} />
{/if}
{/each}
</div>

View File

@ -1,181 +0,0 @@
<script>
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { last } from "lodash/fp"
import { pipe } from "../../helpers"
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import NavItem from "components/common/NavItem.svelte"
import { getComponentDefinition } from "builderStore/storeUtils"
export let components = []
export let currentComponent
export let onSelect = () => {}
export let level = 0
/*
"dragDropStore" is a svelte store.
This component is recursive... a tree.
Using a single, shared store, all the nodes in the tree can subscribe to state that is changed by other nodes, in the same tree.
e.g. Say i have the structure
- Heading 1
- Container
- Heading 2
- Heading 3
- Heading 4
1. When I dragover "Heading 1", a placeholder drop-slot appears below it
2. I drag down a bit so the cursor is INSIDE the container (i.e. now in a child <ComponentsHierarchyChildren />)
3. Using store subscribes... the original drop-slot now knows that it should disappear, and a new one is created inside the container.
*/
export let dragDropStore
let dropUnderComponent
let componentToDrop
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
const get_name = s => (!s ? "" : last(s.split("/")))
const get_capitalised_name = name => pipe(name, [get_name, capitalise])
const isScreenslot = name => name === "##builtin/screenslot"
const selectComponent = component => {
// Set current component
store.actions.components.select(component)
// Get ID path
const path = store.actions.components.findRoute(component)
// Go to correct URL
$goto(`./:page/:screen/${path}`)
}
const dragstart = component => e => {
e.dataTransfer.dropEffect = "move"
dragDropStore.update(s => {
s.componentToDrop = component
return s
})
}
const dragover = (component, index) => e => {
const canHaveChildrenButIsEmpty =
getComponentDefinition($store, component._component).children &&
component._children.length === 0
e.dataTransfer.dropEffect = "copy"
dragDropStore.update(s => {
const isBottomHalf = e.offsetY > e.currentTarget.offsetHeight / 2
s.targetComponent = component
// only allow dropping inside when container type
// is empty. If it has children, the user can drag over
// it's existing children
if (canHaveChildrenButIsEmpty) {
if (index === 0) {
// when its the first component in the screen,
// we divide into 3, so we can paste above, inside or below
const pos = e.offsetY / e.currentTarget.offsetHeight
if (pos < 0.4) {
s.dropPosition = "above"
} else if (pos > 0.8) {
// purposely giving this the least space as it is often covered
// by the component below's "above" space
s.dropPosition = "below"
} else {
s.dropPosition = "inside"
}
} else {
s.dropPosition = isBottomHalf ? "below" : "inside"
}
} else {
s.dropPosition = isBottomHalf ? "below" : "above"
}
return s
})
return false
}
const drop = () => {
if ($dragDropStore.targetComponent !== $dragDropStore.componentToDrop) {
store.actions.components.copy($dragDropStore.componentToDrop, true)
store.actions.components.paste(
$dragDropStore.targetComponent,
$dragDropStore.dropPosition
)
}
dragDropStore.update(s => {
s.dropPosition = ""
s.targetComponent = null
s.componentToDrop = null
return s
})
}
const dragend = () => {
dragDropStore.update(s => {
s.dropPosition = ""
s.targetComponent = null
s.componentToDrop = null
return s
})
}
</script>
<ul>
{#each components as component, index (component._id)}
<li on:click|stopPropagation={() => selectComponent(component)}>
{#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition === 'above'}
<div
on:drop={drop}
ondragover="return false"
ondragenter="return false"
class="drop-item"
style="margin-left: {(level + 1) * 16}px" />
{/if}
<NavItem
draggable
on:dragend={dragend}
on:dragstart={dragstart(component)}
on:dragover={dragover(component, index)}
on:drop={drop}
text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
withArrow
indentLevel={level + 1}
selected={currentComponent === component}>
<ComponentDropdownMenu {component} />
</NavItem>
{#if component._children}
<svelte:self
components={component._children}
{currentComponent}
{onSelect}
{dragDropStore}
level={level + 1} />
{/if}
{#if $dragDropStore && $dragDropStore.targetComponent === component && ($dragDropStore.dropPosition === 'inside' || $dragDropStore.dropPosition === 'below')}
<div
on:drop={drop}
ondragover="return false"
ondragenter="return false"
class="drop-item"
style="margin-left: {(level + ($dragDropStore.dropPosition === 'inside' ? 3 : 1)) * 16}px" />
{/if}
</li>
{/each}
</ul>
<style>
ul {
list-style: none;
padding-left: 0;
margin: 0;
}
.drop-item {
border-radius: var(--border-radius-m);
height: 32px;
background: var(--grey-3);
}
</style>

View File

@ -17,11 +17,11 @@
.filter(
screen =>
screen.props._component.endsWith("/rowdetail") ||
screen.route.endsWith(":id")
screen.routing.route.endsWith(":id")
)
.map(screen => ({
name: screen.props._instanceName,
url: screen.route,
url: screen.routing.route,
sort: screen.props._component,
})),
]

View File

@ -25,7 +25,9 @@
<DataList on:change bind:value={parameter.value}>
<option value="" />
{#each $allScreens as screen}
<option value={screen.route}>{screen.props._instanceName}</option>
<option value={screen.routing.route}>
{screen.props._instanceName}
</option>
{/each}
</DataList>
{:else}

View File

@ -10,7 +10,7 @@
<DataList secondary bind:value={parameters.url}>
<option value="" />
{#each $allScreens as screen}
<option value={screen.route}>{screen.props._instanceName}</option>
<option value={screen.routing.route}>{screen.props._instanceName}</option>
{/each}
</DataList>
</div>

View File

@ -1,12 +1,20 @@
<script>
import { onMount } from "svelte"
import { store, currentScreens } from "builderStore"
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte"
import api from "builderStore/api"
import ComponentNavigationTree from "components/userInterface/ComponentNavigationTree/index.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
import { Modal } from "@budibase/bbui"
let modal
let routes = {}
onMount(() => {
store.actions.routing.fetch()
})
</script>
<div class="title">
@ -16,7 +24,7 @@
<PagesList />
<div class="nav-items-container">
<PageLayout layout={$store.pages[$store.currentPageName]} />
<ComponentsHierarchy screens={$currentScreens} />
<ComponentNavigationTree />
</div>
<Modal bind:this={modal}>
<NewScreenModal />

View File

@ -49,8 +49,8 @@
baseComponent = draftScreen.props._component
}
if (draftScreen.route) {
route = draftScreen.route
if (draftScreen.routing) {
route = draftScreen.routing.route
}
}
@ -69,12 +69,14 @@
draftScreen.props._instanceName = name
draftScreen.props._component = baseComponent
draftScreen.route = route
// TODO: need to fix this up correctly
draftScreen.routing = { route, accessLevelId: "ADMIN" }
await store.actions.screens.create(draftScreen)
if (createLink) {
await store.actions.components.links.save(route, name)
}
await store.actions.routing.fetch()
if (templateIndex !== undefined) {
const template = templates[templateIndex]
@ -88,7 +90,7 @@
const routeNameExists = route => {
return $allScreens.some(
screen => screen.route.toLowerCase() === route.toLowerCase()
screen => screen.routing.route.toLowerCase() === route.toLowerCase()
)
}

View File

@ -1,6 +1,6 @@
<script>
import { goto } from "@sveltech/routify"
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import ComponentTree from "./ComponentNavigationTree/ComponentTree.svelte"
import NavItem from "components/common/NavItem.svelte"
import { last } from "lodash/fp"
import { store } from "builderStore"
@ -37,8 +37,7 @@
on:click={setCurrentScreenToLayout} />
{#if $store.currentPreviewItem?.name === _layout.title && _layout.component.props._children}
<ComponentsHierarchyChildren
thisComponent={_layout.component.props}
<ComponentTree
components={_layout.component.props._children}
currentComponent={$store.currentComponentInfo}
{dragDropStore} />

View File

@ -21,7 +21,7 @@
.filter(screen => !screen.props._component.endsWith("/rowdetail"))
.map(screen => ({
name: screen.props._instanceName,
url: screen.route,
url: screen.routing.route,
sort: screen.props._component,
})),
]
@ -54,7 +54,7 @@
if (idBinding) {
urls.push({
name: detailScreen.props._instanceName,
url: detailScreen.route.replace(
url: detailScreen.routing.route.replace(
":id",
`{{ ${idBinding.runtimeBinding} }}`
),

View File

@ -13,7 +13,7 @@
let promise = getPackage()
async function getPackage() {
const res = await get(`/api/${application}/appPackage`)
const res = await get(`/api/applications/${application}/appPackage`)
const pkg = await res.json()
if (res.ok) {

View File

@ -10,9 +10,7 @@
if ($params.screen !== "page-layout") {
const currentScreenName = decodeURI($params.screen)
const validScreen =
$allScreens.findIndex(
screen => screen.props._instanceName === currentScreenName
) !== -1
$allScreens.findIndex(screen => screen._id === currentScreenName) !== -1
if (!validScreen) {
// Go to main layout if URL set to invalid screen
@ -27,8 +25,8 @@
// Get the correct screen children.
const screenChildren = $store.pages[$params.page]._screens.find(
screen =>
screen.props._instanceName === $params.screen ||
screen.props._instanceName === decodeURIComponent($params.screen)
screen._id === $params.screen ||
screen._id === decodeURIComponent($params.screen)
).props._children
findComponent(componentIds, screenChildren)
}

View File

@ -719,15 +719,6 @@
svelte-flatpickr "^2.4.0"
svelte-portal "^1.0.0"
"@budibase/client@^0.3.7":
version "0.3.7"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.3.7.tgz#8ed2d40d91ba3788a69ee5db5078f757adb4187f"
integrity sha512-EgpHfw/WOUYeCG4cILDbaN2WFBDSPS698Z+So7FP5l+4E1fvmqtpXVKJYsviwYEx8AKKYyU3nuDi0l6xzb5Flg==
dependencies:
deep-equal "^2.0.1"
mustache "^4.0.1"
regexparam "^1.3.0"
"@budibase/colorpicker@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@budibase/colorpicker/-/colorpicker-1.0.1.tgz#940c180e7ebba0cb0756c4c8ef13f5dfab58e810"
@ -1416,11 +1407,6 @@ array-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
array-filter@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
@ -1475,13 +1461,6 @@ atob@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==
dependencies:
array-filter "^1.0.0"
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@ -1753,14 +1732,6 @@ cachedir@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"
call-bind@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce"
integrity sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==
dependencies:
function-bind "^1.1.1"
get-intrinsic "^1.0.0"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@ -2456,26 +2427,6 @@ deep-equal@^1.0.1:
object-keys "^1.1.1"
regexp.prototype.flags "^1.2.0"
deep-equal@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.4.tgz#6b0b407a074666033169df3acaf128e1c6f3eab6"
integrity sha512-BUfaXrVoCfgkOQY/b09QdO9L3XNoF2XH0A3aY9IQwQL/ZjLOe8FQgCNVl1wiolhsFo8kFdO9zdPViCPbmaJA5w==
dependencies:
es-abstract "^1.18.0-next.1"
es-get-iterator "^1.1.0"
is-arguments "^1.0.4"
is-date-object "^1.0.2"
is-regex "^1.1.1"
isarray "^2.0.5"
object-is "^1.1.3"
object-keys "^1.1.1"
object.assign "^4.1.1"
regexp.prototype.flags "^1.3.0"
side-channel "^1.0.3"
which-boxed-primitive "^1.0.1"
which-collection "^1.0.1"
which-typed-array "^1.1.2"
deep-is@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
@ -2645,23 +2596,6 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
string.prototype.trimleft "^2.1.1"
string.prototype.trimright "^2.1.1"
es-abstract@^1.17.4:
version "1.17.7"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
dependencies:
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
is-callable "^1.2.2"
is-regex "^1.1.1"
object-inspect "^1.8.0"
object-keys "^1.1.1"
object.assign "^4.1.1"
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1:
version "1.18.0-next.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
@ -2680,20 +2614,6 @@ es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1:
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
es-get-iterator@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.1.tgz#b93ddd867af16d5118e00881396533c1c6647ad9"
integrity sha512-qorBw8Y7B15DVLaJWy6WdEV/ZkieBcu6QCq/xzWzGOKJqgG1j754vXRfZ3NY7HSShneqU43mPB4OkQBTkvHhFw==
dependencies:
call-bind "^1.0.0"
get-intrinsic "^1.0.1"
has-symbols "^1.0.1"
is-arguments "^1.0.4"
is-map "^2.0.1"
is-set "^2.0.1"
is-string "^1.0.5"
isarray "^2.0.5"
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
@ -3011,7 +2931,7 @@ for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
foreach@^2.0.5, foreach@~2.0.1:
foreach@~2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
@ -3097,15 +3017,6 @@ get-caller-file@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
get-intrinsic@^1.0.0, get-intrinsic@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.1.tgz#94a9768fcbdd0595a1c9273aacf4c89d075631be"
integrity sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
get-port@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc"
@ -3403,22 +3314,12 @@ is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
is-bigint@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4"
integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
dependencies:
binary-extensions "^2.0.0"
is-boolean-object@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@ -3450,7 +3351,7 @@ is-data-descriptor@^1.0.0:
dependencies:
kind-of "^6.0.0"
is-date-object@^1.0.1, is-date-object@^1.0.2:
is-date-object@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
@ -3516,11 +3417,6 @@ is-installed-globally@^0.3.2:
global-dirs "^2.0.1"
is-path-inside "^3.0.1"
is-map@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
is-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
@ -3530,11 +3426,6 @@ is-negative-zero@^2.0.0:
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
is-number-object@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@ -3598,11 +3489,6 @@ is-regex@^1.0.5:
dependencies:
has "^1.0.3"
is-set@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
@ -3611,41 +3497,16 @@ is-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
is-string@^1.0.4, is-string@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
is-symbol@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
dependencies:
has-symbols "^1.0.1"
is-typed-array@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d"
integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==
dependencies:
available-typed-arrays "^1.0.0"
es-abstract "^1.17.4"
foreach "^2.0.5"
has-symbols "^1.0.1"
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
is-weakmap@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
is-weakset@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
@ -3666,11 +3527,6 @@ isarray@1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isbuffer@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/isbuffer/-/isbuffer-0.0.0.tgz#38c146d9df528b8bf9b0701c3d43cf12df3fc39b"
@ -4796,7 +4652,7 @@ object-inspect@^1.8.0:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
object-is@^1.0.1, object-is@^1.1.3:
object-is@^1.0.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81"
integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==
@ -5351,7 +5207,7 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0:
regexp.prototype.flags@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
@ -5359,11 +5215,6 @@ regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0:
define-properties "^1.1.3"
es-abstract "^1.17.0-next.1"
regexparam@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-1.3.0.tgz#2fe42c93e32a40eff6235d635e0ffa344b92965f"
integrity sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==
regexpu-core@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938"
@ -5777,14 +5628,6 @@ shortid@^2.2.15:
dependencies:
nanoid "^2.1.0"
side-channel@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==
dependencies:
es-abstract "^1.18.0-next.0"
object-inspect "^1.8.0"
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@ -6412,6 +6255,11 @@ uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
uuid@^8.3.1:
version "8.3.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@ -6506,43 +6354,10 @@ whatwg-url@^8.0.0:
tr46 "^2.0.2"
webidl-conversions "^5.0.0"
which-boxed-primitive@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"
integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==
dependencies:
is-bigint "^1.0.0"
is-boolean-object "^1.0.0"
is-number-object "^1.0.3"
is-string "^1.0.4"
is-symbol "^1.0.2"
which-collection@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
dependencies:
is-map "^2.0.1"
is-set "^2.0.1"
is-weakmap "^2.0.1"
is-weakset "^2.0.1"
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
which-typed-array@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2"
integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==
dependencies:
available-typed-arrays "^1.0.2"
es-abstract "^1.17.5"
foreach "^2.0.5"
function-bind "^1.1.1"
has-symbols "^1.0.1"
is-typed-array "^1.1.3"
which@^1.2.9, which@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"

View File

@ -55,7 +55,7 @@
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^4.0.4",
"svelte": "3.23.x",
"svelte": "^3.29.7",
"svelte-jester": "^1.0.6"
},
"gitHead": "e4e053cb6ff9a0ddc7115b44ccaa24b8ec41fb9a"

View File

@ -7,6 +7,7 @@ export async function baseApiCall(method, url, body) {
headers: {
"Content-Type": "application/json",
"x-budibase-app-id": getAppId(window.document.cookie),
"x-budibase-type": "client",
},
body: body && JSON.stringify(body),
credentials: "same-origin",

View File

@ -43,7 +43,9 @@ export const screenRouter = ({ screens, onScreenSelected, window }) => {
return sanitize(url)
}
const routes = screens.map(s => makeRootedPath(s.route))
const routes = screens.map(screen =>
makeRootedPath(screen.routing ? screen.routing.route : null)
)
let fallback = routes.findIndex(([p]) => p === makeRootedPath("*"))
if (fallback < 0) fallback = 0

View File

@ -51,7 +51,10 @@ const addWindowGlobals = (window, page, screens) => {
}
export const makePage = props => ({ props })
export const makeScreen = (route, props) => ({ props, route })
export const makeScreen = (route, props) => ({
props,
routing: { route, accessLevelId: "" },
})
export const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))

View File

@ -1,10 +1,9 @@
const CouchDB = require("../../db")
const {
generateAdminPermissions,
generatePowerUserPermissions,
POWERUSER_LEVEL_ID,
ADMIN_LEVEL_ID,
} = require("../../utilities/accessLevels")
BUILTIN_LEVELS,
AccessLevel,
getAccessLevel,
} = require("../../utilities/security/accessLevels")
const {
generateAccessLevelID,
getAccessLevelParams,
@ -19,85 +18,26 @@ exports.fetch = async function(ctx) {
)
const customAccessLevels = body.rows.map(row => row.doc)
const staticAccessLevels = [
{
_id: ADMIN_LEVEL_ID,
name: "Admin",
permissions: await generateAdminPermissions(ctx.user.appId),
},
{
_id: POWERUSER_LEVEL_ID,
name: "Power User",
permissions: await generatePowerUserPermissions(ctx.user.appId),
},
]
const staticAccessLevels = [BUILTIN_LEVELS.ADMIN, BUILTIN_LEVELS.POWER]
ctx.body = [...staticAccessLevels, ...customAccessLevels]
}
exports.find = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
ctx.body = await db.get(ctx.params.levelId)
ctx.body = await getAccessLevel(ctx.user.appId, ctx.params.levelId)
}
exports.update = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const level = await db.get(ctx.params.levelId)
level.name = ctx.body.name
level.permissions = ctx.request.body.permissions
const result = await db.put(level)
level._rev = result.rev
ctx.body = level
ctx.message = `Level ${level.name} updated successfully.`
}
exports.patch = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const level = await db.get(ctx.params.levelId)
const { removedPermissions, addedPermissions, _rev } = ctx.request.body
if (!_rev) throw new Error("Must supply a _rev to update an access level")
level._rev = _rev
if (removedPermissions) {
level.permissions = level.permissions.filter(
p =>
!removedPermissions.some(
rem => rem.name === p.name && rem.itemId === p.itemId
)
)
}
if (addedPermissions) {
level.permissions = [
...level.permissions.filter(
p =>
!addedPermissions.some(
add => add.name === p.name && add.itemId === p.itemId
)
),
...addedPermissions,
]
}
const result = await db.put(level)
level._rev = result.rev
ctx.body = level
ctx.message = `Access Level ${level.name} updated successfully.`
}
exports.create = async function(ctx) {
exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const level = {
name: ctx.request.body.name,
_rev: ctx.request.body._rev,
permissions: ctx.request.body.permissions || [],
_id: generateAccessLevelID(),
type: "accesslevel",
let id = ctx.request.body._id || generateAccessLevelID()
const level = new AccessLevel(
id,
ctx.request.body.name,
ctx.request.body.inherits
)
if (ctx.request.body._rev) {
level._rev = ctx.request.body._rev
}
const result = await db.put(level)
level._rev = result.rev
ctx.body = level

View File

@ -8,15 +8,18 @@ const fs = require("fs-extra")
const { join, resolve } = require("../../utilities/centralPath")
const packageJson = require("../../../package.json")
const { createLinkView } = require("../../db/linkedRows")
const { createRoutingView } = require("../../utilities/routing")
const { downloadTemplate } = require("../../utilities/templates")
const {
generateAppID,
DocumentTypes,
SEPARATOR,
getPageParams,
getScreenParams,
generatePageID,
generateScreenID,
} = require("../../db/utils")
const { BUILTIN_LEVEL_IDS } = require("../../utilities/security/accessLevels")
const {
downloadExtractComponentLibraries,
} = require("../../utilities/createAppPackage")
@ -26,6 +29,20 @@ const { cloneDeep } = require("lodash/fp")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
// utility function, need to do away with this
async function getMainAndUnauthPage(db) {
let pages = await db.allDocs(
getPageParams(null, {
include_docs: true,
})
)
pages = pages.rows.map(row => row.doc)
const mainPage = pages.find(page => page.name === PageTypes.MAIN)
const unauthPage = pages.find(page => page.name === PageTypes.UNAUTHENTICATED)
return { mainPage, unauthPage }
}
async function createInstance(template) {
const appId = generateAppID()
@ -38,6 +55,7 @@ async function createInstance(template) {
})
// add view for linked rows
await createLinkView(appId)
await createRoutingView(appId)
// replicate the template data to the instance DB
if (template) {
@ -65,19 +83,36 @@ exports.fetch = async function(ctx) {
}
}
exports.fetchAppDefinition = async function(ctx) {
const db = new CouchDB(ctx.params.appId)
// TODO: need to get rid of pages here, they shouldn't be needed anymore
const { mainPage, unauthPage } = await getMainAndUnauthPage(db)
const userAccessLevelId =
!ctx.user.accessLevel || !ctx.user.accessLevel._id
? BUILTIN_LEVEL_IDS.PUBLIC
: ctx.user.accessLevel._id
const correctPage =
userAccessLevelId === BUILTIN_LEVEL_IDS.PUBLIC ? unauthPage : mainPage
const screens = (
await db.allDocs(
getScreenParams(correctPage._id, {
include_docs: true,
})
)
).rows.map(row => row.doc)
// TODO: need to handle access control here, limit screens to user access level
ctx.body = {
page: correctPage,
screens: screens,
libraries: ["@budibase/standard-components"],
}
}
exports.fetchAppPackage = async function(ctx) {
const db = new CouchDB(ctx.params.appId)
const application = await db.get(ctx.params.appId)
let pages = await db.allDocs(
getPageParams(null, {
include_docs: true,
})
)
pages = pages.rows.map(row => row.doc)
const mainPage = pages.find(page => page.name === PageTypes.MAIN)
const unauthPage = pages.find(page => page.name === PageTypes.UNAUTHENTICATED)
const { mainPage, unauthPage } = await getMainAndUnauthPage(db)
ctx.body = {
application,
pages: {

View File

@ -34,6 +34,7 @@ exports.authenticate = async ctx => {
userId: dbUser._id,
accessLevelId: dbUser.accessLevelId,
version: app.version,
permissions: dbUser.permissions || [],
}
// if in cloud add the user api key
if (env.CLOUD) {

View File

@ -0,0 +1,100 @@
const { getRoutingInfo } = require("../../utilities/routing")
const {
getUserAccessLevelHierarchy,
BUILTIN_LEVEL_IDS,
} = require("../../utilities/security/accessLevels")
const URL_SEPARATOR = "/"
function Routing() {
this.json = {}
}
Routing.prototype.getTopLevel = function(fullpath) {
if (fullpath.charAt(0) !== URL_SEPARATOR) {
fullpath = URL_SEPARATOR + fullpath
}
// replace the first value with the home route
return URL_SEPARATOR + fullpath.split(URL_SEPARATOR)[1]
}
Routing.prototype.getScreensProp = function(fullpath) {
const topLevel = this.getTopLevel(fullpath)
if (!this.json[topLevel]) {
this.json[topLevel] = {
subpaths: {},
}
}
if (!this.json[topLevel].subpaths[fullpath]) {
this.json[topLevel].subpaths[fullpath] = {
screens: {},
}
}
return this.json[topLevel].subpaths[fullpath].screens
}
Routing.prototype.addScreenId = function(fullpath, accessLevel, screenId) {
this.getScreensProp(fullpath)[accessLevel] = screenId
}
/**
* Gets the full routing structure by querying the routing view and processing the result into the tree.
* @param {string} appId The application to produce the routing structure for.
* @returns {Promise<object>} The routing structure, this is the full structure designed for use in the builder,
* if the client routing is required then the updateRoutingStructureForUserLevel should be used.
*/
async function getRoutingStructure(appId) {
const screenRoutes = await getRoutingInfo(appId)
const routing = new Routing()
for (let screenRoute of screenRoutes) {
let fullpath = screenRoute.routing.route
const accessLevel = screenRoute.routing.accessLevelId
routing.addScreenId(fullpath, accessLevel, screenRoute.id)
}
return { routes: routing.json }
}
exports.fetch = async ctx => {
ctx.body = await getRoutingStructure(ctx.appId)
}
exports.clientFetch = async ctx => {
const routing = await getRoutingStructure(ctx.appId)
let accessLevelId = ctx.user.accessLevel._id
// builder is a special case, always return the full routing structure
if (accessLevelId === BUILTIN_LEVEL_IDS.BUILDER) {
accessLevelId = BUILTIN_LEVEL_IDS.ADMIN
}
const accessLevelIds = await getUserAccessLevelHierarchy(
ctx.appId,
accessLevelId
)
for (let topLevel of Object.values(routing.routes)) {
for (let subpathKey of Object.keys(topLevel.subpaths)) {
let found = false
const subpath = topLevel.subpaths[subpathKey]
const accessLevelOptions = Object.keys(subpath.screens)
if (accessLevelOptions.length === 1 && !accessLevelOptions[0]) {
subpath.screenId = subpath.screens[accessLevelOptions[0]]
subpath.accessLevelId = BUILTIN_LEVEL_IDS.BASIC
found = true
} else {
for (let levelId of accessLevelIds) {
if (accessLevelOptions.indexOf(levelId) !== -1) {
subpath.screenId = subpath.screens[levelId]
subpath.accessLevelId = levelId
found = true
break
}
}
}
delete subpath.screens
if (!found) {
delete topLevel.subpaths[subpathKey]
}
}
}
ctx.body = routing
}

View File

@ -1,20 +1,28 @@
const CouchDB = require("../../db")
const { getScreenParams, generateScreenID } = require("../../db/utils")
const { AccessController } = require("../../utilities/security/accessLevels")
exports.fetch = async ctx => {
const db = new CouchDB(ctx.user.appId)
const appId = ctx.user.appId
const db = new CouchDB(appId)
const screens = await db.allDocs(
getScreenParams(null, {
include_docs: true,
})
const screens = (
await db.allDocs(
getScreenParams(null, {
include_docs: true,
})
)
).rows.map(element => element.doc)
ctx.body = await new AccessController(appId).checkScreensAccess(
screens,
ctx.user.accessLevel._id
)
ctx.body = screens.rows.map(element => element.doc)
}
exports.find = async ctx => {
const db = new CouchDB(ctx.user.appId)
const appId = ctx.user.appId
const db = new CouchDB(appId)
const screens = await db.allDocs(
getScreenParams(ctx.params.pageId, {
@ -22,7 +30,10 @@ exports.find = async ctx => {
})
)
ctx.body = screens.response.rows
ctx.body = await new AccessController(appId).checkScreensAccess(
screens,
ctx.user.accessLevel._id
)
}
exports.save = async ctx => {

View File

@ -2,9 +2,11 @@ const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt")
const { generateUserID, getUserParams } = require("../../db/utils")
const {
POWERUSER_LEVEL_ID,
ADMIN_LEVEL_ID,
} = require("../../utilities/accessLevels")
BUILTIN_LEVEL_ID_ARRAY,
} = require("../../utilities/security/accessLevels")
const {
BUILTIN_PERMISSION_NAMES,
} = require("../../utilities/security/permissions")
exports.fetch = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
@ -18,7 +20,13 @@ exports.fetch = async function(ctx) {
exports.create = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const { username, password, name, accessLevelId } = ctx.request.body
const {
username,
password,
name,
accessLevelId,
permissions,
} = ctx.request.body
if (!username || !password) {
ctx.throw(400, "Username and Password Required.")
@ -35,6 +43,7 @@ exports.create = async function(ctx) {
name: name || username,
type: "user",
accessLevelId,
permissions: permissions || [BUILTIN_PERMISSION_NAMES.POWER],
}
try {
@ -89,10 +98,7 @@ exports.find = async function(ctx) {
const checkAccessLevel = async (db, accessLevelId) => {
if (!accessLevelId) return
if (
accessLevelId === POWERUSER_LEVEL_ID ||
accessLevelId === ADMIN_LEVEL_ID
) {
if (BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevelId) !== -1) {
return {
_id: accessLevelId,
name: accessLevelId,

View File

@ -5,6 +5,7 @@ const { join } = require("../../../utilities/centralPath")
const os = require("os")
const exporters = require("./exporters")
const { fetchView } = require("../row")
const { ViewNames } = require("../../../db/utils")
const controller = {
fetch: async ctx => {
@ -13,8 +14,8 @@ const controller = {
const response = []
for (let name of Object.keys(designDoc.views)) {
// Only return custom views
if (name === "by_link") {
// Only return custom views, not built ins
if (Object.values(ViewNames).indexOf(name) !== -1) {
continue
}
response.push({

View File

@ -4,25 +4,7 @@ const compress = require("koa-compress")
const zlib = require("zlib")
const { budibaseAppsDir } = require("../utilities/budibaseDir")
const { isDev } = require("../utilities")
const {
authRoutes,
pageRoutes,
screenRoutes,
userRoutes,
deployRoutes,
applicationRoutes,
rowRoutes,
tableRoutes,
viewRoutes,
staticRoutes,
componentRoutes,
automationRoutes,
accesslevelRoutes,
apiKeysRoutes,
templatesRoutes,
analyticsRoutes,
webhookRoutes,
} = require("./routes")
const { mainRoutes, authRoutes, staticRoutes } = require("./routes")
const router = new Router()
const env = require("../environment")
@ -72,52 +54,12 @@ router.use(authRoutes.routes())
router.use(authRoutes.allowedMethods())
// authenticated routes
router.use(viewRoutes.routes())
router.use(viewRoutes.allowedMethods())
router.use(tableRoutes.routes())
router.use(tableRoutes.allowedMethods())
router.use(rowRoutes.routes())
router.use(rowRoutes.allowedMethods())
router.use(userRoutes.routes())
router.use(userRoutes.allowedMethods())
router.use(automationRoutes.routes())
router.use(automationRoutes.allowedMethods())
router.use(webhookRoutes.routes())
router.use(webhookRoutes.allowedMethods())
router.use(deployRoutes.routes())
router.use(deployRoutes.allowedMethods())
router.use(templatesRoutes.routes())
router.use(templatesRoutes.allowedMethods())
// end auth routes
router.use(pageRoutes.routes())
router.use(pageRoutes.allowedMethods())
router.use(screenRoutes.routes())
router.use(screenRoutes.allowedMethods())
router.use(applicationRoutes.routes())
router.use(applicationRoutes.allowedMethods())
router.use(componentRoutes.routes())
router.use(componentRoutes.allowedMethods())
router.use(accesslevelRoutes.routes())
router.use(accesslevelRoutes.allowedMethods())
router.use(apiKeysRoutes.routes())
router.use(apiKeysRoutes.allowedMethods())
router.use(analyticsRoutes.routes())
router.use(analyticsRoutes.allowedMethods())
for (let route of mainRoutes) {
router.use(route.routes())
router.use(route.allowedMethods())
}
// WARNING - static routes will catch everything else after them this must be last
router.use(staticRoutes.routes())
router.use(staticRoutes.allowedMethods())

View File

@ -1,14 +1,18 @@
const Router = require("@koa/router")
const controller = require("../controllers/accesslevel")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()
router
.post("/api/accesslevels", controller.create)
.put("/api/accesslevels", controller.update)
.get("/api/accesslevels", controller.fetch)
.get("/api/accesslevels/:levelId", controller.find)
.delete("/api/accesslevels/:levelId/:rev", controller.destroy)
.patch("/api/accesslevels/:levelId", controller.patch)
.post("/api/accesslevels", authorized(BUILDER), controller.save)
.get("/api/accesslevels", authorized(BUILDER), controller.fetch)
.get("/api/accesslevels/:levelId", authorized(BUILDER), controller.find)
.delete(
"/api/accesslevels/:levelId/:rev",
authorized(BUILDER),
controller.destroy
)
module.exports = router

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const controller = require("../controllers/analytics")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router")
const controller = require("../controllers/apikeys")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()

View File

@ -1,19 +1,20 @@
const Router = require("@koa/router")
const controller = require("../controllers/application")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()
router
.get("/api/applications/:appId/definition", controller.fetchAppDefinition)
.get("/api/applications", authorized(BUILDER), controller.fetch)
.get(
"/api/:appId/appPackage",
"/api/applications/:appId/appPackage",
authorized(BUILDER),
controller.fetchAppPackage
)
.put("/api/:appId", authorized(BUILDER), controller.update)
.put("/api/applications/:appId", authorized(BUILDER), controller.update)
.post("/api/applications", authorized(BUILDER), controller.create)
.delete("/api/:appId", authorized(BUILDER), controller.delete)
.delete("/api/applications/:appId", authorized(BUILDER), controller.delete)
module.exports = router

View File

@ -2,7 +2,11 @@ const Router = require("@koa/router")
const controller = require("../controllers/automation")
const authorized = require("../../middleware/authorized")
const joiValidator = require("../../middleware/joi-validator")
const { BUILDER, EXECUTE_AUTOMATION } = require("../../utilities/accessLevels")
const {
BUILDER,
PermissionLevels,
PermissionTypes,
} = require("../../utilities/security/permissions")
const Joi = require("joi")
const router = Router()
@ -75,7 +79,7 @@ router
)
.post(
"/api/automations/:id/trigger",
authorized(EXECUTE_AUTOMATION),
authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE),
controller.trigger
)
.delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy)

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router")
const controller = require("../controllers/component")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router")
const controller = require("../controllers/deploy")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()

View File

@ -15,23 +15,28 @@ const deployRoutes = require("./deploy")
const apiKeysRoutes = require("./apikeys")
const templatesRoutes = require("./templates")
const analyticsRoutes = require("./analytics")
const routingRoutes = require("./routing")
module.exports = {
exports.mainRoutes = [
deployRoutes,
authRoutes,
pageRoutes,
screenRoutes,
userRoutes,
applicationRoutes,
rowRoutes,
tableRoutes,
viewRoutes,
staticRoutes,
componentRoutes,
automationRoutes,
viewRoutes,
componentRoutes,
accesslevelRoutes,
apiKeysRoutes,
templatesRoutes,
analyticsRoutes,
webhookRoutes,
}
routingRoutes,
// these need to be handled last as they still use /api/:tableId
// this could be breaking as koa may recognise other routes as this
tableRoutes,
rowRoutes,
]
exports.authRoutes = authRoutes
exports.staticRoutes = staticRoutes

View File

@ -1,6 +1,6 @@
const Router = require("@koa/router")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const { BUILDER } = require("../../utilities/security/permissions")
const controller = require("../controllers/page")
const router = Router()

View File

@ -0,0 +1,13 @@
const Router = require("@koa/router")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
const controller = require("../controllers/routing")
const router = Router()
// gets the full structure, not just the correct screen ID for your access level
router
.get("/api/routing/client", controller.clientFetch)
.get("/api/routing", authorized(BUILDER), controller.fetch)
module.exports = router

View File

@ -2,46 +2,49 @@ const Router = require("@koa/router")
const rowController = require("../controllers/row")
const authorized = require("../../middleware/authorized")
const usage = require("../../middleware/usageQuota")
const { READ_TABLE, WRITE_TABLE } = require("../../utilities/accessLevels")
const {
PermissionLevels,
PermissionTypes,
} = require("../../utilities/security/permissions")
const router = Router()
router
.get(
"/api/:tableId/:rowId/enrich",
authorized(READ_TABLE, ctx => ctx.params.tableId),
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
rowController.fetchEnrichedRow
)
.get(
"/api/:tableId/rows",
authorized(READ_TABLE, ctx => ctx.params.tableId),
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
rowController.fetchTableRows
)
.get(
"/api/:tableId/rows/:rowId",
authorized(READ_TABLE, ctx => ctx.params.tableId),
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
rowController.find
)
.post("/api/rows/search", rowController.search)
.post(
"/api/:tableId/rows",
authorized(WRITE_TABLE, ctx => ctx.params.tableId),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
usage,
rowController.save
)
.patch(
"/api/:tableId/rows/:id",
authorized(WRITE_TABLE, ctx => ctx.params.tableId),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
rowController.patch
)
.post(
"/api/:tableId/rows/validate",
authorized(WRITE_TABLE, ctx => ctx.params.tableId),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
rowController.validate
)
.delete(
"/api/:tableId/rows/:rowId/:revId",
authorized(WRITE_TABLE, ctx => ctx.params.tableId),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),
usage,
rowController.destroy
)

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router")
const controller = require("../controllers/screen")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const { BUILDER } = require("../../utilities/security/permissions")
const joiValidator = require("../../middleware/joi-validator")
const Joi = require("joi")
@ -12,17 +12,20 @@ function generateSaveValidation() {
return joiValidator.body(Joi.object({
_css: Joi.string().allow(""),
name: Joi.string().required(),
route: Joi.string().required(),
routing: Joi.object({
route: Joi.string().required(),
accessLevelId: Joi.string().required().allow(""),
}).required().unknown(true),
props: Joi.object({
_id: Joi.string().required(),
_component: Joi.string().required(),
_children: Joi.array().required(),
_instanceName: Joi.string().required(),
_styles: Joi.object().required(),
type: Joi.string().optional(),
table: Joi.string().optional(),
}).required().unknown(true),
}).unknown(true))
_id: Joi.string().required(),
_component: Joi.string().required(),
_children: Joi.array().required(),
_instanceName: Joi.string().required(),
_styles: Joi.object().required(),
type: Joi.string().optional(),
table: Joi.string().optional(),
}).required().unknown(true),
}).unknown(true))
}
router

View File

@ -3,7 +3,7 @@ const controller = require("../controllers/static")
const { budibaseTempDir } = require("../../utilities/budibaseDir")
const env = require("../../environment")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const { BUILDER } = require("../../utilities/security/permissions")
const usage = require("../../middleware/usageQuota")
const router = Router()

View File

@ -1,7 +1,11 @@
const Router = require("@koa/router")
const tableController = require("../controllers/table")
const authorized = require("../../middleware/authorized")
const { BUILDER, READ_TABLE } = require("../../utilities/accessLevels")
const {
BUILDER,
PermissionLevels,
PermissionTypes,
} = require("../../utilities/security/permissions")
const router = Router()
@ -9,7 +13,7 @@ router
.get("/api/tables", authorized(BUILDER), tableController.fetch)
.get(
"/api/tables/:id",
authorized(READ_TABLE, ctx => ctx.params.id),
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
tableController.find
)
.post("/api/tables", authorized(BUILDER), tableController.save)

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router")
const controller = require("../controllers/templates")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()

View File

@ -6,13 +6,10 @@ const {
defaultHeaders
} = require("./couchTestUtils")
const {
generateAdminPermissions,
generatePowerUserPermissions,
POWERUSER_LEVEL_ID,
ADMIN_LEVEL_ID,
READ_TABLE,
WRITE_TABLE,
} = require("../../../utilities/accessLevels")
BUILTIN_LEVEL_IDS,
} = require("../../../utilities/security/accessLevels")
const accessLevelBody = { name: "user", inherits: BUILTIN_LEVEL_IDS.BASIC }
describe("/accesslevels", () => {
let server
@ -41,7 +38,7 @@ describe("/accesslevels", () => {
it("returns a success message when level is successfully created", async () => {
const res = await request
.post(`/api/accesslevels`)
.send({ name: "user" })
.send(accessLevelBody)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
@ -49,7 +46,6 @@ describe("/accesslevels", () => {
expect(res.res.statusMessage).toEqual("Access Level 'user' created successfully.")
expect(res.body._id).toBeDefined()
expect(res.body._rev).toBeDefined()
expect(res.body.permissions).toEqual([])
})
});
@ -59,7 +55,7 @@ describe("/accesslevels", () => {
it("should list custom levels, plus 2 default levels", async () => {
const createRes = await request
.post(`/api/accesslevels`)
.send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE }] })
.send(accessLevelBody)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
@ -74,16 +70,17 @@ describe("/accesslevels", () => {
expect(res.body.length).toBe(3)
const adminLevel = res.body.find(r => r._id === ADMIN_LEVEL_ID)
const adminLevel = res.body.find(r => r._id === BUILTIN_LEVEL_IDS.ADMIN)
expect(adminLevel.inherits).toEqual(BUILTIN_LEVEL_IDS.POWER)
expect(adminLevel).toBeDefined()
expect(adminLevel.permissions).toEqual(await generateAdminPermissions(appId))
const powerUserLevel = res.body.find(r => r._id === POWERUSER_LEVEL_ID)
const powerUserLevel = res.body.find(r => r._id === BUILTIN_LEVEL_IDS.POWER)
expect(powerUserLevel.inherits).toEqual(BUILTIN_LEVEL_IDS.BASIC)
expect(powerUserLevel).toBeDefined()
expect(powerUserLevel.permissions).toEqual(await generatePowerUserPermissions(appId))
const customLevelFetched = res.body.find(r => r._id === customLevel._id)
expect(customLevelFetched.permissions).toEqual(customLevel.permissions)
expect(customLevelFetched.inherits).toEqual(BUILTIN_LEVEL_IDS.BASIC)
expect(customLevelFetched).toBeDefined()
})
});
@ -92,7 +89,7 @@ describe("/accesslevels", () => {
it("should delete custom access level", async () => {
const createRes = await request
.post(`/api/accesslevels`)
.send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE } ] })
.send({ name: "user" })
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
@ -110,71 +107,4 @@ describe("/accesslevels", () => {
.expect(404)
})
})
describe("patch", () => {
it("should add given permissions", async () => {
const createRes = await request
.post(`/api/accesslevels`)
.send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE }] })
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
const customLevel = createRes.body
await request
.patch(`/api/accesslevels/${customLevel._id}`)
.send({
_rev: customLevel._rev,
addedPermissions: [ { itemId: table._id, name: WRITE_TABLE } ]
})
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
const finalRes = await request
.get(`/api/accesslevels/${customLevel._id}`)
.set(defaultHeaders(appId))
.expect(200)
expect(finalRes.body.permissions.length).toBe(2)
expect(finalRes.body.permissions.some(p => p.name === WRITE_TABLE)).toBe(true)
expect(finalRes.body.permissions.some(p => p.name === READ_TABLE)).toBe(true)
})
it("should remove given permissions", async () => {
const createRes = await request
.post(`/api/accesslevels`)
.send({
name: "user",
permissions: [
{ itemId: table._id, name: READ_TABLE },
{ itemId: table._id, name: WRITE_TABLE },
]
})
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
const customLevel = createRes.body
await request
.patch(`/api/accesslevels/${customLevel._id}`)
.send({
_rev: customLevel._rev,
removedPermissions: [ { itemId: table._id, name: WRITE_TABLE }]
})
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
const finalRes = await request
.get(`/api/accesslevels/${customLevel._id}`)
.set(defaultHeaders(appId))
.expect(200)
expect(finalRes.body.permissions.length).toBe(1)
expect(finalRes.body.permissions.some(p => p.name === READ_TABLE)).toBe(true)
})
})
});

View File

@ -1,11 +1,11 @@
const CouchDB = require("../../../db")
const supertest = require("supertest")
const {
POWERUSER_LEVEL_ID,
ANON_LEVEL_ID,
BUILDER_LEVEL_ID,
generateAdminPermissions,
} = require("../../../utilities/accessLevels")
BUILTIN_LEVEL_IDS,
} = require("../../../utilities/security/accessLevels")
const {
BUILTIN_PERMISSION_NAMES,
} = require("../../../utilities/security/permissions")
const packageJson = require("../../../../package")
const jwt = require("jsonwebtoken")
const env = require("../../../environment")
@ -26,7 +26,7 @@ exports.supertest = async () => {
exports.defaultHeaders = appId => {
const builderUser = {
userId: "BUILDER",
accessLevelId: BUILDER_LEVEL_ID,
accessLevelId: BUILTIN_LEVEL_IDS.BUILDER,
}
const builderToken = jwt.sign(builderUser, env.JWT_SECRET)
@ -109,7 +109,9 @@ exports.clearApplications = async request => {
.set(exports.defaultHeaders())
for (let app of res.body) {
const appId = app._id
await request.delete(`/api/${appId}`).set(exports.defaultHeaders(appId))
await request
.delete(`/api/applications/${appId}`)
.set(exports.defaultHeaders(appId))
}
}
@ -126,21 +128,13 @@ exports.createUser = async (
name: "Bill",
username,
password,
accessLevelId: POWERUSER_LEVEL_ID,
accessLevelId: BUILTIN_LEVEL_IDS.POWER,
})
return res.body
}
const createUserWithOnePermission = async (
request,
appId,
permName,
itemId
) => {
let permissions = await generateAdminPermissions(appId)
permissions = permissions.filter(
p => p.name === permName && p.itemId === itemId
)
const createUserWithOnePermission = async (request, appId, permName) => {
let permissions = [permName]
return await createUserWithPermissions(
request,
@ -151,7 +145,7 @@ const createUserWithOnePermission = async (
}
const createUserWithAdminPermissions = async (request, appId) => {
let permissions = await generateAdminPermissions(appId)
let permissions = [BUILTIN_PERMISSION_NAMES.ADMIN]
return await createUserWithPermissions(
request,
@ -164,13 +158,9 @@ const createUserWithAdminPermissions = async (request, appId) => {
const createUserWithAllPermissionExceptOne = async (
request,
appId,
permName,
itemId
permName
) => {
let permissions = await generateAdminPermissions(appId)
permissions = permissions.filter(
p => !(p.name === permName && p.itemId === itemId)
)
let permissions = [permName]
return await createUserWithPermissions(
request,
@ -186,11 +176,6 @@ const createUserWithPermissions = async (
permissions,
username
) => {
const accessRes = await request
.post(`/api/accesslevels`)
.send({ name: "TestLevel", permissions })
.set(exports.defaultHeaders(appId))
const password = `password_${username}`
await request
.post(`/api/users`)
@ -199,12 +184,13 @@ const createUserWithPermissions = async (
name: username,
username,
password,
accessLevelId: accessRes.body._id,
accessLevelId: BUILTIN_LEVEL_IDS.POWER,
permissions,
})
const anonUser = {
userId: "ANON",
accessLevelId: ANON_LEVEL_ID,
accessLevelId: BUILTIN_LEVEL_IDS.PUBLIC,
appId: appId,
version: packageJson.version,
}
@ -232,15 +218,10 @@ exports.testPermissionsForEndpoint = async ({
url,
body,
appId,
permissionName,
itemId,
permName1,
permName2,
}) => {
const headers = await createUserWithOnePermission(
request,
appId,
permissionName,
itemId
)
const headers = await createUserWithOnePermission(request, appId, permName1)
await createRequest(request, method, url, body)
.set(headers)
@ -249,8 +230,7 @@ exports.testPermissionsForEndpoint = async ({
const noPermsHeaders = await createUserWithAllPermissionExceptOne(
request,
appId,
permissionName,
itemId
permName2
)
await createRequest(request, method, url, body)

View File

@ -5,11 +5,12 @@ const {
createUser,
testPermissionsForEndpoint,
} = require("./couchTestUtils")
const {
POWERUSER_LEVEL_ID,
LIST_USERS,
USER_MANAGEMENT
} = require("../../../utilities/accessLevels")
const {
BUILTIN_PERMISSION_NAMES,
} = require("../../../utilities/security/permissions")
const {
BUILTIN_LEVEL_IDS,
} = require("../../../utilities/security/accessLevels")
describe("/users", () => {
let request
@ -53,7 +54,8 @@ describe("/users", () => {
method: "GET",
url: `/api/users`,
appId: appId,
permissionName: LIST_USERS,
permName1: BUILTIN_PERMISSION_NAMES.POWER,
permName2: BUILTIN_PERMISSION_NAMES.WRITE,
})
})
@ -65,7 +67,7 @@ describe("/users", () => {
const res = await request
.post(`/api/users`)
.set(defaultHeaders(appId))
.send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: POWERUSER_LEVEL_ID })
.send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: BUILTIN_LEVEL_IDS.POWER })
.expect(200)
.expect('Content-Type', /json/)
@ -77,10 +79,11 @@ describe("/users", () => {
await testPermissionsForEndpoint({
request,
method: "POST",
body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", accessLevelId: POWERUSER_LEVEL_ID },
body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", accessLevelId: BUILTIN_LEVEL_IDS.POWER },
url: `/api/users`,
appId: appId,
permissionName: USER_MANAGEMENT,
permName1: BUILTIN_PERMISSION_NAMES.ADMIN,
permName2: BUILTIN_PERMISSION_NAMES.POWER,
})
})

View File

@ -1,19 +1,39 @@
const Router = require("@koa/router")
const controller = require("../controllers/user")
const authorized = require("../../middleware/authorized")
const { USER_MANAGEMENT, LIST_USERS } = require("../../utilities/accessLevels")
const {
PermissionLevels,
PermissionTypes,
} = require("../../utilities/security/permissions")
const usage = require("../../middleware/usageQuota")
const router = Router()
router
.get("/api/users", authorized(LIST_USERS), controller.fetch)
.get("/api/users/:username", authorized(USER_MANAGEMENT), controller.find)
.put("/api/users/", authorized(USER_MANAGEMENT), controller.update)
.post("/api/users", authorized(USER_MANAGEMENT), usage, controller.create)
.get(
"/api/users",
authorized(PermissionTypes.USER, PermissionLevels.READ),
controller.fetch
)
.get(
"/api/users/:username",
authorized(PermissionTypes.USER, PermissionLevels.READ),
controller.find
)
.put(
"/api/users/",
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
controller.update
)
.post(
"/api/users",
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
usage,
controller.create
)
.delete(
"/api/users/:username",
authorized(USER_MANAGEMENT),
authorized(PermissionTypes.USER, PermissionLevels.WRITE),
usage,
controller.destroy
)

View File

@ -2,7 +2,11 @@ const Router = require("@koa/router")
const viewController = require("../controllers/view")
const rowController = require("../controllers/row")
const authorized = require("../../middleware/authorized")
const { BUILDER, READ_VIEW } = require("../../utilities/accessLevels")
const {
BUILDER,
PermissionTypes,
PermissionLevels,
} = require("../../utilities/security/permissions")
const usage = require("../../middleware/usageQuota")
const router = Router()
@ -10,7 +14,7 @@ const router = Router()
router
.get(
"/api/views/:viewName",
authorized(READ_VIEW, ctx => ctx.params.viewName),
authorized(PermissionTypes.VIEW, PermissionLevels.READ),
rowController.fetchView
)
.get("/api/views", authorized(BUILDER), viewController.fetch)

View File

@ -2,7 +2,11 @@ const Router = require("@koa/router")
const controller = require("../controllers/webhook")
const authorized = require("../../middleware/authorized")
const joiValidator = require("../../middleware/joi-validator")
const { BUILDER, EXECUTE_WEBHOOK } = require("../../utilities/accessLevels")
const {
BUILDER,
PermissionTypes,
PermissionLevels,
} = require("../../utilities/security/permissions")
const Joi = require("joi")
const router = Router()
@ -38,7 +42,7 @@ router
)
.post(
"/api/webhooks/trigger/:instance/:id",
authorized(EXECUTE_WEBHOOK),
authorized(PermissionTypes.WEBHOOK, PermissionLevels.EXECUTE),
controller.trigger
)

View File

@ -1,4 +1,4 @@
const accessLevels = require("../../utilities/accessLevels")
const accessLevels = require("../../utilities/security/accessLevels")
const userController = require("../../api/controllers/user")
const env = require("../../environment")
const usage = require("../../utilities/usageQuota")
@ -11,7 +11,7 @@ module.exports.definition = {
type: "ACTION",
stepId: "CREATE_USER",
inputs: {
accessLevelId: accessLevels.POWERUSER_LEVEL_ID,
accessLevelId: accessLevels.BUILTIN_LEVEL_IDS.POWER,
},
schema: {
inputs: {
@ -28,8 +28,8 @@ module.exports.definition = {
accessLevelId: {
type: "string",
title: "Access Level",
enum: accessLevels.ACCESS_LEVELS,
pretty: Object.values(accessLevels.PRETTY_ACCESS_LEVELS),
enum: accessLevels.BUILTIN_LEVEL_IDS,
pretty: accessLevels.BUILTIN_LEVEL_NAME_ARRAY,
},
},
required: ["username", "password", "accessLevelId"],

View File

@ -1,3 +1,5 @@
const { BUILTIN_LEVEL_IDS } = require("../utilities/security/accessLevels")
exports.HOME_SCREEN = {
description: "",
url: "",
@ -98,6 +100,9 @@ exports.HOME_SCREEN = {
],
_instanceName: "Home",
},
route: "/",
routing: {
route: "/",
accessLevelId: BUILTIN_LEVEL_IDS.BASIC,
},
name: "d834fea2-1b3e-4320-ab34-f9009f5ecc59",
}

View File

@ -1,5 +1,6 @@
const CouchDB = require("../index")
const Sentry = require("@sentry/node")
const { ViewNames, getQueryIndex } = require("../utils")
/**
* Only needed so that boolean parameters are being used for includeDocs
@ -40,7 +41,7 @@ exports.createLinkView = async appId => {
}
designDoc.views = {
...designDoc.views,
by_link: view,
[ViewNames.LINK]: view,
}
await db.put(designDoc)
}
@ -76,7 +77,7 @@ exports.getLinkDocuments = async function({
}
params.include_docs = !!includeDocs
try {
const response = await db.query("database/by_link", params)
const response = await db.query(getQueryIndex(ViewNames.LINK), params)
if (includeDocs) {
return response.rows.map(row => row.doc)
} else {

View File

@ -17,10 +17,20 @@ const DocumentTypes = {
SCREEN: "screen",
}
const ViewNames = {
LINK: "by_link",
ROUTING: "screen_routes",
}
exports.ViewNames = ViewNames
exports.DocumentTypes = DocumentTypes
exports.SEPARATOR = SEPARATOR
exports.UNICODE_MAX = UNICODE_MAX
exports.getQueryIndex = viewName => {
return `database/${viewName}`
}
/**
* If creating DB allDocs/query params with only a single top level ID this can be used, this
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.

View File

@ -1,15 +1,11 @@
const jwt = require("jsonwebtoken")
const STATUS_CODES = require("../utilities/statusCodes")
const accessLevelController = require("../api/controllers/accesslevel")
const {
ADMIN_LEVEL_ID,
POWERUSER_LEVEL_ID,
BUILDER_LEVEL_ID,
ANON_LEVEL_ID,
} = require("../utilities/accessLevels")
const env = require("../environment")
getAccessLevel,
BUILTIN_LEVELS,
} = require("../utilities/security/accessLevels")
const { AuthTypes } = require("../constants")
const { getAppId, getCookieName, setCookie } = require("../utilities")
const { getAppId, getCookieName, setCookie, isClient } = require("../utilities")
module.exports = async (ctx, next) => {
if (ctx.path === "/_builder") {
@ -27,17 +23,11 @@ module.exports = async (ctx, next) => {
appId = cookieAppId
}
const appToken = ctx.cookies.get(getCookieName(appId))
const builderToken = ctx.cookies.get(getCookieName())
let token
// if running locally in the builder itself
if (!env.CLOUD && !appToken) {
token = builderToken
ctx.auth.authenticated = AuthTypes.BUILDER
} else {
token = appToken
ctx.auth.authenticated = AuthTypes.APP
let token = ctx.cookies.get(getCookieName(appId))
let authType = AuthTypes.APP
if (!token && !isClient(ctx)) {
authType = AuthTypes.BUILDER
token = ctx.cookies.get(getCookieName())
}
if (!token) {
@ -45,12 +35,14 @@ module.exports = async (ctx, next) => {
ctx.appId = appId
ctx.user = {
appId,
accessLevel: BUILTIN_LEVELS.PUBLIC,
}
await next()
return
}
try {
ctx.auth.authenticated = authType
const jwtPayload = jwt.verify(token, ctx.config.jwtSecret)
ctx.appId = appId
ctx.auth.apiKey = jwtPayload.apiKey
@ -65,36 +57,3 @@ module.exports = async (ctx, next) => {
await next()
}
/**
* Return the full access level object either from constants
* or the database based on the access level ID passed.
*
* @param {*} appId - appId of the user
* @param {*} accessLevelId - the id of the users access level
*/
const getAccessLevel = async (appId, accessLevelId) => {
if (
accessLevelId === POWERUSER_LEVEL_ID ||
accessLevelId === ADMIN_LEVEL_ID ||
accessLevelId === BUILDER_LEVEL_ID ||
accessLevelId === ANON_LEVEL_ID
) {
return {
_id: accessLevelId,
name: accessLevelId,
permissions: [],
}
}
const findAccessContext = {
params: {
levelId: accessLevelId,
},
user: {
appId,
},
}
await accessLevelController.find(findAccessContext)
return findAccessContext.body
}

View File

@ -1,17 +1,17 @@
const { BUILTIN_LEVEL_IDS } = require("../utilities/security/accessLevels")
const {
adminPermissions,
ADMIN_LEVEL_ID,
POWERUSER_LEVEL_ID,
BUILDER_LEVEL_ID,
BUILDER,
} = require("../utilities/accessLevels")
PermissionTypes,
doesHavePermission,
} = require("../utilities/security/permissions")
const env = require("../environment")
const { apiKeyTable } = require("../db/dynamoClient")
const { AuthTypes } = require("../constants")
const ADMIN_ACCESS = [BUILTIN_LEVEL_IDS.ADMIN, BUILTIN_LEVEL_IDS.BUILDER]
const LOCAL_PASS = new RegExp(["webhooks/trigger", "webhooks/schema"].join("|"))
module.exports = (permName, getItemId) => async (ctx, next) => {
module.exports = (permType, permLevel = null) => async (ctx, next) => {
// webhooks can pass locally
if (!env.CLOUD && LOCAL_PASS.test(ctx.request.url)) {
return next()
@ -37,7 +37,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
}
// don't expose builder endpoints in the cloud
if (env.CLOUD && permName === BUILDER) return
if (env.CLOUD && permType === PermissionTypes.BUILDER) return
if (!ctx.auth.authenticated) {
ctx.throw(403, "Session not authenticated")
@ -47,41 +47,19 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
ctx.throw(403, "User not found")
}
if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) {
const accessLevel = ctx.user.accessLevel
const permissions = ctx.user.permissions
if (ADMIN_ACCESS.indexOf(accessLevel._id) !== -1) {
return next()
}
if (ctx.user.accessLevel._id === BUILDER_LEVEL_ID) {
return next()
}
if (permName === BUILDER) {
if (permType === PermissionTypes.BUILDER) {
ctx.throw(403, "Not Authorized")
return
}
const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "")
const thisPermissionId = permissionId({
name: permName,
itemId: getItemId && getItemId(ctx),
})
// power user has everything, except the admin specific perms
if (
ctx.user.accessLevel._id === POWERUSER_LEVEL_ID &&
!adminPermissions.map(permissionId).includes(thisPermissionId)
) {
return next()
if (!doesHavePermission(permType, permLevel, permissions)) {
ctx.throw(403, "User does not have permission")
}
if (
ctx.user.accessLevel.permissions
.map(permissionId)
.includes(thisPermissionId)
) {
return next()
}
ctx.throw(403, "Not Authorized")
return next()
}

View File

@ -1,36 +0,0 @@
// Permissions
module.exports.READ_TABLE = "read-table"
module.exports.WRITE_TABLE = "write-table"
module.exports.READ_VIEW = "read-view"
module.exports.EXECUTE_AUTOMATION = "execute-automation"
module.exports.EXECUTE_WEBHOOK = "execute-webhook"
module.exports.USER_MANAGEMENT = "user-management"
module.exports.BUILDER = "builder"
module.exports.LIST_USERS = "list-users"
// Access Level IDs
module.exports.ADMIN_LEVEL_ID = "ADMIN"
module.exports.POWERUSER_LEVEL_ID = "POWER_USER"
module.exports.BUILDER_LEVEL_ID = "BUILDER"
module.exports.ANON_LEVEL_ID = "ANON"
module.exports.ACCESS_LEVELS = [
module.exports.ADMIN_LEVEL_ID,
module.exports.POWERUSER_LEVEL_ID,
module.exports.BUILDER_LEVEL_ID,
module.exports.ANON_LEVEL_ID,
]
module.exports.PRETTY_ACCESS_LEVELS = {
[module.exports.ADMIN_LEVEL_ID]: "Admin",
[module.exports.POWERUSER_LEVEL_ID]: "Power user",
[module.exports.BUILDER_LEVEL_ID]: "Builder",
}
module.exports.adminPermissions = [
{
name: module.exports.USER_MANAGEMENT,
},
]
// to avoid circular dependencies this is included later, after exporting all enums
const permissions = require("./permissions")
module.exports.generateAdminPermissions = permissions.generateAdminPermissions
module.exports.generatePowerUserPermissions =
permissions.generatePowerUserPermissions

View File

@ -1,4 +1,5 @@
const { BUILDER_LEVEL_ID } = require("../accessLevels")
const { BUILTIN_LEVEL_IDS } = require("../security/accessLevels")
const { BUILTIN_PERMISSION_NAMES } = require("../security/permissions")
const env = require("../../environment")
const CouchDB = require("../../db")
const jwt = require("jsonwebtoken")
@ -9,7 +10,8 @@ const APP_PREFIX = DocumentTypes.APP + SEPARATOR
module.exports = async (ctx, appId, version) => {
const builderUser = {
userId: "BUILDER",
accessLevelId: BUILDER_LEVEL_ID,
accessLevelId: BUILTIN_LEVEL_IDS.BUILDER,
permissions: [BUILTIN_PERMISSION_NAMES.ADMIN],
version,
}
if (env.BUDIBASE_API_KEY) {

View File

@ -70,3 +70,7 @@ exports.setCookie = (ctx, name, value) => {
overwrite: true,
})
}
exports.isClient = ctx => {
return ctx.headers["x-budibase-type"] === "client"
}

View File

@ -1,66 +0,0 @@
const viewController = require("../api/controllers/view")
const tableController = require("../api/controllers/table")
const automationController = require("../api/controllers/automation")
const accessLevels = require("./accessLevels")
// this has been broken out to reduce risk of circular dependency from utilities, no enums defined here
const generateAdminPermissions = async appId => [
...accessLevels.adminPermissions,
...(await generatePowerUserPermissions(appId)),
]
const generatePowerUserPermissions = async appId => {
const fetchTablesCtx = {
user: {
appId,
},
}
await tableController.fetch(fetchTablesCtx)
const tables = fetchTablesCtx.body
const fetchViewsCtx = {
user: {
appId,
},
}
await viewController.fetch(fetchViewsCtx)
const views = fetchViewsCtx.body
const fetchAutomationsCtx = {
user: {
appId,
},
}
await automationController.fetch(fetchAutomationsCtx)
const automations = fetchAutomationsCtx.body
const readTablePermissions = tables.map(m => ({
itemId: m._id,
name: accessLevels.READ_TABLE,
}))
const writeTablePermissions = tables.map(m => ({
itemId: m._id,
name: accessLevels.WRITE_TABLE,
}))
const viewPermissions = views.map(v => ({
itemId: v.name,
name: accessLevels.READ_VIEW,
}))
const executeAutomationPermissions = automations.map(w => ({
itemId: w._id,
name: accessLevels.EXECUTE_AUTOMATION,
}))
return [
...readTablePermissions,
...writeTablePermissions,
...viewPermissions,
...executeAutomationPermissions,
{ name: accessLevels.LIST_USERS },
]
}
module.exports.generateAdminPermissions = generateAdminPermissions
module.exports.generatePowerUserPermissions = generatePowerUserPermissions

View File

@ -0,0 +1,24 @@
const CouchDB = require("../../db")
const { createRoutingView } = require("./routingUtils")
const { ViewNames, getQueryIndex, UNICODE_MAX } = require("../../db/utils")
exports.getRoutingInfo = async appId => {
const db = new CouchDB(appId)
try {
const allRouting = await db.query(getQueryIndex(ViewNames.ROUTING), {
startKey: "",
endKey: UNICODE_MAX,
})
return allRouting.rows.map(row => row.value)
} catch (err) {
// check if the view doesn't exist, it should for all new instances
if (err != null && err.name === "not_found") {
await createRoutingView(appId)
return exports.getRoutingInfo(appId)
} else {
throw err
}
}
}
exports.createRoutingView = createRoutingView

View File

@ -0,0 +1,24 @@
const CouchDB = require("../../db")
const { DocumentTypes, SEPARATOR, ViewNames } = require("../../db/utils")
const SCREEN_PREFIX = DocumentTypes.SCREEN + SEPARATOR
exports.createRoutingView = async appId => {
const db = new CouchDB(appId)
const designDoc = await db.get("_design/database")
const view = {
// if using variables in a map function need to inject them before use
map: `function(doc) {
if (doc._id.startsWith("${SCREEN_PREFIX}")) {
emit(doc._id, {
id: doc._id,
routing: doc.routing,
})
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewNames.ROUTING]: view,
}
await db.put(designDoc)
}

View File

@ -0,0 +1,150 @@
const CouchDB = require("../../db")
const { cloneDeep } = require("lodash/fp")
const BUILTIN_IDS = {
ADMIN: "ADMIN",
POWER: "POWER_USER",
BASIC: "BASIC",
PUBLIC: "PUBLIC",
BUILDER: "BUILDER",
}
function AccessLevel(id, name, inherits) {
this._id = id
this.name = name
if (inherits) {
this.inherits = inherits
}
}
exports.BUILTIN_LEVELS = {
ADMIN: new AccessLevel(BUILTIN_IDS.ADMIN, "Admin", BUILTIN_IDS.POWER),
POWER: new AccessLevel(BUILTIN_IDS.POWER, "Power", BUILTIN_IDS.BASIC),
BASIC: new AccessLevel(BUILTIN_IDS.BASIC, "Basic", BUILTIN_IDS.PUBLIC),
PUBLIC: new AccessLevel(BUILTIN_IDS.PUBLIC, "Public"),
BUILDER: new AccessLevel(BUILTIN_IDS.BUILDER, "Builder"),
}
exports.BUILTIN_LEVEL_ID_ARRAY = Object.values(exports.BUILTIN_LEVELS).map(
level => level._id
)
exports.BUILTIN_LEVEL_NAME_ARRAY = Object.values(exports.BUILTIN_LEVELS).map(
level => level.name
)
function isBuiltin(accessLevel) {
return exports.BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevel) !== -1
}
/**
* Gets the access level object, this is mainly useful for two purposes, to check if the level exists and
* to check if the access level inherits any others.
* @param {string} appId The app in which to look for the access level.
* @param {string|null} accessLevelId The level ID to lookup.
* @returns {Promise<AccessLevel|object|null>} The access level object, which may contain an "inherits" property.
*/
exports.getAccessLevel = async (appId, accessLevelId) => {
if (!accessLevelId) {
return null
}
let accessLevel
if (isBuiltin(accessLevelId)) {
accessLevel = cloneDeep(
Object.values(exports.BUILTIN_LEVELS).find(
level => level._id === accessLevelId
)
)
} else {
const db = new CouchDB(appId)
accessLevel = await db.get(accessLevelId)
}
return accessLevel
}
/**
* Returns an ordered array of the user's inherited access level IDs, this can be used
* to determine if a user can access something that requires a specific access level.
* @param {string} appId The ID of the application from which access levels should be obtained.
* @param {string} userAccessLevelId The user's access level, this can be found in their access token.
* @returns {Promise<string[]>} returns an ordered array of the access levels, with the first being their
* highest level of access and the last being the lowest level.
*/
exports.getUserAccessLevelHierarchy = async (appId, userAccessLevelId) => {
// special case, if they don't have a level then they are a public user
if (!userAccessLevelId) {
return [BUILTIN_IDS.PUBLIC]
}
let accessLevelIds = [userAccessLevelId]
let userAccess = await exports.getAccessLevel(appId, userAccessLevelId)
// check if inherited makes it possible
while (
userAccess &&
userAccess.inherits &&
accessLevelIds.indexOf(userAccess.inherits) === -1
) {
accessLevelIds.push(userAccess.inherits)
// go to get the inherited incase it inherits anything
userAccess = await exports.getAccessLevel(appId, userAccess.inherits)
}
// add the user's actual level at the end (not at start as that stops iteration
return accessLevelIds
}
class AccessController {
constructor(appId) {
this.appId = appId
this.userHierarchies = {}
}
async hasAccess(tryingAccessLevelId, userAccessLevelId) {
// special cases, the screen has no access level, the access levels are the same or the user
// is currently in the builder
if (
tryingAccessLevelId == null ||
tryingAccessLevelId === "" ||
tryingAccessLevelId === userAccessLevelId ||
userAccessLevelId === BUILTIN_IDS.BUILDER
) {
return true
}
let accessLevelIds = this.userHierarchies[userAccessLevelId]
if (!accessLevelIds) {
accessLevelIds = await exports.getUserAccessLevelHierarchy(
this.appId,
userAccessLevelId
)
this.userHierarchies[userAccessLevelId] = userAccessLevelId
}
return accessLevelIds.indexOf(tryingAccessLevelId) !== -1
}
async checkScreensAccess(screens, userAccessLevelId) {
let accessibleScreens = []
// don't want to handle this with Promise.all as this would mean all custom access levels would be
// retrieved at same time, it is likely a custom levels will be re-used and therefore want
// to work in sync for performance save
for (let screen of screens) {
const accessible = await this.checkScreenAccess(screen, userAccessLevelId)
if (accessible) {
accessibleScreens.push(accessible)
}
}
return accessibleScreens
}
async checkScreenAccess(screen, userAccessLevelId) {
const accessLevelId =
screen && screen.routing ? screen.routing.accessLevelId : null
if (await this.hasAccess(accessLevelId, userAccessLevelId)) {
return screen
}
return null
}
}
exports.AccessController = AccessController
exports.BUILTIN_LEVEL_IDS = BUILTIN_IDS
exports.isBuiltin = isBuiltin
exports.AccessLevel = AccessLevel

View File

@ -0,0 +1,113 @@
const { flatten } = require("lodash")
const PermissionLevels = {
READ: "read",
WRITE: "write",
EXECUTE: "execute",
ADMIN: "admin",
}
const PermissionTypes = {
TABLE: "table",
USER: "user",
AUTOMATION: "automation",
WEBHOOK: "webhook",
BUILDER: "builder",
VIEW: "view",
}
function Permission(type, level) {
this.level = level
this.type = type
}
/**
* Given the specified permission level for the user return the levels they are allowed to carry out.
* @param {string} userPermLevel The permission level of the user.
* @return {string[]} All the permission levels this user is allowed to carry out.
*/
function getAllowedLevels(userPermLevel) {
switch (userPermLevel) {
case PermissionLevels.READ:
return [PermissionLevels.READ]
case PermissionLevels.WRITE:
return [PermissionLevels.READ, PermissionLevels.WRITE]
case PermissionLevels.EXECUTE:
return [PermissionLevels.EXECUTE]
case PermissionLevels.ADMIN:
return [
PermissionLevels.READ,
PermissionLevels.WRITE,
PermissionLevels.EXECUTE,
]
default:
return []
}
}
exports.BUILTIN_PERMISSION_NAMES = {
READ_ONLY: "read_only",
WRITE: "write",
ADMIN: "admin",
POWER: "power",
}
exports.BUILTIN_PERMISSIONS = {
READ_ONLY: {
name: exports.BUILTIN_PERMISSION_NAMES.READ_ONLY,
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.READ),
new Permission(PermissionTypes.VIEW, PermissionLevels.READ),
],
},
WRITE: {
name: exports.BUILTIN_PERMISSION_NAMES.WRITE,
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE),
new Permission(PermissionTypes.VIEW, PermissionLevels.READ),
],
},
POWER: {
name: exports.BUILTIN_PERMISSION_NAMES.POWER,
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE),
new Permission(PermissionTypes.USER, PermissionLevels.READ),
new Permission(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE),
new Permission(PermissionTypes.VIEW, PermissionLevels.READ),
new Permission(PermissionTypes.WEBHOOK, PermissionLevels.READ),
],
},
ADMIN: {
name: exports.BUILTIN_PERMISSION_NAMES.ADMIN,
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.ADMIN),
new Permission(PermissionTypes.USER, PermissionLevels.ADMIN),
new Permission(PermissionTypes.AUTOMATION, PermissionLevels.ADMIN),
new Permission(PermissionTypes.VIEW, PermissionLevels.ADMIN),
new Permission(PermissionTypes.WEBHOOK, PermissionLevels.READ),
],
},
}
exports.doesHavePermission = (permType, permLevel, userPermissionNames) => {
const builtins = Object.values(exports.BUILTIN_PERMISSIONS)
let permissions = flatten(
builtins
.filter(builtin => userPermissionNames.indexOf(builtin.name) !== -1)
.map(builtin => builtin.permissions)
)
for (let permission of permissions) {
if (
permission.type === permType &&
getAllowedLevels(permission.level).indexOf(permLevel) !== -1
) {
return true
}
}
return false
}
// utility as a lot of things need simply the builder permission
exports.BUILDER = PermissionTypes.BUILDER
exports.PermissionTypes = PermissionTypes
exports.PermissionLevels = PermissionLevels

View File

@ -200,10 +200,10 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@budibase/client@^0.3.7":
version "0.3.7"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.3.7.tgz#8ed2d40d91ba3788a69ee5db5078f757adb4187f"
integrity sha512-EgpHfw/WOUYeCG4cILDbaN2WFBDSPS698Z+So7FP5l+4E1fvmqtpXVKJYsviwYEx8AKKYyU3nuDi0l6xzb5Flg==
"@budibase/client@^0.3.8":
version "0.3.8"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.3.8.tgz#75df7e97e8f0d9b58c00e2bb0d3b4a55f8d04735"
integrity sha512-tnFdmCdXKS+uZGoipr69Wa0oVoFHmyoV0ydihI6q0gKQH0KutypVHAaul2qPB8t5a/mTZopC//2WdmCeX1GKVg==
dependencies:
deep-equal "^2.0.1"
mustache "^4.0.1"
@ -886,9 +886,9 @@
"@types/node" "*"
"@types/fs-extra@^9.0.1":
version "9.0.4"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.4.tgz#12553138cf0438db9a31cdc8b0a3aa9332eb67aa"
integrity sha512-50GO5ez44lxK5MDH90DYHFFfqxH7+fTqEEnvguQRzJ/tY9qFrMSHLiYHite+F3SNmf7+LHC1eMXojuD+E3Qcyg==
version "9.0.2"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.2.tgz#e1e1b578c48e8d08ae7fc36e552b94c6f4621609"
integrity sha512-jp0RI6xfZpi5JL8v7WQwpBEQTq63RqW2kxwTZt+m27LcJqQdPVU1yGnT1ZI4EtCDynQQJtIGyQahkiCGCS7e+A==
dependencies:
"@types/node" "*"
@ -950,9 +950,9 @@
"@types/yargs-parser" "*"
"@types/yargs@^15.0.5":
version "15.0.9"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.9.tgz#524cd7998fe810cdb02f26101b699cccd156ff19"
integrity sha512-HmU8SeIRhZCWcnRskCs36Q1Q00KBV6Cqh/ora8WN1+22dY07AZdn6Gel8QZ3t26XYPImtcL8WV/eqjhVmMEw4g==
version "15.0.8"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.8.tgz#7644904cad7427eb704331ea9bf1ee5499b82e23"
integrity sha512-b0BYzFUzBpOhPjpl1wtAHU994jBeKF4TKVlT7ssFv44T617XNcPdRoG4AzHLVshLzlrF7i3lTelH7UbuNYV58Q==
dependencies:
"@types/yargs-parser" "*"
@ -2124,9 +2124,9 @@ debug@^3.1.0, debug@^3.2.6:
ms "^2.1.1"
debug@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.0.tgz#efa41cbf14fc9448075367fdaaddf82376da211e"
integrity sha512-jjO6JD2rKfiZQnBoRzhRTbXjHLGLfH+UtGkWLc/UXAh/rzZMyjbgn0NcfFpqT8nd1kTtFnDiJcrIFkq4UKeJVg==
version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
dependencies:
ms "2.1.2"
@ -3621,9 +3621,9 @@ hosted-git-info@^2.1.4:
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
hosted-git-info@^3.0.5:
version "3.0.7"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.7.tgz#a30727385ea85acfcee94e0aad9e368c792e036c"
integrity sha512-fWqc0IcuXs+BmE9orLDyVykAG9GJtGLGuZAAqgcckPgv5xad4AcXGIv8galtQvlwutxSlaMcdw7BUtq2EIvqCQ==
version "3.0.5"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.5.tgz#bea87905ef7317442e8df3087faa3c842397df03"
integrity sha512-i4dpK6xj9BIpVOTboXIlKG9+8HMKggcrMX7WA24xZtKwX0TPelq/rbaS5rCKeNX8sJXZJGdSxpnEGtta+wismQ==
dependencies:
lru-cache "^6.0.0"
@ -8185,7 +8185,7 @@ y18n@^4.0.0:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
y18n@^5.0.2:
y18n@^5.0.5:
version "5.0.5"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18"
integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==
@ -8230,16 +8230,16 @@ yargs@^13.2.4, yargs@^13.3.0:
yargs-parser "^13.1.2"
yargs@^16.0.3:
version "16.1.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.1.0.tgz#fc333fe4791660eace5a894b39d42f851cd48f2a"
integrity sha512-upWFJOmDdHN0syLuESuvXDmrRcWd1QafJolHskzaw79uZa7/x53gxQKiR07W59GWY1tFhhU/Th9DrtSfpS782g==
version "16.1.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.1.1.tgz#5a4a095bd1ca806b0a50d0c03611d38034d219a1"
integrity sha512-hAD1RcFP/wfgfxgMVswPE+z3tlPFtxG8/yWUrG2i17sTWGCGqWnxKcLTF4cUKDUK8fzokwsmO9H0TDkRbMHy8w==
dependencies:
cliui "^7.0.2"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.0"
y18n "^5.0.2"
y18n "^5.0.5"
yargs-parser "^20.2.2"
yauzl@^2.10.0, yauzl@^2.4.2: