Merge branch 'master' into feature/add-e2e-tests
This commit is contained in:
commit
39f22ff834
|
@ -51,7 +51,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@beyonk/svelte-notifications": "^2.0.3",
|
||||
"@budibase/bbui": "^1.1.1",
|
||||
"@budibase/bbui": "^1.8.0",
|
||||
"@budibase/client": "^0.0.32",
|
||||
"@nx-js/compiler-util": "^2.0.0",
|
||||
"codemirror": "^5.51.0",
|
||||
|
|
|
@ -165,6 +165,10 @@ export default {
|
|||
src: "node_modules/@budibase/client/dist/budibase-client.esm.mjs",
|
||||
dest: outputpath,
|
||||
},
|
||||
{
|
||||
src: "node_modules/@budibase/bbui/dist/bbui.css",
|
||||
dest: outputpath,
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
const apiCall = method => async (url, body) => {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-user-agent": "Budibase Builder",
|
||||
},
|
||||
body: body && JSON.stringify(body),
|
||||
headers,
|
||||
})
|
||||
|
||||
// if (response.status === 500) {
|
||||
// throw new Error("Server Error");
|
||||
// }
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
@ -22,9 +18,9 @@ export const del = apiCall("DELETE")
|
|||
export const put = apiCall("PUT")
|
||||
|
||||
export default {
|
||||
post,
|
||||
get,
|
||||
patch,
|
||||
delete: del,
|
||||
put,
|
||||
post: apiCall("POST"),
|
||||
get: apiCall("GET"),
|
||||
patch: apiCall("PATCH"),
|
||||
delete: apiCall("DELETE"),
|
||||
put: apiCall("PUT"),
|
||||
}
|
||||
|
|
|
@ -24,8 +24,8 @@ export const getBackendUiStore = () => {
|
|||
store.actions = {
|
||||
database: {
|
||||
select: async db => {
|
||||
const modelsResponse = await api.get(`/api/${db._id}/models`)
|
||||
const viewsResponse = await api.get(`/api/${db._id}/views`)
|
||||
const modelsResponse = await api.get(`/api/models`)
|
||||
const viewsResponse = await api.get(`/api/views`)
|
||||
const models = await modelsResponse.json()
|
||||
const views = await viewsResponse.json()
|
||||
store.update(state => {
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { values } from "lodash/fp"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import * as backendStoreActions from "./backend"
|
||||
import { writable, get } from "svelte/store"
|
||||
import api from "../api"
|
||||
import { DEFAULT_PAGES_OBJECT } from "../../constants"
|
||||
import { getExactComponent } from "components/userInterface/pagesParsing/searchComponents"
|
||||
import { rename } from "components/userInterface/pagesParsing/renameScreen"
|
||||
import {
|
||||
createProps,
|
||||
makePropsSafe,
|
||||
|
@ -24,6 +22,7 @@ import {
|
|||
saveCurrentPreviewItem as _saveCurrentPreviewItem,
|
||||
saveScreenApi as _saveScreenApi,
|
||||
regenerateCssForCurrentScreen,
|
||||
renameCurrentScreen,
|
||||
} from "../storeUtils"
|
||||
|
||||
export const getStore = () => {
|
||||
|
@ -52,7 +51,6 @@ export const getStore = () => {
|
|||
store.createDatabaseForApp = backendStoreActions.createDatabaseForApp(store)
|
||||
|
||||
store.saveScreen = saveScreen(store)
|
||||
store.renameScreen = renameScreen(store)
|
||||
store.deleteScreen = deleteScreen(store)
|
||||
store.setCurrentScreen = setCurrentScreen(store)
|
||||
store.setCurrentPage = setCurrentPage(store)
|
||||
|
@ -63,6 +61,7 @@ export const getStore = () => {
|
|||
store.addChildComponent = addChildComponent(store)
|
||||
store.selectComponent = selectComponent(store)
|
||||
store.setComponentProp = setComponentProp(store)
|
||||
store.setPageOrScreenProp = setPageOrScreenProp(store)
|
||||
store.setComponentStyle = setComponentStyle(store)
|
||||
store.setComponentCode = setComponentCode(store)
|
||||
store.setScreenType = setScreenType(store)
|
||||
|
@ -207,46 +206,6 @@ const deleteScreen = store => name => {
|
|||
})
|
||||
}
|
||||
|
||||
const renameScreen = store => (oldname, newname) => {
|
||||
store.update(s => {
|
||||
const { screens, pages, error, changedScreens } = rename(
|
||||
s.pages,
|
||||
s.screens,
|
||||
oldname,
|
||||
newname
|
||||
)
|
||||
|
||||
if (error) {
|
||||
// should really do something with this
|
||||
return s
|
||||
}
|
||||
|
||||
s.screens = screens
|
||||
s.pages = pages
|
||||
if (s.currentPreviewItem.name === oldname)
|
||||
s.currentPreviewItem.name = newname
|
||||
|
||||
const saveAllChanged = async () => {
|
||||
for (let screenName of changedScreens) {
|
||||
const changedScreen = getExactComponent(screens, screenName)
|
||||
await api.post(`/_builder/api/${s.appId}/screen`, changedScreen)
|
||||
}
|
||||
}
|
||||
|
||||
api
|
||||
.patch(`/_builder/api/${s.appId}/screen`, {
|
||||
oldname,
|
||||
newname,
|
||||
})
|
||||
.then(() => saveAllChanged())
|
||||
.then(() => {
|
||||
_savePage(s)
|
||||
})
|
||||
|
||||
return s
|
||||
})
|
||||
}
|
||||
|
||||
const savePage = store => async page => {
|
||||
store.update(state => {
|
||||
if (state.currentFrontEndType !== "page" || !state.currentPageName) {
|
||||
|
@ -335,13 +294,10 @@ const addChildComponent = store => (componentToAdd, presetName) => {
|
|||
|
||||
const presetProps = presetName ? component.presets[presetName] : {}
|
||||
|
||||
const instanceId = get(backendUiStore).selectedDatabase._id
|
||||
|
||||
const newComponent = createProps(
|
||||
component,
|
||||
{
|
||||
...presetProps,
|
||||
_instanceId: instanceId,
|
||||
},
|
||||
state
|
||||
)
|
||||
|
@ -400,6 +356,18 @@ const setComponentProp = store => (name, value) => {
|
|||
})
|
||||
}
|
||||
|
||||
const setPageOrScreenProp = store => (name, value) => {
|
||||
store.update(state => {
|
||||
if (name === "name" && state.currentFrontEndType === "screen") {
|
||||
state = renameCurrentScreen(value, state)
|
||||
} else {
|
||||
state.currentPreviewItem[name] = value
|
||||
_saveCurrentPreviewItem(state)
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const setComponentStyle = store => (type, name, value) => {
|
||||
store.update(state => {
|
||||
if (!state.currentComponentInfo._styles) {
|
||||
|
|
|
@ -3,8 +3,8 @@ import api from "../../api"
|
|||
import Workflow from "./Workflow"
|
||||
|
||||
const workflowActions = store => ({
|
||||
fetch: async instanceId => {
|
||||
const WORKFLOWS_URL = `/api/${instanceId}/workflows`
|
||||
fetch: async () => {
|
||||
const WORKFLOWS_URL = `/api/workflows`
|
||||
const workflowResponse = await api.get(WORKFLOWS_URL)
|
||||
const json = await workflowResponse.json()
|
||||
store.update(state => {
|
||||
|
@ -12,14 +12,14 @@ const workflowActions = store => ({
|
|||
return state
|
||||
})
|
||||
},
|
||||
create: async ({ instanceId, name }) => {
|
||||
create: async ({ name }) => {
|
||||
const workflow = {
|
||||
name,
|
||||
definition: {
|
||||
steps: [],
|
||||
},
|
||||
}
|
||||
const CREATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
|
||||
const CREATE_WORKFLOW_URL = `/api/workflows`
|
||||
const response = await api.post(CREATE_WORKFLOW_URL, workflow)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
|
@ -28,8 +28,8 @@ const workflowActions = store => ({
|
|||
return state
|
||||
})
|
||||
},
|
||||
save: async ({ instanceId, workflow }) => {
|
||||
const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
|
||||
save: async ({ workflow }) => {
|
||||
const UPDATE_WORKFLOW_URL = `/api/workflows`
|
||||
const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
|
@ -42,8 +42,8 @@ const workflowActions = store => ({
|
|||
return state
|
||||
})
|
||||
},
|
||||
update: async ({ instanceId, workflow }) => {
|
||||
const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
|
||||
update: async ({ workflow }) => {
|
||||
const UPDATE_WORKFLOW_URL = `/api/workflows`
|
||||
const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
|
@ -55,9 +55,9 @@ const workflowActions = store => ({
|
|||
return state
|
||||
})
|
||||
},
|
||||
delete: async ({ instanceId, workflow }) => {
|
||||
delete: async ({ workflow }) => {
|
||||
const { _id, _rev } = workflow
|
||||
const DELETE_WORKFLOW_URL = `/api/${instanceId}/workflows/${_id}/${_rev}`
|
||||
const DELETE_WORKFLOW_URL = `/api/workflows/${_id}/${_rev}`
|
||||
await api.delete(DELETE_WORKFLOW_URL)
|
||||
|
||||
store.update(state => {
|
||||
|
|
|
@ -45,6 +45,19 @@ export const saveScreenApi = (screen, s) => {
|
|||
.then(() => savePage(s))
|
||||
}
|
||||
|
||||
export const renameCurrentScreen = (newname, state) => {
|
||||
const oldname = state.currentPreviewItem.name
|
||||
state.currentPreviewItem.name = newname
|
||||
api.patch(
|
||||
`/_builder/api/${state.appId}/pages/${state.currentPageName}/screen`,
|
||||
{
|
||||
oldname,
|
||||
newname,
|
||||
}
|
||||
)
|
||||
return state
|
||||
}
|
||||
|
||||
export const walkProps = (props, action, cancelToken = null) => {
|
||||
cancelToken = cancelToken || { cancelled: false }
|
||||
action(props, () => {
|
||||
|
|
|
@ -53,12 +53,10 @@
|
|||
let views = []
|
||||
let currentPage = 0
|
||||
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
$: {
|
||||
if ($backendUiStore.selectedView) {
|
||||
api
|
||||
.fetchDataForView($backendUiStore.selectedView, instanceId)
|
||||
.fetchDataForView($backendUiStore.selectedView)
|
||||
.then(records => {
|
||||
data = records || []
|
||||
headers = Object.keys($backendUiStore.selectedModel.schema).filter(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import api from "builderStore/api"
|
||||
|
||||
export async function createUser(user, instanceId) {
|
||||
const CREATE_USER_URL = `/api/${instanceId}/users`
|
||||
export async function createUser(user) {
|
||||
const CREATE_USER_URL = `/api/users`
|
||||
const response = await api.post(CREATE_USER_URL, user)
|
||||
return await response.json()
|
||||
}
|
||||
|
@ -14,21 +14,21 @@ export async function createDatabase(appname, instanceName) {
|
|||
return await response.json()
|
||||
}
|
||||
|
||||
export async function deleteRecord(record, instanceId) {
|
||||
const DELETE_RECORDS_URL = `/api/${instanceId}/${record._modelId}/records/${record._id}/${record._rev}`
|
||||
export async function deleteRecord(record) {
|
||||
const DELETE_RECORDS_URL = `/api/${record._modelId}/records/${record._id}/${record._rev}`
|
||||
const response = await api.delete(DELETE_RECORDS_URL)
|
||||
return response
|
||||
}
|
||||
|
||||
export async function saveRecord(record, instanceId, modelId) {
|
||||
const SAVE_RECORDS_URL = `/api/${instanceId}/${modelId}/records`
|
||||
export async function saveRecord(record, modelId) {
|
||||
const SAVE_RECORDS_URL = `/api/${modelId}/records`
|
||||
const response = await api.post(SAVE_RECORDS_URL, record)
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
export async function fetchDataForView(viewName, instanceId) {
|
||||
const FETCH_RECORDS_URL = `/api/${instanceId}/views/${viewName}`
|
||||
export async function fetchDataForView(viewName) {
|
||||
const FETCH_RECORDS_URL = `/api/views/${viewName}`
|
||||
|
||||
const response = await api.get(FETCH_RECORDS_URL)
|
||||
return await response.json()
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
let fieldToEdit
|
||||
|
||||
$: modelFields = model.schema ? Object.entries(model.schema) : []
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
function editField() {}
|
||||
|
||||
|
@ -27,7 +26,7 @@
|
|||
function onFinishedFieldEdit() {}
|
||||
|
||||
async function saveModel() {
|
||||
const SAVE_MODEL_URL = `/api/${instanceId}/models`
|
||||
const SAVE_MODEL_URL = `/api/models`
|
||||
const response = await api.post(SAVE_MODEL_URL, model)
|
||||
const newModel = await response.json()
|
||||
backendUiStore.actions.models.create(newModel)
|
||||
|
|
|
@ -14,8 +14,6 @@
|
|||
let errors = []
|
||||
let selectedModel
|
||||
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
$: modelSchema = $backendUiStore.selectedModel
|
||||
? Object.entries($backendUiStore.selectedModel.schema)
|
||||
: []
|
||||
|
@ -49,7 +47,6 @@
|
|||
...record,
|
||||
modelId: $backendUiStore.selectedModel._id,
|
||||
},
|
||||
instanceId,
|
||||
$backendUiStore.selectedModel._id
|
||||
)
|
||||
if (recordResponse.errors) {
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
function deleteView() {}
|
||||
|
||||
async function saveView() {
|
||||
const SAVE_VIEW_URL = `/api/${instanceId}/views`
|
||||
const SAVE_VIEW_URL = `/api/views`
|
||||
const response = await api.post(SAVE_VIEW_URL, view)
|
||||
backendUiStore.update(state => {
|
||||
state.views = [...state.views, response.view]
|
||||
|
|
|
@ -10,12 +10,11 @@
|
|||
let accessLevelId
|
||||
|
||||
$: valid = username && password && accessLevelId
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
$: appId = $store.appId
|
||||
|
||||
async function createUser() {
|
||||
const user = { name: username, username, password, accessLevelId }
|
||||
const response = await api.createUser(user, instanceId)
|
||||
const response = await api.createUser(user)
|
||||
backendUiStore.actions.users.create(response)
|
||||
onClosed()
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
export let record
|
||||
export let onClosed
|
||||
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
</script>
|
||||
|
||||
<section>
|
||||
|
@ -25,7 +24,7 @@
|
|||
<ActionButton
|
||||
alert
|
||||
on:click={async () => {
|
||||
await api.deleteRecord(record, instanceId)
|
||||
await api.deleteRecord(record)
|
||||
backendUiStore.actions.records.delete(record)
|
||||
onClosed()
|
||||
}}>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
CreateEditModelModal,
|
||||
CreateEditViewModal,
|
||||
} from "components/database/ModelDataTable/modals"
|
||||
import api from "builderStore/api"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
}
|
||||
|
||||
async function deleteModel(modelToDelete) {
|
||||
const DELETE_MODEL_URL = `/api/${instanceId}/models/${node._id}/${node._rev}`
|
||||
const DELETE_MODEL_URL = `/api/models/${node._id}/${node._rev}`
|
||||
const response = await api.delete(DELETE_MODEL_URL)
|
||||
backendUiStore.update(state => {
|
||||
state.models = state.models.filter(
|
||||
|
|
|
@ -12,11 +12,10 @@
|
|||
|
||||
$: currentAppInfo = {
|
||||
appname: $store.appname,
|
||||
instanceId: $backendUiStore.selectedDatabase._id,
|
||||
}
|
||||
|
||||
async function fetchUsers() {
|
||||
const FETCH_USERS_URL = `/api/${currentAppInfo.instanceId}/users`
|
||||
const FETCH_USERS_URL = `/api/users`
|
||||
const response = await api.get(FETCH_USERS_URL)
|
||||
const users = await response.json()
|
||||
backendUiStore.update(state => {
|
||||
|
|
|
@ -91,7 +91,6 @@
|
|||
? screenPlaceholder
|
||||
: $store.currentPreviewItem,
|
||||
],
|
||||
appRootPath: "",
|
||||
}
|
||||
|
||||
$: selectedComponentType = getComponentTypeName($store.currentComponentInfo)
|
||||
|
@ -108,6 +107,8 @@
|
|||
selectedComponentType,
|
||||
selectedComponentId,
|
||||
frontendDefinition,
|
||||
appId: $store.appId,
|
||||
instanceId: $backendUiStore.selectedDatabase._id,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ export default `<html>
|
|||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<script src='/assets/budibase-client.js'></script>
|
||||
<script>
|
||||
function receiveMessage(event) {
|
||||
|
||||
|
@ -45,11 +46,10 @@ export default `<html>
|
|||
styles.appendChild(document.createTextNode(data.styles))
|
||||
|
||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = data.frontendDefinition;
|
||||
if (clientModule) {
|
||||
clientModule.loadBudibase({ window, localStorage })
|
||||
if (window.loadBudibase) {
|
||||
loadBudibase({ window, localStorage })
|
||||
}
|
||||
}
|
||||
let clientModule
|
||||
let styles
|
||||
let selectedComponentStyle
|
||||
|
||||
|
@ -59,12 +59,9 @@ export default `<html>
|
|||
return false;
|
||||
}, true)
|
||||
|
||||
import('/_builder/budibase-client.esm.mjs')
|
||||
.then(module => {
|
||||
clientModule = module
|
||||
window.addEventListener('message', receiveMessage)
|
||||
window.dispatchEvent(new Event('bb-ready'))
|
||||
})
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<script>
|
||||
import FlatButton from "./FlatButton.svelte";
|
||||
|
||||
export let format = "hex";
|
||||
export let onclick = format => {};
|
||||
|
||||
let colorFormats = ["hex", "rgb", "hsl"];
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.flatbutton-group {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
width: 170px;
|
||||
height: 30px;
|
||||
align-self: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="flatbutton-group">
|
||||
{#each colorFormats as text}
|
||||
<FlatButton
|
||||
selected={format === text}
|
||||
{text}
|
||||
on:click={() => onclick(text)} />
|
||||
{/each}
|
||||
</div>
|
|
@ -0,0 +1,23 @@
|
|||
<script>
|
||||
import {buildStyle} from "./helpers.js"
|
||||
|
||||
export let backgroundSize = "10px"
|
||||
export let borderRadius = ""
|
||||
export let height = ""
|
||||
export let width = ""
|
||||
|
||||
$: style = buildStyle({backgroundSize, borderRadius, height, width})
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
div {
|
||||
background-image: url('data:image/svg+xml;utf8, <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path fill="white" d="M1,0H2V1H1V0ZM0,1H1V2H0V1Z"/><path fill="gray" d="M0,0H1V1H0V0ZM1,1H2V2H1V1Z"/></svg>');
|
||||
height: fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div {style}>
|
||||
<slot />
|
||||
</div>
|
|
@ -0,0 +1,160 @@
|
|||
<script>
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import CheckedBackground from "./CheckedBackground.svelte"
|
||||
import {buildStyle} from "./helpers.js"
|
||||
import {
|
||||
getColorFormat,
|
||||
convertToHSVA,
|
||||
convertHsvaToFormat
|
||||
} from "./utils.js";
|
||||
import Slider from "./Slider.svelte";
|
||||
import Palette from "./Palette.svelte";
|
||||
import ButtonGroup from "./ButtonGroup.svelte";
|
||||
import Input from "./Input.svelte";
|
||||
|
||||
export let value = "#3ec1d3ff";
|
||||
export let format = "hexa";
|
||||
|
||||
let h = null;
|
||||
let s = null;
|
||||
let v = null;
|
||||
let a = null;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
onMount(() => {
|
||||
if (format) {
|
||||
convertAndSetHSVA()
|
||||
}
|
||||
});
|
||||
|
||||
function convertAndSetHSVA() {
|
||||
let hsva = convertToHSVA(value, format);
|
||||
setHSVA(hsva);
|
||||
}
|
||||
|
||||
function setHSVA([hue, sat, val, alpha]) {
|
||||
h = hue;
|
||||
s = sat;
|
||||
v = val;
|
||||
a = alpha;
|
||||
}
|
||||
|
||||
//fired by choosing a color from the palette
|
||||
function setSaturationAndValue({ detail }) {
|
||||
s = detail.s;
|
||||
v = detail.v;
|
||||
value = convertHsvaToFormat([h, s, v, a], format);
|
||||
dispatch("change", value)
|
||||
}
|
||||
|
||||
function setHue(hue) {
|
||||
h = hue;
|
||||
value = convertHsvaToFormat([h, s, v, a], format);
|
||||
}
|
||||
|
||||
function setAlpha(alpha) {
|
||||
a = alpha === "1.00" ? "1" :alpha;
|
||||
value = convertHsvaToFormat([h, s, v, a], format);
|
||||
}
|
||||
|
||||
function changeFormatAndConvert(f) {
|
||||
format = f;
|
||||
value = convertHsvaToFormat([h, s, v, a], format);
|
||||
}
|
||||
|
||||
function handleColorInput(text) {
|
||||
let f = getColorFormat(text)
|
||||
if(f) {
|
||||
format = f;
|
||||
value = text
|
||||
convertAndSetHSVA()
|
||||
dispatch("change", value)
|
||||
}
|
||||
}
|
||||
|
||||
$: border = s < 10 ? "1px dashed #dedada" : ""
|
||||
$: style = buildStyle({background: value, border})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.colorpicker-container {
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
flex-direction: column;
|
||||
height: 265px;
|
||||
width: 220px;
|
||||
background: #ffffff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0.15em 1.5em 0 rgba(0,0,0,.1), 0 0 1em 0 rgba(0,0,0,.03);
|
||||
}
|
||||
|
||||
.palette-panel {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
background: white;
|
||||
border: 1px solid #d2d2d2;
|
||||
color: #777373;
|
||||
}
|
||||
|
||||
.alpha-hue-panel {
|
||||
display: grid;
|
||||
grid-template-columns: 25px 1fr;
|
||||
grid-gap: 15px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selected-color {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.format-input-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="colorpicker-container">
|
||||
|
||||
<div class="palette-panel">
|
||||
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
<div class="alpha-hue-panel">
|
||||
<div>
|
||||
<CheckedBackground borderRadius="50%" backgroundSize="8px">
|
||||
<div class="selected-color" {style} />
|
||||
</CheckedBackground>
|
||||
</div>
|
||||
<div>
|
||||
<Slider type="hue" value={h} on:change={hue => setHue(hue.detail)} />
|
||||
|
||||
<CheckedBackground borderRadius="10px" backgroundSize="7px">
|
||||
<Slider
|
||||
type="alpha"
|
||||
value={a}
|
||||
on:change={alpha => setAlpha(alpha.detail)} />
|
||||
</CheckedBackground>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="format-input-panel">
|
||||
<ButtonGroup {format} onclick={changeFormatAndConvert} />
|
||||
<Input {value} on:input={event => handleColorInput(event.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,146 @@
|
|||
<script>
|
||||
import Colorpicker from "./Colorpicker.svelte"
|
||||
import CheckedBackground from "./CheckedBackground.svelte"
|
||||
import {createEventDispatcher, afterUpdate, beforeUpdate} from "svelte"
|
||||
import {buildStyle} from "./helpers.js"
|
||||
import { fade } from 'svelte/transition';
|
||||
import {getColorFormat} from "./utils.js"
|
||||
|
||||
export let value = "#3ec1d3ff"
|
||||
export let open = false;
|
||||
export let width = "25px"
|
||||
export let height = "25px"
|
||||
|
||||
let format = "hexa";
|
||||
let dimensions = {top: 0, left: 0}
|
||||
let colorPreview = null
|
||||
|
||||
let previewHeight = null
|
||||
let previewWidth = null
|
||||
let pickerWidth = 250
|
||||
let pickerHeight = 300
|
||||
|
||||
let anchorEl = null
|
||||
let parentNodes = [];
|
||||
let errorMsg = null
|
||||
|
||||
$: previewStyle = buildStyle({width, height, background: value})
|
||||
$: errorPreviewStyle = buildStyle({width, height})
|
||||
$: pickerStyle = buildStyle({top: `${dimensions.top}px`, left: `${dimensions.left}px`})
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
beforeUpdate(() => {
|
||||
format = getColorFormat(value)
|
||||
if(!format) {
|
||||
errorMsg = `Colorpicker - ${value} is an unknown color format. Please use a hex, rgb or hsl value`
|
||||
console.error(errorMsg)
|
||||
}else{
|
||||
errorMsg = null
|
||||
}
|
||||
})
|
||||
|
||||
afterUpdate(() => {
|
||||
if(colorPreview && colorPreview.offsetParent && !anchorEl) {
|
||||
//Anchor relative to closest positioned ancestor element. If none, then anchor to body
|
||||
anchorEl = colorPreview.offsetParent
|
||||
let curEl = colorPreview
|
||||
let els = []
|
||||
//Travel up dom tree from preview element to find parent elements that scroll
|
||||
while(!anchorEl.isSameNode(curEl)) {
|
||||
curEl = curEl.parentNode
|
||||
let elOverflow = window.getComputedStyle(curEl).getPropertyValue("overflow")
|
||||
if(/scroll|auto/.test(elOverflow)) {
|
||||
els.push(curEl)
|
||||
}
|
||||
}
|
||||
parentNodes = els
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function openColorpicker(event) {
|
||||
if(colorPreview) {
|
||||
const {top: spaceAbove, width, bottom, right, left: spaceLeft} = colorPreview.getBoundingClientRect()
|
||||
const {innerHeight, innerWidth} = window
|
||||
|
||||
const {offsetLeft, offsetTop} = colorPreview
|
||||
//get the scrollTop value for all scrollable parent elements
|
||||
let scrollTop = parentNodes.reduce((scrollAcc, el) => scrollAcc += el.scrollTop, 0);
|
||||
|
||||
const spaceBelow = (innerHeight - spaceAbove) - previewHeight
|
||||
const top = spaceAbove > spaceBelow ? (offsetTop - pickerHeight) - scrollTop : (offsetTop + previewHeight) - scrollTop
|
||||
|
||||
//TOO: Testing and Scroll Awareness for x Scroll
|
||||
const spaceRight = (innerWidth - spaceLeft) + previewWidth
|
||||
const left = spaceRight > spaceLeft ? (offsetLeft + previewWidth) : offsetLeft - pickerWidth
|
||||
|
||||
dimensions = {top, left}
|
||||
|
||||
open = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onColorChange(color) {
|
||||
value = color.detail;
|
||||
dispatch("change", color.detail)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="color-preview-container">
|
||||
{#if !errorMsg}
|
||||
<CheckedBackground borderRadius="3px" backgroundSize="8px">
|
||||
<div bind:this={colorPreview} bind:clientHeight={previewHeight} bind:clientWidth={previewWidth} class="color-preview" style={previewStyle} on:click={openColorpicker} />
|
||||
</CheckedBackground>
|
||||
|
||||
{#if open}
|
||||
<div class="picker-container" bind:clientHeight={pickerHeight} bind:clientWidth={pickerWidth} style={pickerStyle}>
|
||||
<Colorpicker on:change={onColorChange} {format} {value} />
|
||||
</div>
|
||||
<div on:click|self={() => open = false} class="overlay"></div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="color-preview preview-error" style={errorPreviewStyle}>
|
||||
<span>×</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.color-preview-container{
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
border-radius: 3px;
|
||||
border: 1px solid #dedada;
|
||||
}
|
||||
|
||||
.preview-error {
|
||||
background: #cccccc;
|
||||
color: #808080;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.picker-container {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.overlay{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<script>
|
||||
export let text = "";
|
||||
export let selected = false;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.flatbutton {
|
||||
cursor: pointer;
|
||||
border: 1px solid #d4d4d4;
|
||||
border-radius: 8px;
|
||||
text-transform: uppercase;
|
||||
margin: 5px;
|
||||
transition: all 0.3s;
|
||||
font-size: 10px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f1f3f4;
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: #ffffff;
|
||||
background-color: #003cb0;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="flatbutton" class:selected on:click>{text}</div>
|
|
@ -0,0 +1,28 @@
|
|||
<script>
|
||||
export let value = "";
|
||||
</script>
|
||||
|
||||
<style>
|
||||
div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 175px;
|
||||
font-size: 13px;
|
||||
background: #f1f3f4;
|
||||
border-radius: 8px;
|
||||
height: 20px;
|
||||
outline-color: #003cb0;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
border: 1px solid #dadada;
|
||||
font-weight: 550;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<input on:input type="text" {value} maxlength="25" />
|
||||
</div>
|
|
@ -0,0 +1,63 @@
|
|||
<script>
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import CheckedBackground from "./CheckedBackground.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let h = 0;
|
||||
export let s = 0;
|
||||
export let v = 0;
|
||||
export let a = 1;
|
||||
|
||||
let palette;
|
||||
|
||||
let paletteHeight, paletteWidth = 0;
|
||||
|
||||
|
||||
function handleClick(event) {
|
||||
const { left, top } = palette.getBoundingClientRect();
|
||||
let clickX = (event.clientX - left)
|
||||
let clickY = (event.clientY - top)
|
||||
if((clickX > 0 && clickY > 0) && (clickX < paletteWidth && clickY < paletteHeight)) {
|
||||
let s = (clickX / paletteWidth) * 100
|
||||
let v = 100 - ((clickY / paletteHeight) * 100)
|
||||
dispatch("change", {s, v})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$: pickerX = (s * paletteWidth) / 100;
|
||||
$: pickerY = paletteHeight * ((100 - v) / 100)
|
||||
|
||||
$: paletteGradient = `linear-gradient(to top, rgba(0, 0, 0, 1), transparent),
|
||||
linear-gradient(to left, hsla(${h}, 100%, 50%, ${a}), rgba(255, 255, 255, ${a}))
|
||||
`;
|
||||
$: style = `background: ${paletteGradient};`;
|
||||
|
||||
$: pickerStyle = `transform: translate(${pickerX - 8}px, ${pickerY - 8}px);`
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.palette {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
cursor: crosshair;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.picker {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: transparent;
|
||||
border: 2px solid white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<CheckedBackground width="100%">
|
||||
<div bind:this={palette} bind:clientHeight={paletteHeight} bind:clientWidth={paletteWidth} on:click={handleClick} class="palette" {style}>
|
||||
<div class="picker" style={pickerStyle} />
|
||||
</div>
|
||||
</CheckedBackground>
|
|
@ -0,0 +1,87 @@
|
|||
<script>
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import dragable from "./drag.js";
|
||||
|
||||
export let value = 1;
|
||||
export let type = "hue";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let slider;
|
||||
let sliderWidth = 0;
|
||||
|
||||
function handleClick(mouseX) {
|
||||
const { left, width } = slider.getBoundingClientRect();
|
||||
let clickPosition = mouseX - left;
|
||||
|
||||
let percentageClick = (clickPosition / sliderWidth).toFixed(2)
|
||||
|
||||
if (percentageClick >= 0 && percentageClick <= 1) {
|
||||
let value =
|
||||
type === "hue"
|
||||
? 360 * percentageClick
|
||||
: percentageClick;
|
||||
dispatch("change", value);
|
||||
}
|
||||
}
|
||||
|
||||
$: thumbPosition =
|
||||
type === "hue" ? sliderWidth * (value / 360) : sliderWidth * value;
|
||||
|
||||
$: style = `transform: translateX(${thumbPosition - 6}px);`;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.color-format-slider {
|
||||
position: relative;
|
||||
align-self: center;
|
||||
height: 8px;
|
||||
width: 150px;
|
||||
border-radius: 10px;
|
||||
margin: 10px 0px;
|
||||
border: 1px solid #e8e8ef;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hue {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(0, 100%, 50%),
|
||||
hsl(60, 100%, 50%),
|
||||
hsl(120, 100%, 50%),
|
||||
hsl(180, 100%, 50%),
|
||||
hsl(240, 100%, 50%),
|
||||
hsl(300, 100%, 50%),
|
||||
hsl(360, 100%, 50%)
|
||||
);
|
||||
}
|
||||
|
||||
.alpha {
|
||||
background: linear-gradient(to right, transparent, rgb(0 0 0));
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border: 1px solid #777676;
|
||||
border-radius: 50%;
|
||||
background-color: #ffffff;
|
||||
cursor:grab;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
bind:this={slider}
|
||||
bind:clientWidth={sliderWidth}
|
||||
on:click={event => handleClick(event.clientX)}
|
||||
class="color-format-slider"
|
||||
class:hue={type === 'hue'}
|
||||
class:alpha={type === 'alpha'}>
|
||||
<div
|
||||
use:dragable
|
||||
on:drag={e => handleClick(e.detail)}
|
||||
class="slider-thumb"
|
||||
{style} />
|
||||
</div>
|
|
@ -0,0 +1,22 @@
|
|||
export default function(node) {
|
||||
function handleMouseDown() {
|
||||
window.addEventListener("mousemove", handleMouseMove)
|
||||
window.addEventListener("mouseup", handleMouseUp)
|
||||
}
|
||||
|
||||
function handleMouseMove(event) {
|
||||
let mouseX = event.clientX
|
||||
node.dispatchEvent(
|
||||
new CustomEvent("drag", {
|
||||
detail: mouseX,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
window.removeEventListener("mousedown", handleMouseDown)
|
||||
window.removeEventListener("mousemove", handleMouseMove)
|
||||
}
|
||||
|
||||
node.addEventListener("mousedown", handleMouseDown)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
export const buildStyle = styles => {
|
||||
let str = ""
|
||||
for (let s in styles) {
|
||||
if (styles[s]) {
|
||||
let key = convertCamel(s)
|
||||
str += `${key}: ${styles[s]}; `
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
export const convertCamel = str => {
|
||||
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import Colorpreview from "./Colorpreview.svelte"
|
||||
export default Colorpreview
|
|
@ -0,0 +1,279 @@
|
|||
export const isValidHex = str =>
|
||||
/^#(?:[A-F0-9]{3}$|[A-F0-9]{4}$|[A-F0-9]{6}$|[A-F0-9]{8})$/gi.test(str)
|
||||
|
||||
const getHexaValues = hexString => {
|
||||
if (hexString.length <= 5) {
|
||||
let hexArr = hexString.match(/[A-F0-9]/gi)
|
||||
let t = hexArr.map(c => (c += c))
|
||||
return t
|
||||
} else {
|
||||
return hexString.match(/[A-F0-9]{2}/gi)
|
||||
}
|
||||
}
|
||||
|
||||
export const isValidRgb = str => {
|
||||
const hasValidStructure = /^(?:rgba\(|rgb\()(?:[0-9,\s]|\.(?=\d))*\)$/gi.test(
|
||||
str
|
||||
)
|
||||
if (hasValidStructure) {
|
||||
return testRgbaValues(str.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
const findNonNumericChars = /[a-z()\s]/gi
|
||||
|
||||
export const getNumericValues = str =>
|
||||
str
|
||||
.replace(findNonNumericChars, "")
|
||||
.split(",")
|
||||
.map(v => (v !== "" ? v : undefined))
|
||||
|
||||
export const testRgbaValues = str => {
|
||||
const rgba = getNumericValues(str)
|
||||
const [r, g, b, a] = rgba
|
||||
|
||||
let isValidLengthRange =
|
||||
(str.startsWith("rgb(") && rgba.length === 3) ||
|
||||
(str.startsWith("rgba(") && rgba.length === 4)
|
||||
let isValidColorRange = [r, g, b].every(v => v >= 0 && v <= 255)
|
||||
let isValidAlphaRange = str.startsWith("rgba(")
|
||||
? `${a}`.length <= 4 && a >= 0 && a <= 1
|
||||
: true
|
||||
|
||||
return isValidLengthRange && isValidColorRange && isValidAlphaRange
|
||||
}
|
||||
|
||||
export const isValidHsl = str => {
|
||||
const hasValidStructure = /^(?:hsl\(|hsla\()(?:[0-9,%\s]|\.(?=\d))*\)$/gi.test(
|
||||
str
|
||||
)
|
||||
if (hasValidStructure) {
|
||||
return testHslaValues(str.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
export const testHslaValues = str => {
|
||||
const hsla = getNumericValues(str)
|
||||
const [h, s, l, a] = hsla
|
||||
const isUndefined = [h, s, l].some(v => v === undefined)
|
||||
|
||||
if (isUndefined) return false
|
||||
|
||||
let isValidLengthRange =
|
||||
(str.startsWith("hsl(") && hsla.length === 3) ||
|
||||
(str.startsWith("hsla(") && hsla.length === 4)
|
||||
let isValidHue = h >= 0 && h <= 360
|
||||
let isValidSatLum = [s, l].every(
|
||||
v => v.endsWith("%") && parseInt(v) >= 0 && parseInt(v) <= 100
|
||||
)
|
||||
let isValidAlphaRange = str.startsWith("hsla(")
|
||||
? `${a}`.length <= 4 && a >= 0 && a <= 1
|
||||
: true
|
||||
|
||||
return isValidLengthRange && isValidHue && isValidSatLum && isValidAlphaRange
|
||||
}
|
||||
|
||||
export const getColorFormat = color => {
|
||||
if (typeof color === "string") {
|
||||
if (isValidHex(color)) {
|
||||
return "hex"
|
||||
} else if (isValidRgb(color)) {
|
||||
return "rgb"
|
||||
} else if (isValidHsl(color)) {
|
||||
return "hsl"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const convertToHSVA = (value, format) => {
|
||||
switch (format) {
|
||||
case "hex":
|
||||
return getAndConvertHexa(value)
|
||||
case "rgb":
|
||||
return getAndConvertRgba(value)
|
||||
case "hsl":
|
||||
return getAndConvertHsla(value)
|
||||
}
|
||||
}
|
||||
|
||||
export const convertHsvaToFormat = (hsva, format) => {
|
||||
switch (format) {
|
||||
case "hex":
|
||||
return hsvaToHexa(hsva, true)
|
||||
case "rgb":
|
||||
return hsvaToRgba(hsva, true)
|
||||
case "hsl":
|
||||
return hsvaToHsla(hsva)
|
||||
}
|
||||
}
|
||||
|
||||
export const getAndConvertHexa = color => {
|
||||
let [rHex, gHex, bHex, aHex] = getHexaValues(color)
|
||||
return hexaToHSVA([rHex, gHex, bHex], aHex)
|
||||
}
|
||||
|
||||
export const getAndConvertRgba = color => {
|
||||
let rgba = getNumericValues(color)
|
||||
return rgbaToHSVA(rgba)
|
||||
}
|
||||
|
||||
export const getAndConvertHsla = color => {
|
||||
let hsla = getNumericValues(color)
|
||||
return hslaToHSVA(hsla)
|
||||
}
|
||||
|
||||
export const hexaToHSVA = (hex, alpha = "FF") => {
|
||||
const rgba = hex
|
||||
.map(v => parseInt(v, 16))
|
||||
.concat(Number((parseInt(alpha, 16) / 255).toFixed(2)))
|
||||
return rgbaToHSVA(rgba)
|
||||
}
|
||||
|
||||
export const rgbaToHSVA = rgba => {
|
||||
const [r, g, b, a = 1] = rgba
|
||||
let hsv = _rgbToHSV([r, g, b])
|
||||
return [...hsv, a].map(x => parseFloat(x))
|
||||
}
|
||||
|
||||
export const hslaToHSVA = ([h, s, l, a = 1]) => {
|
||||
let sat = s.replace(/%/, "")
|
||||
let lum = l.replace(/%/, "")
|
||||
let hsv = _hslToHSV([h, sat, lum])
|
||||
return [...hsv, a].map(x => parseFloat(x))
|
||||
}
|
||||
|
||||
export const hsvaToHexa = (hsva, asString = false) => {
|
||||
const [r, g, b, a] = hsvaToRgba(hsva)
|
||||
|
||||
const hexa = [r, g, b]
|
||||
.map(v => {
|
||||
let hex = Math.round(v).toString(16)
|
||||
return hex.length === 1 ? `0${hex}` : hex
|
||||
})
|
||||
.concat(Math.round(a * 255).toString(16))
|
||||
return asString ? `#${hexa.join("")}` : hexa
|
||||
}
|
||||
|
||||
export const hsvaToRgba = ([h, s, v, a = 1], asString = false) => {
|
||||
let rgb = _hsvToRgb([h, s, v]).map(x => Math.round(x))
|
||||
let rgba = [...rgb, a < 1 ? _fixNum(a, 2) : a]
|
||||
return asString ? `rgba(${rgba.join(",")})` : rgba
|
||||
}
|
||||
|
||||
export const hsvaToHsla = ([h, s, v, a = 1]) => {
|
||||
let [hue, sat, lum] = _hsvToHSL([h, s, v])
|
||||
let hsla = [hue, sat + "%", lum + "%", a < 1 ? _fixNum(a, 2) : a]
|
||||
return `hsla(${hsla.join(",")})`
|
||||
}
|
||||
|
||||
export const _hslToHSV = hsl => {
|
||||
const h = hsl[0]
|
||||
let s = hsl[1] / 100
|
||||
let l = hsl[2] / 100
|
||||
let smin = s
|
||||
const lmin = Math.max(l, 0.01)
|
||||
|
||||
l *= 2
|
||||
s *= l <= 1 ? l : 2 - l
|
||||
smin *= lmin <= 1 ? lmin : 2 - lmin
|
||||
const v = (l + s) / 2
|
||||
const sv = l === 0 ? (2 * smin) / (lmin + smin) : (2 * s) / (l + s)
|
||||
|
||||
return [h, sv * 100, v * 100]
|
||||
}
|
||||
|
||||
//Credit : https://github.com/Qix-/color-convert
|
||||
export const _rgbToHSV = rgb => {
|
||||
let rdif
|
||||
let gdif
|
||||
let bdif
|
||||
let h
|
||||
let s
|
||||
|
||||
const r = rgb[0] / 255
|
||||
const g = rgb[1] / 255
|
||||
const b = rgb[2] / 255
|
||||
const v = Math.max(r, g, b)
|
||||
const diff = v - Math.min(r, g, b)
|
||||
const diffc = function(c) {
|
||||
return (v - c) / 6 / diff + 1 / 2
|
||||
}
|
||||
|
||||
if (diff === 0) {
|
||||
h = 0
|
||||
s = 0
|
||||
} else {
|
||||
s = diff / v
|
||||
rdif = diffc(r)
|
||||
gdif = diffc(g)
|
||||
bdif = diffc(b)
|
||||
|
||||
if (r === v) {
|
||||
h = bdif - gdif
|
||||
} else if (g === v) {
|
||||
h = 1 / 3 + rdif - bdif
|
||||
} else if (b === v) {
|
||||
h = 2 / 3 + gdif - rdif
|
||||
}
|
||||
|
||||
if (h < 0) {
|
||||
h += 1
|
||||
} else if (h > 1) {
|
||||
h -= 1
|
||||
}
|
||||
}
|
||||
|
||||
const hsvResult = [h * 360, s * 100, v * 100].map(v => Math.round(v))
|
||||
return hsvResult
|
||||
}
|
||||
|
||||
//Credit : https://github.com/Qix-/color-convert
|
||||
export const _hsvToRgb = hsv => {
|
||||
const h = hsv[0] / 60
|
||||
const s = hsv[1] / 100
|
||||
let v = hsv[2] / 100
|
||||
const hi = Math.floor(h) % 6
|
||||
|
||||
const f = h - Math.floor(h)
|
||||
const p = 255 * v * (1 - s)
|
||||
const q = 255 * v * (1 - s * f)
|
||||
const t = 255 * v * (1 - s * (1 - f))
|
||||
v *= 255
|
||||
|
||||
switch (hi) {
|
||||
case 0:
|
||||
return [v, t, p]
|
||||
case 1:
|
||||
return [q, v, p]
|
||||
case 2:
|
||||
return [p, v, t]
|
||||
case 3:
|
||||
return [p, q, v]
|
||||
case 4:
|
||||
return [t, p, v]
|
||||
case 5:
|
||||
return [v, p, q]
|
||||
}
|
||||
}
|
||||
|
||||
//Credit : https://github.com/Qix-/color-convert
|
||||
export const _hsvToHSL = hsv => {
|
||||
const h = hsv[0]
|
||||
const s = hsv[1] / 100
|
||||
const v = hsv[2] / 100
|
||||
const vmin = Math.max(v, 0.01)
|
||||
let sl
|
||||
let l
|
||||
|
||||
l = (2 - s) * v
|
||||
const lmin = (2 - s) * vmin
|
||||
sl = s * vmin
|
||||
sl /= lmin <= 1 ? lmin : 2 - lmin
|
||||
sl = sl || 0
|
||||
l /= 2
|
||||
|
||||
return [_fixNum(h, 0), _fixNum(sl * 100, 0), _fixNum(l * 100, 0)]
|
||||
}
|
||||
|
||||
export const _fixNum = (value, decimalPlaces) =>
|
||||
Number(parseFloat(value).toFixed(decimalPlaces))
|
|
@ -0,0 +1,106 @@
|
|||
import { getColorFormat, convertToHSVA, convertHsvaToFormat } from "./utils"
|
||||
|
||||
describe("convertToHSVA - convert to hsva from format", () => {
|
||||
test("convert from hexa", () => {
|
||||
expect(convertToHSVA("#f222d382", "hex")).toEqual([309, 86, 95, 0.51])
|
||||
})
|
||||
|
||||
test("convert from hex", () => {
|
||||
expect(convertToHSVA("#f222d3", "hex")).toEqual([309, 86, 95, 1])
|
||||
})
|
||||
|
||||
test("convert from rgba", () => {
|
||||
expect(convertToHSVA("rgba(242, 34, 211, 1)", "rgb")).toEqual([
|
||||
309,
|
||||
86,
|
||||
95,
|
||||
1,
|
||||
])
|
||||
})
|
||||
|
||||
test("convert from rgb", () => {
|
||||
expect(convertToHSVA("rgb(150, 80, 255)", "rgb")).toEqual([264, 69, 100, 1])
|
||||
})
|
||||
|
||||
test("convert from from hsl", () => {
|
||||
expect(convertToHSVA("hsl(264, 100%, 65.7%)", "hsl")).toEqual([
|
||||
264,
|
||||
68.6,
|
||||
100,
|
||||
1,
|
||||
])
|
||||
})
|
||||
|
||||
test("convert from from hsla", () => {
|
||||
expect(convertToHSVA("hsla(264, 100%, 65.7%, 0.51)", "hsl")).toEqual([
|
||||
264,
|
||||
68.6,
|
||||
100,
|
||||
0.51,
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("convertHsvaToFormat - convert from hsva to format", () => {
|
||||
test("Convert to hexa", () => {
|
||||
expect(convertHsvaToFormat([264, 68.63, 100, 0.5], "hex")).toBe("#9650ff80")
|
||||
})
|
||||
|
||||
test("Convert to rgba", () => {
|
||||
expect(convertHsvaToFormat([264, 68.63, 100, 0.75], "rgb")).toBe(
|
||||
"rgba(150,80,255,0.75)"
|
||||
)
|
||||
})
|
||||
|
||||
test("Convert to hsla", () => {
|
||||
expect(convertHsvaToFormat([264, 68.63, 100, 1], "hsl")).toBe(
|
||||
"hsla(264,100%,66%,1)"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Get Color Format", () => {
|
||||
test("Testing valid hex string", () => {
|
||||
expect(getColorFormat("#FFF")).toBe("hex")
|
||||
})
|
||||
|
||||
test("Testing invalid hex string", () => {
|
||||
expect(getColorFormat("#FFZ")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("Testing valid hex with alpha", () => {
|
||||
expect(getColorFormat("#FF00BB80")).toBe("hex")
|
||||
})
|
||||
|
||||
test("Test valid rgb value", () => {
|
||||
expect(getColorFormat("RGB(255, 20, 50)")).toBe("rgb")
|
||||
})
|
||||
|
||||
test("Testing invalid rgb value", () => {
|
||||
expect(getColorFormat("rgb(255, 0)")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("Testing rgb value with alpha", () => {
|
||||
expect(getColorFormat("rgba(255, 0, 50, 0.5)")).toBe("rgb")
|
||||
})
|
||||
|
||||
test("Testing rgb value with incorrectly provided alpha", () => {
|
||||
expect(getColorFormat("rgb(255, 0, 50, 0.5)")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("Testing invalid hsl value", () => {
|
||||
expect(getColorFormat("hsla(255, 0)")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("Testing hsla value with alpha", () => {
|
||||
expect(getColorFormat("hsla(150, 60%, 50%, 0.5)")).toBe("hsl")
|
||||
})
|
||||
|
||||
test("Testing hsl value with incorrectly provided alpha", () => {
|
||||
expect(getColorFormat("hsl(150, 0, 50, 0.5)")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("Testing out of bounds hsl", () => {
|
||||
expect(getColorFormat("hsl(375, 0, 50)")).toBeUndefined()
|
||||
})
|
||||
})
|
|
@ -13,7 +13,6 @@
|
|||
import CodeEditor from "./CodeEditor.svelte"
|
||||
import LayoutEditor from "./LayoutEditor.svelte"
|
||||
import EventsEditor from "./EventsEditor"
|
||||
|
||||
import panelStructure from "./temporaryPanelStructure.js"
|
||||
import CategoryTab from "./CategoryTab.svelte"
|
||||
import DesignView from "./DesignView.svelte"
|
||||
|
@ -40,28 +39,9 @@
|
|||
|
||||
let panelDefinition = {}
|
||||
|
||||
$: {
|
||||
if (componentPropDefinition.properties) {
|
||||
if (selectedCategory.value === "design") {
|
||||
panelDefinition = componentPropDefinition.properties["design"]
|
||||
} else {
|
||||
let panelDef = componentPropDefinition.properties["settings"]
|
||||
if (
|
||||
$store.currentFrontEndType === "page" &&
|
||||
$store.currentView !== "component"
|
||||
) {
|
||||
panelDefinition = [...page, ...panelDef]
|
||||
} else if (
|
||||
$store.currentFrontEndType === "screen" &&
|
||||
$store.currentView !== "component"
|
||||
) {
|
||||
panelDefinition = [...screen, ...panelDef]
|
||||
} else {
|
||||
panelDefinition = panelDef
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$: panelDefinition =
|
||||
componentPropDefinition.properties &&
|
||||
componentPropDefinition.properties[selectedCategory.value]
|
||||
|
||||
const onStyleChanged = store.setComponentStyle
|
||||
const onPropChanged = store.setComponentProp
|
||||
|
@ -107,7 +87,9 @@
|
|||
{componentInstance}
|
||||
{componentDefinition}
|
||||
{panelDefinition}
|
||||
onChange={onPropChanged} />
|
||||
onChange={onPropChanged}
|
||||
onScreenPropChange={store.setPageOrScreenProp}
|
||||
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
|
||||
{:else if selectedCategory.value === 'events'}
|
||||
<EventsEditor component={componentInstance} />
|
||||
{/if}
|
||||
|
@ -121,8 +103,6 @@
|
|||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
@ -140,6 +120,5 @@
|
|||
margin-top: 20px;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} />
|
||||
</div>
|
||||
|
||||
<div class="positioned-wrapper">
|
||||
<div class="design-view-property-groups">
|
||||
{#if propertyGroupNames.length > 0}
|
||||
{#each propertyGroupNames as groupName}
|
||||
|
@ -49,6 +50,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.design-view-container {
|
||||
|
@ -62,10 +64,15 @@
|
|||
flex: 0 0 50px;
|
||||
}
|
||||
|
||||
.positioned-wrapper{
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.design-view-property-groups {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,11 +9,11 @@
|
|||
|
||||
const pages = [
|
||||
{
|
||||
title: "Main",
|
||||
title: "Private",
|
||||
id: "main",
|
||||
},
|
||||
{
|
||||
title: "Login",
|
||||
title: "Public",
|
||||
id: "unauthenticated",
|
||||
},
|
||||
]
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
if (v.target) {
|
||||
let val = props.valueKey ? v.target[props.valueKey] : v.target.value
|
||||
onChange(key, val)
|
||||
}else if(v.detail) {
|
||||
onChange(key, v.detail)
|
||||
} else {
|
||||
onChange(key, v)
|
||||
}
|
||||
|
|
|
@ -2,20 +2,59 @@
|
|||
import PropertyControl from "./PropertyControl.svelte"
|
||||
import InputGroup from "../common/Inputs/InputGroup.svelte"
|
||||
import Colorpicker from "../common/Colorpicker.svelte"
|
||||
import { goto } from "@sveltech/routify"
|
||||
import { excludeProps } from "./propertyCategories.js"
|
||||
import Input from "../common/Input.svelte"
|
||||
|
||||
export let panelDefinition = []
|
||||
export let componentDefinition = {}
|
||||
export let componentInstance = {}
|
||||
export let onChange = () => {}
|
||||
export let onScreenPropChange = () => {}
|
||||
export let screenOrPageInstance
|
||||
|
||||
const propExistsOnComponentDef = prop => prop in componentDefinition.props
|
||||
|
||||
function handleChange(key, data) {
|
||||
data.target ? onChange(key, data.target.value) : onChange(key, data)
|
||||
}
|
||||
|
||||
function handleScreenPropChange(name, value) {
|
||||
onScreenPropChange(name, value)
|
||||
if (!isPage && name === "name") {
|
||||
// screen name is changed... change URL
|
||||
$goto(`./:page/${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
const screenDefinition = [
|
||||
{ key: "name", label: "Name", control: Input },
|
||||
{ key: "description", label: "Description", control: Input },
|
||||
{ key: "route", label: "Route", control: Input },
|
||||
]
|
||||
|
||||
const pageDefinition = [
|
||||
{ key: "title", label: "Title", control: Input },
|
||||
{ key: "favicon", label: "Favicon", control: Input },
|
||||
]
|
||||
|
||||
$: isPage = screenOrPageInstance && screenOrPageInstance.favicon
|
||||
$: screenOrPageDefinition = isPage ? pageDefinition : screenDefinition
|
||||
</script>
|
||||
|
||||
{#if screenOrPageInstance}
|
||||
{#each screenOrPageDefinition as def}
|
||||
<PropertyControl
|
||||
control={def.control}
|
||||
label={def.label}
|
||||
key={def.key}
|
||||
value={screenOrPageInstance[def.key]}
|
||||
onChange={handleScreenPropChange}
|
||||
props={{ ...excludeProps(def, ['control', 'label']) }} />
|
||||
{/each}
|
||||
<hr />
|
||||
{/if}
|
||||
|
||||
{#if panelDefinition && panelDefinition.length > 0}
|
||||
{#each panelDefinition as definition}
|
||||
{#if propExistsOnComponentDef(definition.key)}
|
||||
|
|
|
@ -2,7 +2,7 @@ import Input from "../common/Input.svelte"
|
|||
import OptionSelect from "./OptionSelect.svelte"
|
||||
import InputGroup from "../common/Inputs/InputGroup.svelte"
|
||||
import FlatButtonGroup from "./FlatButtonGroup.svelte"
|
||||
// import Colorpicker from "../common/Colorpicker.svelte"
|
||||
import Colorpicker from "./Colorpicker"
|
||||
/*
|
||||
TODO: Allow for default values for all properties
|
||||
*/
|
||||
|
@ -256,8 +256,8 @@ export const typography = [
|
|||
{
|
||||
label: "Color",
|
||||
key: "color",
|
||||
control: Input,
|
||||
placeholder: "hex",
|
||||
control: Colorpicker,
|
||||
defaultValue: "#000",
|
||||
},
|
||||
{
|
||||
label: "align",
|
||||
|
@ -305,7 +305,8 @@ export const background = [
|
|||
{
|
||||
label: "Color",
|
||||
key: "background",
|
||||
control: Input,
|
||||
control: Colorpicker,
|
||||
defaultValue: "#000",
|
||||
},
|
||||
{
|
||||
label: "Image",
|
||||
|
@ -347,7 +348,8 @@ export const border = [
|
|||
{
|
||||
label: "Color",
|
||||
key: "border-color",
|
||||
control: Input,
|
||||
control: Colorpicker,
|
||||
defaultValue: "#000",
|
||||
},
|
||||
{
|
||||
label: "Style",
|
||||
|
|
|
@ -11,6 +11,18 @@ export default {
|
|||
name: "Basic",
|
||||
isCategory: true,
|
||||
children: [
|
||||
{
|
||||
_component: "@budibase/standard-components/embed",
|
||||
icon: "ri-code-line",
|
||||
name: "Embed",
|
||||
description: "Embed content from 3rd party sources",
|
||||
properties: {
|
||||
design: {
|
||||
...all,
|
||||
},
|
||||
settings: [{ label: "Embed", key: "embed", control: Input }],
|
||||
},
|
||||
},
|
||||
{
|
||||
_component: "@budibase/standard-components/container",
|
||||
name: "Container",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { store, backendUiStore, workflowStore } from "builderStore"
|
||||
import { notifier } from "@beyonk/svelte-notifications"
|
||||
import api from "builderStore/api"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
|
||||
export let onClosed
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import { onMount, getContext } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import { notifier } from "@beyonk/svelte-notifications"
|
||||
import api from "builderStore/api"
|
||||
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
|
||||
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import { workflowStore, backendUiStore } from "builderStore"
|
||||
import { notifier } from "@beyonk/svelte-notifications"
|
||||
import Flowchart from "./flowchart/FlowChart.svelte"
|
||||
import api from "builderStore/api"
|
||||
|
||||
let selectedWorkflow
|
||||
let uiTree
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import { WorkflowList } from "../"
|
||||
import WorkflowBlock from "./WorkflowBlock.svelte"
|
||||
import api from "builderStore/api"
|
||||
import blockDefinitions from "../blockDefinitions"
|
||||
|
||||
let selectedTab = "TRIGGER"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { store, backendUiStore, workflowStore } from "builderStore"
|
||||
import { notifier } from "@beyonk/svelte-notifications"
|
||||
import api from "builderStore/api"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
|
||||
export let onClosed
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import { notifier } from "@beyonk/svelte-notifications"
|
||||
import { onMount, getContext } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
import CreateWorkflowModal from "./CreateWorkflowModal.svelte"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
@ -23,7 +22,7 @@
|
|||
}
|
||||
|
||||
onMount(() => {
|
||||
workflowStore.actions.fetch($backendUiStore.selectedDatabase._id)
|
||||
workflowStore.actions.fetch()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { onMount } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import { WorkflowList, BlockList } from "./"
|
||||
import api from "builderStore/api"
|
||||
import blockDefinitions from "./blockDefinitions"
|
||||
|
||||
let selectedTab = "WORKFLOWS"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset='utf8'>
|
||||
<meta name='viewport' content='width=device-width'>
|
||||
|
@ -14,6 +15,7 @@
|
|||
<link rel='stylesheet' href='/_builder/budibase.css'>
|
||||
<link rel='stylesheet' href='/_builder/monokai.css'>
|
||||
<link rel='stylesheet' href='/_builder/bundle.css'>
|
||||
<link rel='stylesheet' href='/_builder/bbui.css'>
|
||||
<link rel='stylesheet' href='/_builder/fonts.css'>
|
||||
<link rel='stylesheet' href="/_builder/uikit.min.css">
|
||||
</head>
|
||||
|
@ -21,4 +23,5 @@
|
|||
<body id="app">
|
||||
<script src='/_builder/bundle.js'></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -103,7 +103,6 @@
|
|||
background-color: var(--white);
|
||||
min-height: 0px;
|
||||
height: calc(100vh - 69px);
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.nav-group-header > div:nth-child(1) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<script>
|
||||
import { params } from "@sveltech/routify"
|
||||
store.setCurrentPage($params.page)
|
||||
</script>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
"client": "web"
|
||||
}
|
||||
},
|
||||
"testURL": "http://jest-breaks-if-this-does-not-exist",
|
||||
"testURL": "http://test.com",
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js",
|
||||
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { authenticate } from "./authenticate"
|
||||
import { triggerWorkflow } from "./workflow"
|
||||
|
||||
export const createApi = ({ rootPath = "", setState, getState }) => {
|
||||
export const createApi = ({ setState, getState }) => {
|
||||
const apiCall = method => async ({ url, body }) => {
|
||||
const response = await fetch(`${rootPath}${url}`, {
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
@ -45,7 +45,6 @@ export const createApi = ({ rootPath = "", setState, getState }) => {
|
|||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
|
||||
|
||||
const apiOpts = {
|
||||
rootPath,
|
||||
setState,
|
||||
getState,
|
||||
isSuccess,
|
||||
|
|
|
@ -2,6 +2,7 @@ import { attachChildren } from "./render/attachChildren"
|
|||
import { createTreeNode } from "./render/prepareRenderComponent"
|
||||
import { screenRouter } from "./render/screenRouter"
|
||||
import { createStateManager } from "./state/stateManager"
|
||||
import { getAppId } from "./render/getAppId"
|
||||
|
||||
export const createApp = ({
|
||||
componentLibraries,
|
||||
|
@ -15,11 +16,9 @@ export const createApp = ({
|
|||
const onScreenSlotRendered = screenSlotNode => {
|
||||
const onScreenSelected = (screen, url) => {
|
||||
const stateManager = createStateManager({
|
||||
frontendDefinition,
|
||||
componentLibraries,
|
||||
onScreenSlotRendered: () => {},
|
||||
routeTo,
|
||||
appRootPath: frontendDefinition.appRootPath,
|
||||
})
|
||||
const getAttachChildrenParams = attachChildrenParams(stateManager)
|
||||
screenSlotNode.props._children = [screen.props]
|
||||
|
@ -36,10 +35,10 @@ export const createApp = ({
|
|||
routeTo = screenRouter({
|
||||
screens: frontendDefinition.screens,
|
||||
onScreenSelected,
|
||||
appRootPath: frontendDefinition.appRootPath,
|
||||
window,
|
||||
})
|
||||
const fallbackPath = window.location.pathname.replace(
|
||||
frontendDefinition.appRootPath,
|
||||
getAppId(window.document.cookie),
|
||||
""
|
||||
)
|
||||
routeTo(currentUrl || fallbackPath)
|
||||
|
@ -59,10 +58,8 @@ export const createApp = ({
|
|||
|
||||
let rootTreeNode
|
||||
const pageStateManager = createStateManager({
|
||||
frontendDefinition,
|
||||
componentLibraries,
|
||||
onScreenSlotRendered,
|
||||
appRootPath: frontendDefinition.appRootPath,
|
||||
// seems weird, but the routeTo variable may not be available at this point
|
||||
routeTo: url => routeTo(url),
|
||||
})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { createApp } from "./createApp"
|
||||
import { builtins, builtinLibName } from "./render/builtinComponents"
|
||||
import { getAppId } from "./render/getAppId"
|
||||
|
||||
/**
|
||||
* create a web application from static budibase definition files.
|
||||
|
@ -8,7 +9,7 @@ import { builtins, builtinLibName } from "./render/builtinComponents"
|
|||
export const loadBudibase = async opts => {
|
||||
const _window = (opts && opts.window) || window
|
||||
// const _localStorage = (opts && opts.localStorage) || localStorage
|
||||
|
||||
const appId = getAppId(_window.document.cookie)
|
||||
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
|
||||
|
||||
const user = {}
|
||||
|
@ -20,9 +21,7 @@ export const loadBudibase = async opts => {
|
|||
for (let library of libraries) {
|
||||
// fetch the JavaScript for the component libraries from the server
|
||||
componentLibraryModules[library] = await import(
|
||||
`/${frontendDefinition.appId}/componentlibrary?library=${encodeURI(
|
||||
library
|
||||
)}`
|
||||
`/componentlibrary?library=${encodeURI(library)}`
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -38,11 +37,11 @@ export const loadBudibase = async opts => {
|
|||
componentLibraries: componentLibraryModules,
|
||||
frontendDefinition,
|
||||
user,
|
||||
window,
|
||||
window: _window,
|
||||
})
|
||||
|
||||
const route = _window.location
|
||||
? _window.location.pathname.replace(frontendDefinition.appRootPath, "")
|
||||
? _window.location.pathname.replace(`${appId}/`, "").replace(appId, "")
|
||||
: ""
|
||||
|
||||
initialisePage(frontendDefinition.page, _window.document.body, route)
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
export const getAppId = docCookie => {
|
||||
const cookie =
|
||||
docCookie.split(";").find(c => c.trim().startsWith("budibase:token")) ||
|
||||
docCookie.split(";").find(c => c.trim().startsWith("builder:token"))
|
||||
|
||||
const base64Token = cookie.substring(lengthOfKey)
|
||||
|
||||
const user = JSON.parse(atob(base64Token.split(".")[1]))
|
||||
return user.appId
|
||||
}
|
||||
|
||||
const lengthOfKey = "budibase:token=".length
|
|
@ -1,11 +1,20 @@
|
|||
import regexparam from "regexparam"
|
||||
import { routerStore } from "../state/store"
|
||||
import { getAppId } from "./getAppId"
|
||||
|
||||
export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => {
|
||||
export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
||||
const makeRootedPath = url => {
|
||||
if (appRootPath) {
|
||||
if (url) return `${appRootPath}${url.startsWith("/") ? "" : "/"}${url}`
|
||||
return appRootPath
|
||||
if (
|
||||
window.location &&
|
||||
(window.location.hostname === "localhost" ||
|
||||
window.location.hostname === "127.0.0.1")
|
||||
) {
|
||||
const appId = getAppId(window.document.cookie)
|
||||
if (url) {
|
||||
if (url.startsWith(appId)) return url
|
||||
return `/${appId}${url.startsWith("/") ? "" : "/"}${url}`
|
||||
}
|
||||
return appId
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
@ -70,7 +79,7 @@ export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => {
|
|||
)
|
||||
return
|
||||
|
||||
const target = x.target || "_self"
|
||||
const target = (x && x.target) || "_self"
|
||||
if (!y || target !== "_self" || x.host !== location.host) return
|
||||
|
||||
e.preventDefault()
|
||||
|
|
|
@ -6,31 +6,19 @@ export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
|
|||
|
||||
export const bbFactory = ({
|
||||
store,
|
||||
frontendDefinition,
|
||||
componentLibraries,
|
||||
onScreenSlotRendered,
|
||||
}) => {
|
||||
const relativeUrl = url => {
|
||||
if (!frontendDefinition.appRootPath) return url
|
||||
if (
|
||||
url.startsWith("http:") ||
|
||||
url.startsWith("https:") ||
|
||||
url.startsWith("./")
|
||||
)
|
||||
return url
|
||||
|
||||
return frontendDefinition.appRootPath + "/" + trimSlash(url)
|
||||
}
|
||||
|
||||
const apiCall = method => (url, body) =>
|
||||
fetch(url, {
|
||||
const apiCall = method => (url, body) => {
|
||||
return fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-user-agent": "Budibase Builder",
|
||||
},
|
||||
body: body && JSON.stringify(body),
|
||||
credentials: "same-origin",
|
||||
})
|
||||
}
|
||||
|
||||
const api = {
|
||||
post: apiCall("POST"),
|
||||
|
@ -63,7 +51,6 @@ export const bbFactory = ({
|
|||
getContext: getContext(treeNode),
|
||||
setContext: setContext(treeNode),
|
||||
store: store,
|
||||
relativeUrl,
|
||||
api,
|
||||
parent,
|
||||
}
|
||||
|
|
|
@ -6,14 +6,13 @@ import { createApi } from "../api"
|
|||
|
||||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
||||
|
||||
export const eventHandlers = (rootPath, routeTo) => {
|
||||
export const eventHandlers = routeTo => {
|
||||
const handler = (parameters, execute) => ({
|
||||
execute,
|
||||
parameters,
|
||||
})
|
||||
|
||||
const api = createApi({
|
||||
rootPath,
|
||||
setState,
|
||||
getState: (path, fallback) => getState(path, fallback),
|
||||
})
|
||||
|
|
|
@ -21,13 +21,11 @@ const isMetaProp = propName =>
|
|||
propName === "_styles"
|
||||
|
||||
export const createStateManager = ({
|
||||
appRootPath,
|
||||
frontendDefinition,
|
||||
componentLibraries,
|
||||
onScreenSlotRendered,
|
||||
routeTo,
|
||||
}) => {
|
||||
let handlerTypes = eventHandlers(appRootPath, routeTo)
|
||||
let handlerTypes = eventHandlers(routeTo)
|
||||
let currentState
|
||||
|
||||
const getCurrentState = () => currentState
|
||||
|
@ -35,7 +33,6 @@ export const createStateManager = ({
|
|||
const bb = bbFactory({
|
||||
store: appStore,
|
||||
getCurrentState,
|
||||
frontendDefinition,
|
||||
componentLibraries,
|
||||
onScreenSlotRendered,
|
||||
})
|
||||
|
|
|
@ -18,7 +18,7 @@ describe("screenRouting", () => {
|
|||
|
||||
it("should load correct screen, for initial URL, when appRootPath is something", async () => {
|
||||
const { page, screens } = pageWith3Screens()
|
||||
const { dom } = await load(page, screens, "/testApp/screen2", "/testApp")
|
||||
const { dom } = await load(page, screens, "/TEST_APP_ID/screen2", "127.0.0.1")
|
||||
|
||||
const rootDiv = dom.window.document.body.children[0]
|
||||
expect(rootDiv.children.length).toBe(1)
|
||||
|
@ -50,8 +50,8 @@ describe("screenRouting", () => {
|
|||
const { dom, app } = await load(
|
||||
page,
|
||||
screens,
|
||||
"/testApp/screen2",
|
||||
"/testApp"
|
||||
"/TEST_APP_ID/screen2",
|
||||
"127.0.0.1"
|
||||
)
|
||||
|
||||
app.routeTo()("/screen3")
|
||||
|
|
|
@ -1,19 +1,33 @@
|
|||
import { JSDOM } from "jsdom"
|
||||
import jsdom, { JSDOM } from "jsdom"
|
||||
import { loadBudibase } from "../src/index"
|
||||
|
||||
export const load = async (page, screens, url, appRootPath) => {
|
||||
export const load = async (page, screens, url, host = "test.com") => {
|
||||
screens = screens || []
|
||||
url = url || "/"
|
||||
appRootPath = appRootPath || ""
|
||||
|
||||
const fullUrl = `http://${host}${url}`
|
||||
const cookieJar = new jsdom.CookieJar()
|
||||
const cookie = `${btoa("{}")}.${btoa('{"appId":"TEST_APP_ID"}')}.signature`
|
||||
cookieJar.setCookie(
|
||||
`budibase:token=${cookie};domain=${host};path=/`,
|
||||
fullUrl,
|
||||
{
|
||||
looseMode: false,
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
|
||||
const dom = new JSDOM("<!DOCTYPE html><html><body></body><html>", {
|
||||
url: `http://test${url}`,
|
||||
url: fullUrl,
|
||||
cookieJar,
|
||||
})
|
||||
|
||||
autoAssignIds(page.props)
|
||||
for (let s of screens) {
|
||||
autoAssignIds(s.props)
|
||||
}
|
||||
setAppDef(dom.window, page, screens)
|
||||
addWindowGlobals(dom.window, page, screens, appRootPath, {
|
||||
addWindowGlobals(dom.window, page, screens, {
|
||||
hierarchy: {},
|
||||
actions: [],
|
||||
triggers: [],
|
||||
|
@ -27,11 +41,10 @@ export const load = async (page, screens, url, appRootPath) => {
|
|||
return { dom, app }
|
||||
}
|
||||
|
||||
const addWindowGlobals = (window, page, screens, appRootPath) => {
|
||||
const addWindowGlobals = (window, page, screens) => {
|
||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
||||
page,
|
||||
screens,
|
||||
appRootPath,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,7 +101,6 @@ const setAppDef = (window, page, screens) => {
|
|||
componentLibraries: [],
|
||||
page,
|
||||
screens,
|
||||
appRootPath: "",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,12 +14,6 @@ export default async () => {
|
|||
componentLibraries["@budibase/standard-components"] = standardcomponents
|
||||
const appDef = { hierarchy: {}, actions: {} }
|
||||
const user = { name: "yeo", permissions: [] }
|
||||
const { initialisePage } = createApp(
|
||||
componentLibraries,
|
||||
{ appRootPath: "" },
|
||||
appDef,
|
||||
user,
|
||||
{}
|
||||
)
|
||||
const { initialisePage } = createApp(componentLibraries, {}, appDef, user, {})
|
||||
return initialisePage
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
FROM node:12-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# copy files and install dependencies
|
||||
COPY . ./
|
||||
RUN yarn
|
||||
|
||||
EXPOSE 4001
|
||||
|
||||
CMD ["yarn", "run:docker"]
|
|
@ -25,8 +25,9 @@
|
|||
"scripts": {
|
||||
"test": "jest routes --runInBand",
|
||||
"test:integration": "jest workflow --runInBand",
|
||||
"test:watch": "jest -w",
|
||||
"test:watch": "jest --watch",
|
||||
"initialise": "node ../cli/bin/budi init -b local -q",
|
||||
"run:docker": "node src/index",
|
||||
"budi": "node ../cli/bin/budi",
|
||||
"dev:builder": "nodemon ../cli/bin/budi run",
|
||||
"electron": "electron src/electron.js",
|
||||
|
|
|
@ -8,7 +8,7 @@ const {
|
|||
} = require("../../utilities/accessLevels")
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const body = await db.query("database/by_type", {
|
||||
include_docs: true,
|
||||
key: ["accesslevel"],
|
||||
|
@ -19,12 +19,12 @@ exports.fetch = async function(ctx) {
|
|||
{
|
||||
_id: ADMIN_LEVEL_ID,
|
||||
name: "Admin",
|
||||
permissions: await generateAdminPermissions(ctx.params.instanceId),
|
||||
permissions: await generateAdminPermissions(ctx.user.instanceId),
|
||||
},
|
||||
{
|
||||
_id: POWERUSER_LEVEL_ID,
|
||||
name: "Power User",
|
||||
permissions: await generatePowerUserPermissions(ctx.params.instanceId),
|
||||
permissions: await generatePowerUserPermissions(ctx.user.instanceId),
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -32,12 +32,12 @@ exports.fetch = async function(ctx) {
|
|||
}
|
||||
|
||||
exports.find = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
ctx.body = await db.get(ctx.params.levelId)
|
||||
}
|
||||
|
||||
exports.update = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const level = await db.get(ctx.params.levelId)
|
||||
level.name = ctx.body.name
|
||||
level.permissions = ctx.request.body.permissions
|
||||
|
@ -48,7 +48,7 @@ exports.update = async function(ctx) {
|
|||
}
|
||||
|
||||
exports.patch = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const level = await db.get(ctx.params.levelId)
|
||||
const { removedPermissions, addedPermissions, _rev } = ctx.request.body
|
||||
|
||||
|
@ -84,7 +84,7 @@ exports.patch = async function(ctx) {
|
|||
}
|
||||
|
||||
exports.create = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
|
||||
const level = {
|
||||
name: ctx.request.body.name,
|
||||
|
@ -101,7 +101,7 @@ exports.create = async function(ctx) {
|
|||
}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
await db.remove(ctx.params.levelId, ctx.params.rev)
|
||||
ctx.message = `Access Level ${ctx.params.id} deleted successfully`
|
||||
ctx.status = 200
|
||||
|
|
|
@ -9,6 +9,7 @@ const { copy, exists, readFile, writeFile } = require("fs-extra")
|
|||
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
|
||||
const { exec } = require("child_process")
|
||||
const sqrl = require("squirrelly")
|
||||
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
const db = new CouchDB(ClientDb.name(getClientId(ctx)))
|
||||
|
@ -25,6 +26,14 @@ exports.fetchAppPackage = async function(ctx) {
|
|||
const db = new CouchDB(ClientDb.name(clientId))
|
||||
const application = await db.get(ctx.params.applicationId)
|
||||
ctx.body = await getPackageForBuilder(ctx.config, application)
|
||||
/*
|
||||
instance is hardcoded now - this can only change when we move
|
||||
pages and screens into the database
|
||||
*/
|
||||
const devInstance = application.instances.find(
|
||||
i => i.name === `dev-${clientId}`
|
||||
)
|
||||
setBuilderToken(ctx, ctx.params.applicationId, devInstance._id)
|
||||
}
|
||||
|
||||
exports.create = async function(ctx) {
|
||||
|
@ -37,10 +46,12 @@ exports.create = async function(ctx) {
|
|||
const appId = newid()
|
||||
// insert an appId -> clientId lookup
|
||||
const masterDb = new CouchDB("clientAppLookup")
|
||||
|
||||
await masterDb.put({
|
||||
_id: appId,
|
||||
clientId,
|
||||
})
|
||||
|
||||
const db = new CouchDB(ClientDb.name(clientId))
|
||||
|
||||
const newApplication = {
|
||||
|
@ -56,18 +67,18 @@ exports.create = async function(ctx) {
|
|||
description: ctx.request.body.description,
|
||||
}
|
||||
|
||||
const { rev } = await db.post(newApplication)
|
||||
const { rev } = await db.put(newApplication)
|
||||
newApplication._rev = rev
|
||||
|
||||
const createInstCtx = {
|
||||
params: {
|
||||
applicationId: newApplication._id,
|
||||
user: {
|
||||
appId: newApplication._id,
|
||||
},
|
||||
request: {
|
||||
body: { name: `dev-${clientId}` },
|
||||
},
|
||||
}
|
||||
await instanceController.create(createInstCtx)
|
||||
newApplication.instances.push(createInstCtx.body)
|
||||
|
||||
if (ctx.isDev) {
|
||||
const newAppFolder = await createEmptyAppPackage(ctx, newApplication)
|
||||
|
|
|
@ -4,25 +4,31 @@ const ClientDb = require("../../db/clientDb")
|
|||
const bcrypt = require("../../utilities/bcrypt")
|
||||
|
||||
exports.authenticate = async ctx => {
|
||||
if (!ctx.user.appId) ctx.throw(400, "No appId")
|
||||
|
||||
const { username, password } = ctx.request.body
|
||||
|
||||
if (!username) ctx.throw(400, "Username Required.")
|
||||
if (!password) ctx.throw(400, "Password Required")
|
||||
|
||||
const masterDb = new CouchDB("clientAppLookup")
|
||||
const { clientId } = await masterDb.get(ctx.params.appId)
|
||||
|
||||
const { clientId } = await masterDb.get(ctx.user.appId)
|
||||
|
||||
if (!clientId) {
|
||||
ctx.throw(400, "ClientId not suplied")
|
||||
}
|
||||
// find the instance that the user is associated with
|
||||
const db = new CouchDB(ClientDb.name(clientId))
|
||||
const appId = ctx.params.appId
|
||||
const app = await db.get(appId)
|
||||
const app = await db.get(ctx.user.appId)
|
||||
const instanceId = app.userInstanceMap[username]
|
||||
|
||||
if (!instanceId)
|
||||
ctx.throw(500, "User is not associated with an instance of app", appId)
|
||||
ctx.throw(
|
||||
500,
|
||||
"User is not associated with an instance of app",
|
||||
ctx.user.appId
|
||||
)
|
||||
|
||||
// Check the user exists in the instance DB by username
|
||||
const instanceDb = new CouchDB(instanceId)
|
||||
|
@ -41,16 +47,22 @@ exports.authenticate = async ctx => {
|
|||
const payload = {
|
||||
userId: dbUser._id,
|
||||
accessLevelId: dbUser.accessLevelId,
|
||||
instanceId: instanceId,
|
||||
appId: ctx.user.appId,
|
||||
instanceId,
|
||||
}
|
||||
|
||||
const token = jwt.sign(payload, ctx.config.jwtSecret, {
|
||||
expiresIn: "1 day",
|
||||
})
|
||||
|
||||
const ONE_DAY_FROM_NOW = new Date(Date.now() + 24 * 3600)
|
||||
const expires = new Date()
|
||||
expires.setDate(expires.getDate() + 1)
|
||||
|
||||
ctx.cookies.set("budibase:token", token, { expires: ONE_DAY_FROM_NOW })
|
||||
ctx.cookies.set("budibase:token", token, {
|
||||
expires,
|
||||
path: "/",
|
||||
httpOnly: false,
|
||||
})
|
||||
|
||||
ctx.body = {
|
||||
token,
|
||||
|
|
|
@ -4,19 +4,19 @@ const newid = require("../../db/newid")
|
|||
|
||||
exports.create = async function(ctx) {
|
||||
const instanceName = ctx.request.body.name
|
||||
const appShortId = ctx.params.applicationId.substring(0, 7)
|
||||
const { appId } = ctx.user
|
||||
const appShortId = appId.substring(0, 7)
|
||||
const instanceId = `inst_${appShortId}_${newid()}`
|
||||
const { applicationId } = ctx.params
|
||||
|
||||
const masterDb = new CouchDB("clientAppLookup")
|
||||
const { clientId } = await masterDb.get(applicationId)
|
||||
const { clientId } = await masterDb.get(appId)
|
||||
|
||||
const db = new CouchDB(instanceId)
|
||||
await db.put({
|
||||
_id: "_design/database",
|
||||
metadata: {
|
||||
clientId,
|
||||
applicationId,
|
||||
applicationId: appId,
|
||||
},
|
||||
views: {
|
||||
by_username: {
|
||||
|
@ -46,7 +46,7 @@ exports.create = async function(ctx) {
|
|||
|
||||
// Add the new instance under the app clientDB
|
||||
const clientDb = new CouchDB(client.name(clientId))
|
||||
const budibaseApp = await clientDb.get(applicationId)
|
||||
const budibaseApp = await clientDb.get(appId)
|
||||
const instance = { _id: instanceId, name: instanceName }
|
||||
budibaseApp.instances.push(instance)
|
||||
await clientDb.put(budibaseApp)
|
||||
|
|
|
@ -2,7 +2,7 @@ const CouchDB = require("../../db")
|
|||
const newid = require("../../db/newid")
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const body = await db.query("database/by_type", {
|
||||
include_docs: true,
|
||||
key: ["model"],
|
||||
|
@ -11,13 +11,13 @@ exports.fetch = async function(ctx) {
|
|||
}
|
||||
|
||||
exports.find = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const model = await db.get(ctx.params.id)
|
||||
ctx.body = model
|
||||
}
|
||||
|
||||
exports.create = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const newModel = {
|
||||
type: "model",
|
||||
...ctx.request.body,
|
||||
|
@ -27,6 +27,23 @@ exports.create = async function(ctx) {
|
|||
const result = await db.post(newModel)
|
||||
newModel._rev = result.rev
|
||||
|
||||
const { schema } = ctx.request.body
|
||||
for (let key in schema) {
|
||||
// model has a linked record
|
||||
if (schema[key].type === "link") {
|
||||
// create the link field in the other model
|
||||
const linkedModel = await db.get(schema[key].modelId)
|
||||
linkedModel.schema[newModel.name] = {
|
||||
type: "link",
|
||||
modelId: newModel._id,
|
||||
constraints: {
|
||||
type: "array",
|
||||
},
|
||||
}
|
||||
await db.put(linkedModel)
|
||||
}
|
||||
}
|
||||
|
||||
const designDoc = await db.get("_design/database")
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
|
@ -48,9 +65,12 @@ exports.create = async function(ctx) {
|
|||
exports.update = async function() {}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
|
||||
const modelToDelete = await db.get(ctx.params.modelId)
|
||||
|
||||
await db.remove(modelToDelete)
|
||||
|
||||
await db.remove(ctx.params.modelId, ctx.params.revId)
|
||||
const modelViewId = `all_${ctx.params.modelId}`
|
||||
|
||||
// Delete all records for that model
|
||||
|
@ -59,6 +79,16 @@ exports.destroy = async function(ctx) {
|
|||
records.rows.map(record => ({ id: record.id, _deleted: true }))
|
||||
)
|
||||
|
||||
// Delete linked record fields in dependent models
|
||||
for (let key in modelToDelete.schema) {
|
||||
const { type, modelId } = modelToDelete.schema[key]
|
||||
if (type === "link") {
|
||||
const linkedModel = await db.get(modelId)
|
||||
delete linkedModel.schema[modelToDelete.name]
|
||||
await db.put(linkedModel)
|
||||
}
|
||||
}
|
||||
|
||||
// delete the "all" view
|
||||
const designDoc = await db.get("_design/database")
|
||||
delete designDoc.views[modelViewId]
|
||||
|
|
|
@ -3,7 +3,7 @@ const validateJs = require("validate.js")
|
|||
const newid = require("../../db/newid")
|
||||
|
||||
exports.save = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const record = ctx.request.body
|
||||
record.modelId = ctx.params.modelId
|
||||
|
||||
|
@ -46,7 +46,7 @@ exports.save = async function(ctx) {
|
|||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emit(`record:save`, {
|
||||
record,
|
||||
instanceId: ctx.params.instanceId,
|
||||
instanceId: ctx.user.instanceId,
|
||||
})
|
||||
ctx.body = record
|
||||
ctx.status = 200
|
||||
|
@ -54,23 +54,32 @@ exports.save = async function(ctx) {
|
|||
}
|
||||
|
||||
exports.fetchView = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const response = await db.query(`database/${ctx.params.viewName}`, {
|
||||
include_docs: true,
|
||||
})
|
||||
ctx.body = response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
exports.fetchModel = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
exports.fetchModelRecords = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const response = await db.query(`database/all_${ctx.params.modelId}`, {
|
||||
include_docs: true,
|
||||
})
|
||||
ctx.body = response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
exports.search = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const response = await db.allDocs({
|
||||
include_docs: true,
|
||||
...ctx.request.body,
|
||||
})
|
||||
ctx.body = response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
exports.find = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const record = await db.get(ctx.params.recordId)
|
||||
if (record.modelId !== ctx.params.modelId) {
|
||||
ctx.throw(400, "Supplied modelId doe not match the record's modelId")
|
||||
|
@ -80,7 +89,7 @@ exports.find = async function(ctx) {
|
|||
}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const record = await db.get(ctx.params.recordId)
|
||||
if (record.modelId !== ctx.params.modelId) {
|
||||
ctx.throw(400, "Supplied modelId doe not match the record's modelId")
|
||||
|
@ -92,7 +101,7 @@ exports.destroy = async function(ctx) {
|
|||
|
||||
exports.validate = async function(ctx) {
|
||||
const errors = await validate({
|
||||
instanceId: ctx.params.instanceId,
|
||||
instanceId: ctx.user.instanceId,
|
||||
modelId: ctx.params.modelId,
|
||||
record: ctx.request.body,
|
||||
})
|
||||
|
|
|
@ -4,11 +4,15 @@ const {
|
|||
budibaseAppsDir,
|
||||
budibaseTempDir,
|
||||
} = require("../../utilities/budibaseDir")
|
||||
const env = require("../../environment")
|
||||
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
|
||||
const { ANON_LEVEL_ID } = require("../../utilities/accessLevels")
|
||||
const jwt = require("jsonwebtoken")
|
||||
|
||||
exports.serveBuilder = async function(ctx) {
|
||||
let builderPath = resolve(__dirname, "../../../builder")
|
||||
ctx.cookies.set("builder:token", env.ADMIN_SECRET)
|
||||
if (ctx.file === "index.html") {
|
||||
setBuilderToken(ctx)
|
||||
}
|
||||
await send(ctx, ctx.file, { root: ctx.devPath || builderPath })
|
||||
}
|
||||
|
||||
|
@ -20,6 +24,33 @@ exports.serveApp = async function(ctx) {
|
|||
"public",
|
||||
ctx.isAuthenticated ? "main" : "unauthenticated"
|
||||
)
|
||||
// only set the appId cookie for /appId .. we COULD check for valid appIds
|
||||
// but would like to avoid that DB hit
|
||||
const looksLikeAppId = /^[0-9a-f]{32}$/.test(ctx.params.appId)
|
||||
if (looksLikeAppId && !ctx.isAuthenticated) {
|
||||
const anonUser = {
|
||||
userId: "ANON",
|
||||
accessLevelId: ANON_LEVEL_ID,
|
||||
appId: ctx.params.appId,
|
||||
}
|
||||
const anonToken = jwt.sign(anonUser, ctx.config.jwtSecret)
|
||||
ctx.cookies.set("budibase:token", anonToken, {
|
||||
path: "/",
|
||||
httpOnly: false,
|
||||
})
|
||||
}
|
||||
|
||||
await send(ctx, ctx.file || "index.html", { root: ctx.devPath || appPath })
|
||||
}
|
||||
|
||||
exports.serveAppAsset = async function(ctx) {
|
||||
// default to homedir
|
||||
const appPath = resolve(
|
||||
budibaseAppsDir(),
|
||||
ctx.user.appId,
|
||||
"public",
|
||||
ctx.isAuthenticated ? "main" : "unauthenticated"
|
||||
)
|
||||
|
||||
await send(ctx, ctx.file, { root: ctx.devPath || appPath })
|
||||
}
|
||||
|
@ -28,7 +59,7 @@ exports.serveComponentLibrary = async function(ctx) {
|
|||
// default to homedir
|
||||
let componentLibraryPath = resolve(
|
||||
budibaseAppsDir(),
|
||||
ctx.params.appId,
|
||||
ctx.user.appId,
|
||||
"node_modules",
|
||||
decodeURI(ctx.query.library),
|
||||
"dist"
|
||||
|
|
|
@ -8,7 +8,7 @@ const {
|
|||
} = require("../../utilities/accessLevels")
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
const database = new CouchDB(ctx.params.instanceId)
|
||||
const database = new CouchDB(ctx.user.instanceId)
|
||||
const data = await database.query("database/by_type", {
|
||||
include_docs: true,
|
||||
key: ["user"],
|
||||
|
@ -18,7 +18,7 @@ exports.fetch = async function(ctx) {
|
|||
}
|
||||
|
||||
exports.create = async function(ctx) {
|
||||
const database = new CouchDB(ctx.params.instanceId)
|
||||
const database = new CouchDB(ctx.user.instanceId)
|
||||
const appId = (await database.get("_design/database")).metadata.applicationId
|
||||
const { username, password, name, accessLevelId } = ctx.request.body
|
||||
|
||||
|
@ -50,7 +50,7 @@ exports.create = async function(ctx) {
|
|||
|
||||
app.userInstanceMap = {
|
||||
...app.userInstanceMap,
|
||||
[username]: ctx.params.instanceId,
|
||||
[username]: ctx.user.instanceId,
|
||||
}
|
||||
await db.put(app)
|
||||
|
||||
|
@ -66,14 +66,14 @@ exports.create = async function(ctx) {
|
|||
exports.update = async function() {}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const database = new CouchDB(ctx.params.instanceId)
|
||||
const database = new CouchDB(ctx.user.instanceId)
|
||||
await database.destroy(getUserId(ctx.params.username))
|
||||
ctx.message = `User ${ctx.params.username} deleted.`
|
||||
ctx.status = 200
|
||||
}
|
||||
|
||||
exports.find = async function(ctx) {
|
||||
const database = new CouchDB(ctx.params.instanceId)
|
||||
const database = new CouchDB(ctx.user.instanceId)
|
||||
const user = await database.get(getUserId(ctx.params.username))
|
||||
ctx.body = {
|
||||
username: user.username,
|
||||
|
|
|
@ -3,7 +3,7 @@ const CouchDB = require("../../db")
|
|||
const controller = {
|
||||
query: async () => {},
|
||||
fetch: async ctx => {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const designDoc = await db.get("_design/database")
|
||||
const response = []
|
||||
|
||||
|
@ -24,7 +24,7 @@ const controller = {
|
|||
ctx.body = response
|
||||
},
|
||||
create: async ctx => {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const newView = ctx.request.body
|
||||
|
||||
const designDoc = await db.get("_design/database")
|
||||
|
@ -38,7 +38,7 @@ const controller = {
|
|||
ctx.message = `View ${newView.name} created successfully.`
|
||||
},
|
||||
destroy: async ctx => {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
ctx.body = await db.destroy(ctx.params.userId)
|
||||
},
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ const CouchDB = require("../../../db")
|
|||
const newid = require("../../../db/newid")
|
||||
|
||||
exports.create = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const workflow = ctx.request.body
|
||||
|
||||
workflow._id = newid()
|
||||
|
@ -22,7 +22,7 @@ exports.create = async function(ctx) {
|
|||
}
|
||||
|
||||
exports.update = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const workflow = ctx.request.body
|
||||
|
||||
const response = await db.put(workflow)
|
||||
|
@ -40,7 +40,7 @@ exports.update = async function(ctx) {
|
|||
}
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const response = await db.query(`database/by_type`, {
|
||||
key: ["workflow"],
|
||||
include_docs: true,
|
||||
|
@ -69,6 +69,6 @@ exports.fetchActionScript = async function(ctx) {
|
|||
}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ const {
|
|||
instanceRoutes,
|
||||
clientRoutes,
|
||||
applicationRoutes,
|
||||
recordRoutes,
|
||||
modelRoutes,
|
||||
viewRoutes,
|
||||
staticRoutes,
|
||||
|
@ -69,6 +70,9 @@ router.use(viewRoutes.allowedMethods())
|
|||
router.use(modelRoutes.routes())
|
||||
router.use(modelRoutes.allowedMethods())
|
||||
|
||||
router.use(recordRoutes.routes())
|
||||
router.use(recordRoutes.allowedMethods())
|
||||
|
||||
router.use(userRoutes.routes())
|
||||
router.use(userRoutes.allowedMethods())
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@ const controller = require("../controllers/accesslevel")
|
|||
const router = Router()
|
||||
|
||||
router
|
||||
.post("/api/:instanceId/accesslevels", controller.create)
|
||||
.put("/api/:instanceId/accesslevels", controller.update)
|
||||
.get("/api/:instanceId/accesslevels", controller.fetch)
|
||||
.get("/api/:instanceId/accesslevels/:levelId", controller.find)
|
||||
.delete("/api/:instanceId/accesslevels/:levelId/:rev", controller.destroy)
|
||||
.patch("/api/:instanceId/accesslevels/:levelId", controller.patch)
|
||||
.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)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -3,6 +3,6 @@ const controller = require("../controllers/auth")
|
|||
|
||||
const router = Router()
|
||||
|
||||
router.post("/:appId/api/authenticate", controller.authenticate)
|
||||
router.post("/api/authenticate", controller.authenticate)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -5,6 +5,7 @@ const instanceRoutes = require("./instance")
|
|||
const clientRoutes = require("./client")
|
||||
const applicationRoutes = require("./application")
|
||||
const modelRoutes = require("./model")
|
||||
const recordRoutes = require("./record")
|
||||
const viewRoutes = require("./view")
|
||||
const staticRoutes = require("./static")
|
||||
const componentRoutes = require("./component")
|
||||
|
@ -18,6 +19,7 @@ module.exports = {
|
|||
instanceRoutes,
|
||||
clientRoutes,
|
||||
applicationRoutes,
|
||||
recordRoutes,
|
||||
modelRoutes,
|
||||
viewRoutes,
|
||||
staticRoutes,
|
||||
|
|
|
@ -6,7 +6,7 @@ const { BUILDER } = require("../../utilities/accessLevels")
|
|||
const router = Router()
|
||||
|
||||
router
|
||||
.post("/api/:applicationId/instances", authorized(BUILDER), controller.create)
|
||||
.post("/api/instances", authorized(BUILDER), controller.create)
|
||||
.delete("/api/instances/:instanceId", authorized(BUILDER), controller.destroy)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -1,53 +1,21 @@
|
|||
const Router = require("@koa/router")
|
||||
const modelController = require("../controllers/model")
|
||||
const recordController = require("../controllers/record")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const {
|
||||
READ_MODEL,
|
||||
WRITE_MODEL,
|
||||
BUILDER,
|
||||
} = require("../../utilities/accessLevels")
|
||||
const { BUILDER, READ_MODEL } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
// records
|
||||
|
||||
router
|
||||
.get("/api/models", authorized(BUILDER), modelController.fetch)
|
||||
.get(
|
||||
"/api/:instanceId/:modelId/records",
|
||||
authorized(READ_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.fetchModel
|
||||
"/api/models/:id",
|
||||
authorized(READ_MODEL, ctx => ctx.params.id),
|
||||
modelController.find
|
||||
)
|
||||
.get(
|
||||
"/api/:instanceId/:modelId/records/:recordId",
|
||||
authorized(READ_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.find
|
||||
)
|
||||
.post(
|
||||
"/api/:instanceId/:modelId/records",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.save
|
||||
)
|
||||
.post(
|
||||
"/api/:instanceId/:modelId/records/validate",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.validate
|
||||
)
|
||||
.delete(
|
||||
"/api/:instanceId/:modelId/records/:recordId/:revId",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.destroy
|
||||
)
|
||||
|
||||
// models
|
||||
|
||||
router
|
||||
.get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch)
|
||||
.get("/api/:instanceId/models/:id", authorized(BUILDER), modelController.find)
|
||||
.post("/api/:instanceId/models", authorized(BUILDER), modelController.create)
|
||||
.post("/api/models", authorized(BUILDER), modelController.create)
|
||||
// .patch("/api/:instanceId/models", controller.update)
|
||||
.delete(
|
||||
"/api/:instanceId/models/:modelId/:revId",
|
||||
"/api/models/:modelId/:revId",
|
||||
authorized(BUILDER),
|
||||
modelController.destroy
|
||||
)
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
const Router = require("@koa/router")
|
||||
const recordController = require("../controllers/record")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { READ_MODEL, WRITE_MODEL } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get(
|
||||
"/api/:modelId/records",
|
||||
authorized(READ_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.fetchModelRecords
|
||||
)
|
||||
.get(
|
||||
"/api/:modelId/records/:recordId",
|
||||
authorized(READ_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.find
|
||||
)
|
||||
.post("/api/records/search", recordController.search)
|
||||
.post(
|
||||
"/api/:modelId/records",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.save
|
||||
)
|
||||
.post(
|
||||
"/api/:modelId/records/validate",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.validate
|
||||
)
|
||||
.delete(
|
||||
"/api/:modelId/records/:recordId/:revId",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.destroy
|
||||
)
|
||||
|
||||
module.exports = router
|
|
@ -6,12 +6,8 @@ const { BUILDER } = require("../../utilities/accessLevels")
|
|||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/:instanceId/screens", authorized(BUILDER), controller.fetch)
|
||||
.post("/api/:instanceId/screens", authorized(BUILDER), controller.save)
|
||||
.delete(
|
||||
"/api/:instanceId/:screenId/:revId",
|
||||
authorized(BUILDER),
|
||||
controller.destroy
|
||||
)
|
||||
.get("/api/screens", authorized(BUILDER), controller.fetch)
|
||||
.post("/api/screens", authorized(BUILDER), controller.save)
|
||||
.delete("/api/:screenId/:revId", authorized(BUILDER), controller.destroy)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -21,7 +21,8 @@ if (env.NODE_ENV !== "production") {
|
|||
}
|
||||
|
||||
router
|
||||
.get("/:appId/componentlibrary", controller.serveComponentLibrary)
|
||||
.get("/:appId/:file*", controller.serveApp)
|
||||
.get("/componentlibrary", controller.serveComponentLibrary)
|
||||
.get("/assets/:file*", controller.serveAppAsset)
|
||||
.get("/:appId/:path*", controller.serveApp)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -36,17 +36,17 @@ describe("/accesslevels", () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
instanceId = (await createInstance(request, appId))._id
|
||||
model = await createModel(request, instanceId)
|
||||
view = await createView(request, instanceId)
|
||||
model = await createModel(request, appId, instanceId)
|
||||
view = await createView(request, appId, instanceId)
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
|
||||
it("returns a success message when level is successfully created", async () => {
|
||||
const res = await request
|
||||
.post(`/api/${instanceId}/accesslevels`)
|
||||
.post(`/api/accesslevels`)
|
||||
.send({ name: "user" })
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -62,17 +62,17 @@ describe("/accesslevels", () => {
|
|||
|
||||
it("should list custom levels, plus 2 default levels", async () => {
|
||||
const createRes = await request
|
||||
.post(`/api/${instanceId}/accesslevels`)
|
||||
.post(`/api/accesslevels`)
|
||||
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] })
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const customLevel = createRes.body
|
||||
|
||||
const res = await request
|
||||
.get(`/api/${instanceId}/accesslevels`)
|
||||
.set(defaultHeaders)
|
||||
.get(`/api/accesslevels`)
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -95,22 +95,22 @@ describe("/accesslevels", () => {
|
|||
describe("destroy", () => {
|
||||
it("should delete custom access level", async () => {
|
||||
const createRes = await request
|
||||
.post(`/api/${instanceId}/accesslevels`)
|
||||
.post(`/api/accesslevels`)
|
||||
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL } ] })
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const customLevel = createRes.body
|
||||
|
||||
await request
|
||||
.delete(`/api/${instanceId}/accesslevels/${customLevel._id}/${customLevel._rev}`)
|
||||
.set(defaultHeaders)
|
||||
.delete(`/api/accesslevels/${customLevel._id}/${customLevel._rev}`)
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect(200)
|
||||
|
||||
await request
|
||||
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`)
|
||||
.set(defaultHeaders)
|
||||
.get(`/api/accesslevels/${customLevel._id}`)
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect(404)
|
||||
})
|
||||
})
|
||||
|
@ -118,27 +118,27 @@ describe("/accesslevels", () => {
|
|||
describe("patch", () => {
|
||||
it("should add given permissions", async () => {
|
||||
const createRes = await request
|
||||
.post(`/api/${instanceId}/accesslevels`)
|
||||
.post(`/api/accesslevels`)
|
||||
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] })
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const customLevel = createRes.body
|
||||
|
||||
await request
|
||||
.patch(`/api/${instanceId}/accesslevels/${customLevel._id}`)
|
||||
.patch(`/api/accesslevels/${customLevel._id}`)
|
||||
.send({
|
||||
_rev: customLevel._rev,
|
||||
addedPermissions: [ { itemId: model._id, name: WRITE_MODEL } ]
|
||||
})
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const finalRes = await request
|
||||
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`)
|
||||
.set(defaultHeaders)
|
||||
.get(`/api/accesslevels/${customLevel._id}`)
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect(200)
|
||||
|
||||
expect(finalRes.body.permissions.length).toBe(2)
|
||||
|
@ -148,7 +148,7 @@ describe("/accesslevels", () => {
|
|||
|
||||
it("should remove given permissions", async () => {
|
||||
const createRes = await request
|
||||
.post(`/api/${instanceId}/accesslevels`)
|
||||
.post(`/api/accesslevels`)
|
||||
.send({
|
||||
name: "user",
|
||||
permissions: [
|
||||
|
@ -156,25 +156,25 @@ describe("/accesslevels", () => {
|
|||
{ itemId: model._id, name: WRITE_MODEL },
|
||||
]
|
||||
})
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const customLevel = createRes.body
|
||||
|
||||
await request
|
||||
.patch(`/api/${instanceId}/accesslevels/${customLevel._id}`)
|
||||
.patch(`/api/accesslevels/${customLevel._id}`)
|
||||
.send({
|
||||
_rev: customLevel._rev,
|
||||
removedPermissions: [ { itemId: model._id, name: WRITE_MODEL }]
|
||||
})
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const finalRes = await request
|
||||
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`)
|
||||
.set(defaultHeaders)
|
||||
.get(`/api/accesslevels/${customLevel._id}`)
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect(200)
|
||||
|
||||
expect(finalRes.body.permissions.length).toBe(1)
|
||||
|
|
|
@ -34,7 +34,7 @@ describe("/applications", () => {
|
|||
const res = await request
|
||||
.post("/api/applications")
|
||||
.send({ name: "My App" })
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
expect(res.res.statusMessage).toEqual("Application My App created successfully")
|
||||
|
@ -49,6 +49,7 @@ describe("/applications", () => {
|
|||
method: "POST",
|
||||
url: `/api/applications`,
|
||||
instanceId: instance._id,
|
||||
appId: otherApplication._id,
|
||||
body: { name: "My App" }
|
||||
})
|
||||
})
|
||||
|
@ -63,7 +64,7 @@ describe("/applications", () => {
|
|||
|
||||
const res = await request
|
||||
.get("/api/applications")
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -77,13 +78,13 @@ describe("/applications", () => {
|
|||
const blah = await request
|
||||
.post("/api/applications")
|
||||
.send({ name: "app2", clientId: "new_client"})
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
//.expect(200)
|
||||
|
||||
const client1Res = await request
|
||||
.get(`/api/applications?clientId=${TEST_CLIENT_ID}`)
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -92,7 +93,7 @@ describe("/applications", () => {
|
|||
|
||||
const client2Res = await request
|
||||
.get(`/api/applications?clientId=new_client`)
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -109,6 +110,7 @@ describe("/applications", () => {
|
|||
method: "GET",
|
||||
url: `/api/applications`,
|
||||
instanceId: instance._id,
|
||||
appId: otherApplication._id,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -4,8 +4,12 @@ const supertest = require("supertest")
|
|||
const app = require("../../../app")
|
||||
const {
|
||||
POWERUSER_LEVEL_ID,
|
||||
ANON_LEVEL_ID,
|
||||
BUILDER_LEVEL_ID,
|
||||
generateAdminPermissions,
|
||||
} = require("../../../utilities/accessLevels")
|
||||
const jwt = require("jsonwebtoken")
|
||||
const env = require("../../../environment")
|
||||
|
||||
const TEST_CLIENT_ID = "test-client-id"
|
||||
|
||||
|
@ -20,13 +24,23 @@ exports.supertest = async () => {
|
|||
return { request, server }
|
||||
}
|
||||
|
||||
exports.defaultHeaders = {
|
||||
exports.defaultHeaders = (appId, instanceId) => {
|
||||
const builderUser = {
|
||||
userId: "BUILDER",
|
||||
accessLevelId: BUILDER_LEVEL_ID,
|
||||
appId,
|
||||
instanceId,
|
||||
}
|
||||
|
||||
const builderToken = jwt.sign(builderUser, env.JWT_SECRET)
|
||||
|
||||
return {
|
||||
Accept: "application/json",
|
||||
Cookie: ["builder:token=test-admin-secret"],
|
||||
"x-user-agent": "Budibase Builder",
|
||||
Cookie: [`builder:token=${builderToken}`],
|
||||
}
|
||||
}
|
||||
|
||||
exports.createModel = async (request, instanceId, model) => {
|
||||
exports.createModel = async (request, appId, instanceId, model) => {
|
||||
model = model || {
|
||||
name: "TestModel",
|
||||
type: "model",
|
||||
|
@ -42,20 +56,20 @@ exports.createModel = async (request, instanceId, model) => {
|
|||
}
|
||||
|
||||
const res = await request
|
||||
.post(`/api/${instanceId}/models`)
|
||||
.set(exports.defaultHeaders)
|
||||
.post(`/api/models`)
|
||||
.set(exports.defaultHeaders(appId, instanceId))
|
||||
.send(model)
|
||||
return res.body
|
||||
}
|
||||
|
||||
exports.createView = async (request, instanceId, view) => {
|
||||
exports.createView = async (request, appId, instanceId, view) => {
|
||||
view = view || {
|
||||
map: "function(doc) { emit(doc[doc.key], doc._id); } ",
|
||||
}
|
||||
|
||||
const res = await request
|
||||
.post(`/api/${instanceId}/views`)
|
||||
.set(exports.defaultHeaders)
|
||||
.post(`/api/views`)
|
||||
.set(exports.defaultHeaders(appId, instanceId))
|
||||
.send(view)
|
||||
return res.body
|
||||
}
|
||||
|
@ -65,10 +79,10 @@ exports.createClientDatabase = async id => await create(id || TEST_CLIENT_ID)
|
|||
exports.createApplication = async (request, name = "test_application") => {
|
||||
const res = await request
|
||||
.post("/api/applications")
|
||||
.set(exports.defaultHeaders)
|
||||
.send({
|
||||
name,
|
||||
})
|
||||
.set(exports.defaultHeaders())
|
||||
return res.body
|
||||
}
|
||||
|
||||
|
@ -76,23 +90,24 @@ exports.destroyClientDatabase = async () => await destroy(TEST_CLIENT_ID)
|
|||
|
||||
exports.createInstance = async (request, appId) => {
|
||||
const res = await request
|
||||
.post(`/api/${appId}/instances`)
|
||||
.set(exports.defaultHeaders)
|
||||
.post(`/api/instances`)
|
||||
.send({
|
||||
name: "test-instance",
|
||||
name: "test-instance2",
|
||||
})
|
||||
.set(exports.defaultHeaders(appId))
|
||||
return res.body
|
||||
}
|
||||
|
||||
exports.createUser = async (
|
||||
request,
|
||||
appId,
|
||||
instanceId,
|
||||
username = "babs",
|
||||
password = "babs_password"
|
||||
) => {
|
||||
const res = await request
|
||||
.post(`/api/${instanceId}/users`)
|
||||
.set(exports.defaultHeaders)
|
||||
.post(`/api/users`)
|
||||
.set(exports.defaultHeaders(appId, instanceId))
|
||||
.send({
|
||||
name: "Bill",
|
||||
username,
|
||||
|
@ -104,6 +119,7 @@ exports.createUser = async (
|
|||
|
||||
const createUserWithOnePermission = async (
|
||||
request,
|
||||
appId,
|
||||
instanceId,
|
||||
permName,
|
||||
itemId
|
||||
|
@ -115,17 +131,19 @@ const createUserWithOnePermission = async (
|
|||
|
||||
return await createUserWithPermissions(
|
||||
request,
|
||||
appId,
|
||||
instanceId,
|
||||
permissions,
|
||||
"onePermOnlyUser"
|
||||
)
|
||||
}
|
||||
|
||||
const createUserWithAdminPermissions = async (request, instanceId) => {
|
||||
const createUserWithAdminPermissions = async (request, appId, instanceId) => {
|
||||
let permissions = await generateAdminPermissions(instanceId)
|
||||
|
||||
return await createUserWithPermissions(
|
||||
request,
|
||||
appId,
|
||||
instanceId,
|
||||
permissions,
|
||||
"adminUser"
|
||||
|
@ -134,6 +152,7 @@ const createUserWithAdminPermissions = async (request, instanceId) => {
|
|||
|
||||
const createUserWithAllPermissionExceptOne = async (
|
||||
request,
|
||||
appId,
|
||||
instanceId,
|
||||
permName,
|
||||
itemId
|
||||
|
@ -145,6 +164,7 @@ const createUserWithAllPermissionExceptOne = async (
|
|||
|
||||
return await createUserWithPermissions(
|
||||
request,
|
||||
appId,
|
||||
instanceId,
|
||||
permissions,
|
||||
"allPermsExceptOneUser"
|
||||
|
@ -153,19 +173,20 @@ const createUserWithAllPermissionExceptOne = async (
|
|||
|
||||
const createUserWithPermissions = async (
|
||||
request,
|
||||
appId,
|
||||
instanceId,
|
||||
permissions,
|
||||
username
|
||||
) => {
|
||||
const accessRes = await request
|
||||
.post(`/api/${instanceId}/accesslevels`)
|
||||
.post(`/api/accesslevels`)
|
||||
.send({ name: "TestLevel", permissions })
|
||||
.set(exports.defaultHeaders)
|
||||
.set(exports.defaultHeaders(appId, instanceId))
|
||||
|
||||
const password = `password_${username}`
|
||||
await request
|
||||
.post(`/api/${instanceId}/users`)
|
||||
.set(exports.defaultHeaders)
|
||||
.post(`/api/users`)
|
||||
.set(exports.defaultHeaders(appId, instanceId))
|
||||
.send({
|
||||
name: username,
|
||||
username,
|
||||
|
@ -173,11 +194,20 @@ const createUserWithPermissions = async (
|
|||
accessLevelId: accessRes.body._id,
|
||||
})
|
||||
|
||||
const db = new CouchDB(instanceId)
|
||||
const designDoc = await db.get("_design/database")
|
||||
//const db = new CouchDB(instanceId)
|
||||
//const designDoc = await db.get("_design/database")
|
||||
|
||||
const anonUser = {
|
||||
userId: "ANON",
|
||||
accessLevelId: ANON_LEVEL_ID,
|
||||
appId: appId,
|
||||
}
|
||||
|
||||
const anonToken = jwt.sign(anonUser, env.JWT_SECRET)
|
||||
|
||||
const loginResult = await request
|
||||
.post(`/${designDoc.metadata.applicationId}/api/authenticate`)
|
||||
.post(`/api/authenticate`)
|
||||
.set({ Cookie: `budibase:token=${anonToken}` })
|
||||
.send({ username, password })
|
||||
|
||||
// returning necessary request headers
|
||||
|
@ -192,12 +222,14 @@ exports.testPermissionsForEndpoint = async ({
|
|||
method,
|
||||
url,
|
||||
body,
|
||||
appId,
|
||||
instanceId,
|
||||
permissionName,
|
||||
itemId,
|
||||
}) => {
|
||||
const headers = await createUserWithOnePermission(
|
||||
request,
|
||||
appId,
|
||||
instanceId,
|
||||
permissionName,
|
||||
itemId
|
||||
|
@ -209,6 +241,7 @@ exports.testPermissionsForEndpoint = async ({
|
|||
|
||||
const noPermsHeaders = await createUserWithAllPermissionExceptOne(
|
||||
request,
|
||||
appId,
|
||||
instanceId,
|
||||
permissionName,
|
||||
itemId
|
||||
|
@ -224,9 +257,14 @@ exports.builderEndpointShouldBlockNormalUsers = async ({
|
|||
method,
|
||||
url,
|
||||
body,
|
||||
appId,
|
||||
instanceId,
|
||||
}) => {
|
||||
const headers = await createUserWithAdminPermissions(request, instanceId)
|
||||
const headers = await createUserWithAdminPermissions(
|
||||
request,
|
||||
appId,
|
||||
instanceId
|
||||
)
|
||||
|
||||
await createRequest(request, method, url, body)
|
||||
.set(headers)
|
||||
|
@ -253,3 +291,7 @@ exports.insertDocument = async (databaseId, document) => {
|
|||
exports.destroyDocument = async (databaseId, documentId) => {
|
||||
return await new CouchDB(databaseId).destroy(documentId)
|
||||
}
|
||||
|
||||
exports.getDocument = async (databaseId, documentId) => {
|
||||
return await new CouchDB(databaseId).get(documentId)
|
||||
}
|
||||
|
|
|
@ -24,9 +24,9 @@ describe("/instances", () => {
|
|||
|
||||
it("returns a success message when the instance database is successfully created", async () => {
|
||||
const res = await request
|
||||
.post(`/api/${TEST_APP_ID}/instances`)
|
||||
.post(`/api/instances`)
|
||||
.send({ name: "test-instance" })
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(TEST_APP_ID))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -42,7 +42,7 @@ describe("/instances", () => {
|
|||
const instance = await createInstance(request, TEST_APP_ID);
|
||||
const res = await request
|
||||
.delete(`/api/instances/${instance._id}`)
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(TEST_APP_ID))
|
||||
.expect(200)
|
||||
|
||||
expect(res.res.statusMessage).toEqual(`Instance Database ${instance._id} successfully destroyed.`);
|
||||
|
|
|
@ -3,9 +3,10 @@ const {
|
|||
createModel,
|
||||
supertest,
|
||||
createClientDatabase,
|
||||
createApplication ,
|
||||
createApplication,
|
||||
defaultHeaders,
|
||||
builderEndpointShouldBlockNormalUsers
|
||||
builderEndpointShouldBlockNormalUsers,
|
||||
getDocument
|
||||
} = require("./couchTestUtils")
|
||||
|
||||
describe("/models", () => {
|
||||
|
@ -32,7 +33,7 @@ describe("/models", () => {
|
|||
|
||||
it("returns a success message when the model is successfully created", done => {
|
||||
request
|
||||
.post(`/api/${instance._id}/models`)
|
||||
.post(`/api/models`)
|
||||
.send({
|
||||
name: "TestModel",
|
||||
key: "name",
|
||||
|
@ -40,7 +41,7 @@ describe("/models", () => {
|
|||
name: { type: "string" }
|
||||
}
|
||||
})
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.end(async (err, res) => {
|
||||
|
@ -54,8 +55,9 @@ describe("/models", () => {
|
|||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "POST",
|
||||
url: `/api/${instance._id}/models`,
|
||||
url: `/api/models`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
body: {
|
||||
name: "TestModel",
|
||||
key: "name",
|
||||
|
@ -72,13 +74,13 @@ describe("/models", () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
instance = await createInstance(request, app._id)
|
||||
testModel = await createModel(request, instance._id, testModel)
|
||||
testModel = await createModel(request, app._id, instance._id, testModel)
|
||||
});
|
||||
|
||||
it("returns all the models for that instance in the response body", done => {
|
||||
request
|
||||
.get(`/api/${instance._id}/models`)
|
||||
.set(defaultHeaders)
|
||||
.get(`/api/models`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.end(async (_, res) => {
|
||||
|
@ -93,11 +95,11 @@ describe("/models", () => {
|
|||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "GET",
|
||||
url: `/api/${instance._id}/models`,
|
||||
url: `/api/models`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
})
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
describe("destroy", () => {
|
||||
|
@ -105,13 +107,17 @@ describe("/models", () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
instance = await createInstance(request, app._id)
|
||||
testModel = await createModel(request, instance._id, testModel)
|
||||
testModel = await createModel(request, app._id, instance._id, testModel)
|
||||
});
|
||||
|
||||
it("returns a success response when a model is deleted.", done => {
|
||||
afterEach(() => {
|
||||
delete testModel._rev
|
||||
})
|
||||
|
||||
it("returns a success response when a model is deleted.", async done => {
|
||||
request
|
||||
.delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`)
|
||||
.set(defaultHeaders)
|
||||
.delete(`/api/models/${testModel._id}/${testModel._rev}`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.end(async (_, res) => {
|
||||
|
@ -120,12 +126,48 @@ describe("/models", () => {
|
|||
});
|
||||
})
|
||||
|
||||
it("deletes linked references to the model after deletion", async done => {
|
||||
const linkedModel = await createModel(request, app._id, instance._id, {
|
||||
name: "LinkedModel",
|
||||
type: "model",
|
||||
key: "name",
|
||||
schema: {
|
||||
name: {
|
||||
type: "text",
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
TestModel: {
|
||||
type: "link",
|
||||
modelId: testModel._id,
|
||||
constraints: {
|
||||
type: "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
request
|
||||
.delete(`/api/models/${testModel._id}/${testModel._rev}`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.end(async (_, res) => {
|
||||
expect(res.res.statusMessage).toEqual(`Model ${testModel._id} deleted.`);
|
||||
const dependentModel = await getDocument(instance._id, linkedModel._id)
|
||||
expect(dependentModel.schema.TestModel).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "DELETE",
|
||||
url: `/api/${instance._id}/models/${testModel._id}/${testModel._rev}`,
|
||||
url: `/api/models/${testModel._id}/${testModel._rev}`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ describe("/records", () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
instance = await createInstance(request, app._id)
|
||||
model = await createModel(request, instance._id)
|
||||
model = await createModel(request, app._id, instance._id)
|
||||
record = {
|
||||
name: "Test Contact",
|
||||
status: "new",
|
||||
|
@ -39,9 +39,9 @@ describe("/records", () => {
|
|||
|
||||
const createRecord = async r =>
|
||||
await request
|
||||
.post(`/api/${instance._id}/${model._id}/records`)
|
||||
.post(`/api/${model._id}/records`)
|
||||
.send(r || record)
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -57,14 +57,14 @@ describe("/records", () => {
|
|||
const existing = rec.body
|
||||
|
||||
const res = await request
|
||||
.post(`/api/${instance._id}/${model._id}/records`)
|
||||
.post(`/api/${model._id}/records`)
|
||||
.send({
|
||||
_id: existing._id,
|
||||
_rev: existing._rev,
|
||||
modelId: model._id,
|
||||
name: "Updated Name",
|
||||
})
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -77,8 +77,8 @@ describe("/records", () => {
|
|||
const existing = rec.body
|
||||
|
||||
const res = await request
|
||||
.get(`/api/${instance._id}/${model._id}/records/${existing._id}`)
|
||||
.set(defaultHeaders)
|
||||
.get(`/api/${model._id}/records/${existing._id}`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -100,8 +100,8 @@ describe("/records", () => {
|
|||
await createRecord(newRecord)
|
||||
|
||||
const res = await request
|
||||
.get(`/api/${instance._id}/${model._id}/records`)
|
||||
.set(defaultHeaders)
|
||||
.get(`/api/${model._id}/records`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -110,11 +110,35 @@ describe("/records", () => {
|
|||
expect(res.body.find(r => r.name === record.name)).toBeDefined()
|
||||
})
|
||||
|
||||
it("lists records when queried by their ID", async () => {
|
||||
const newRecord = {
|
||||
modelId: model._id,
|
||||
name: "Second Contact",
|
||||
status: "new"
|
||||
}
|
||||
const record = await createRecord()
|
||||
const secondRecord = await createRecord(newRecord)
|
||||
|
||||
const recordIds = [record.body._id, secondRecord.body._id]
|
||||
|
||||
const res = await request
|
||||
.post(`/api/records/search`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.send({
|
||||
keys: recordIds
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.length).toBe(2)
|
||||
expect(res.body.map(response => response._id)).toEqual(expect.arrayContaining(recordIds))
|
||||
})
|
||||
|
||||
it("load should return 404 when record does not exist", async () => {
|
||||
await createRecord()
|
||||
await request
|
||||
.get(`/api/${instance._id}/${model._id}/records/not-a-valid-id`)
|
||||
.set(defaultHeaders)
|
||||
.get(`/api/${model._id}/records/not-a-valid-id`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404)
|
||||
})
|
||||
|
@ -123,9 +147,9 @@ describe("/records", () => {
|
|||
describe("validate", () => {
|
||||
it("should return no errors on valid record", async () => {
|
||||
const result = await request
|
||||
.post(`/api/${instance._id}/${model._id}/records/validate`)
|
||||
.post(`/api/${model._id}/records/validate`)
|
||||
.send({ name: "ivan" })
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -135,9 +159,9 @@ describe("/records", () => {
|
|||
|
||||
it("should errors on invalid record", async () => {
|
||||
const result = await request
|
||||
.post(`/api/${instance._id}/${model._id}/records/validate`)
|
||||
.post(`/api/${model._id}/records/validate`)
|
||||
.send({ name: 1 })
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
|
|
@ -36,11 +36,11 @@ describe("/users", () => {
|
|||
describe("fetch", () => {
|
||||
|
||||
it("returns a list of users from an instance db", async () => {
|
||||
await createUser(request, instance._id, "brenda", "brendas_password")
|
||||
await createUser(request, instance._id, "pam", "pam_password")
|
||||
await createUser(request, app._id, instance._id, "brenda", "brendas_password")
|
||||
await createUser(request, app._id, instance._id, "pam", "pam_password")
|
||||
const res = await request
|
||||
.get(`/api/${instance._id}/users`)
|
||||
.set(defaultHeaders)
|
||||
.get(`/api/users`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -50,12 +50,13 @@ describe("/users", () => {
|
|||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await createUser(request, instance._id, "brenda", "brendas_password")
|
||||
await createUser(request, app._id, instance._id, "brenda", "brendas_password")
|
||||
await testPermissionsForEndpoint({
|
||||
request,
|
||||
method: "GET",
|
||||
url: `/api/${instance._id}/users`,
|
||||
url: `/api/users`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
permissionName: LIST_USERS,
|
||||
})
|
||||
})
|
||||
|
@ -66,8 +67,8 @@ describe("/users", () => {
|
|||
|
||||
it("returns a success message when a user is successfully created", async () => {
|
||||
const res = await request
|
||||
.post(`/api/${instance._id}/users`)
|
||||
.set(defaultHeaders)
|
||||
.post(`/api/users`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: POWERUSER_LEVEL_ID })
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
|
@ -81,8 +82,9 @@ describe("/users", () => {
|
|||
request,
|
||||
method: "POST",
|
||||
body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", accessLevelId: POWERUSER_LEVEL_ID },
|
||||
url: `/api/${instance._id}/users`,
|
||||
url: `/api/users`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
permissionName: USER_MANAGEMENT,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -30,7 +30,7 @@ describe("/views", () => {
|
|||
|
||||
const createView = async () =>
|
||||
await request
|
||||
.post(`/api/${instance._id}/views`)
|
||||
.post(`/api/views`)
|
||||
.send({
|
||||
name: "TestView",
|
||||
map: `function(doc) {
|
||||
|
@ -40,7 +40,7 @@ describe("/views", () => {
|
|||
}`,
|
||||
reduce: `function(keys, values) { }`
|
||||
})
|
||||
.set(defaultHeaders)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -56,14 +56,14 @@ describe("/views", () => {
|
|||
describe("fetch", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
model = await createModel(request, instance._id);
|
||||
model = await createModel(request, app._id, instance._id);
|
||||
});
|
||||
|
||||
it("should only return custom views", async () => {
|
||||
const view = await createView()
|
||||
const res = await request
|
||||
.get(`/api/${instance._id}/views`)
|
||||
.set(defaultHeaders)
|
||||
.get(`/api/views`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
expect(res.body.length).toBe(1)
|
||||
|
|
|
@ -63,8 +63,8 @@ describe("/workflows", () => {
|
|||
describe("create", () => {
|
||||
it("returns a success message when the workflow is successfully created", async () => {
|
||||
const res = await request
|
||||
.post(`/api/${instance._id}/workflows`)
|
||||
.set(defaultHeaders)
|
||||
.post(`/api/workflows`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.send(TEST_WORKFLOW)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
@ -77,8 +77,9 @@ describe("/workflows", () => {
|
|||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "POST",
|
||||
url: `/api/${instance._id}/workflows`,
|
||||
url: `/api/workflows`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
body: TEST_WORKFLOW
|
||||
})
|
||||
})
|
||||
|
@ -92,8 +93,8 @@ describe("/workflows", () => {
|
|||
workflow.name = "Updated Name";
|
||||
|
||||
const res = await request
|
||||
.put(`/api/${instance._id}/workflows`)
|
||||
.set(defaultHeaders)
|
||||
.put(`/api/workflows`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.send(workflow)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
@ -107,8 +108,8 @@ describe("/workflows", () => {
|
|||
it("return all the workflows for an instance", async () => {
|
||||
await createWorkflow();
|
||||
const res = await request
|
||||
.get(`/api/${instance._id}/workflows`)
|
||||
.set(defaultHeaders)
|
||||
.get(`/api/workflows`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -119,8 +120,9 @@ describe("/workflows", () => {
|
|||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "GET",
|
||||
url: `/api/${instance._id}/workflows`,
|
||||
url: `/api/workflows`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -129,8 +131,8 @@ describe("/workflows", () => {
|
|||
it("deletes a workflow by its ID", async () => {
|
||||
await createWorkflow();
|
||||
const res = await request
|
||||
.delete(`/api/${instance._id}/workflows/${workflow.id}/${workflow.rev}`)
|
||||
.set(defaultHeaders)
|
||||
.delete(`/api/workflows/${workflow.id}/${workflow.rev}`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
|
@ -142,8 +144,9 @@ describe("/workflows", () => {
|
|||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "DELETE",
|
||||
url: `/api/${instance._id}/workflows/${workflow.id}/${workflow._rev}`,
|
||||
url: `/api/workflows/${workflow.id}/${workflow._rev}`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -6,19 +6,11 @@ const { USER_MANAGEMENT, LIST_USERS } = require("../../utilities/accessLevels")
|
|||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/:instanceId/users", authorized(LIST_USERS), controller.fetch)
|
||||
.get(
|
||||
"/api/:instanceId/users/:username",
|
||||
authorized(USER_MANAGEMENT),
|
||||
controller.find
|
||||
)
|
||||
.post(
|
||||
"/api/:instanceId/users",
|
||||
authorized(USER_MANAGEMENT),
|
||||
controller.create
|
||||
)
|
||||
.get("/api/users", authorized(LIST_USERS), controller.fetch)
|
||||
.get("/api/users/:username", authorized(USER_MANAGEMENT), controller.find)
|
||||
.post("/api/users", authorized(USER_MANAGEMENT), controller.create)
|
||||
.delete(
|
||||
"/api/:instanceId/users/:username",
|
||||
"/api/users/:username",
|
||||
authorized(USER_MANAGEMENT),
|
||||
controller.destroy
|
||||
)
|
||||
|
|
|
@ -8,13 +8,13 @@ const router = Router()
|
|||
|
||||
router
|
||||
.get(
|
||||
"/api/:instanceId/views/:viewName",
|
||||
"/api/views/:viewName",
|
||||
authorized(READ_VIEW, ctx => ctx.params.viewName),
|
||||
recordController.fetchView
|
||||
)
|
||||
.get("/api/:instanceId/views", authorized(BUILDER), viewController.fetch)
|
||||
.get("/api/views", authorized(BUILDER), viewController.fetch)
|
||||
// .patch("/api/:databaseId/views", controller.update);
|
||||
// .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy);
|
||||
.post("/api/:instanceId/views", authorized(BUILDER), viewController.create)
|
||||
.post("/api/views", authorized(BUILDER), viewController.create)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -6,20 +6,16 @@ const { BUILDER } = require("../../utilities/accessLevels")
|
|||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/:instanceId/workflows", authorized(BUILDER), controller.fetch)
|
||||
.get("/api/workflows", authorized(BUILDER), controller.fetch)
|
||||
.get("/api/workflows/:id", authorized(BUILDER), controller.find)
|
||||
.get(
|
||||
"/api/:instanceId/workflows/:id/:action",
|
||||
"/api/workflows/:id/:action",
|
||||
authorized(BUILDER),
|
||||
controller.fetchActionScript
|
||||
)
|
||||
.put("/api/:instanceId/workflows", authorized(BUILDER), controller.update)
|
||||
.post("/api/:instanceId/workflows", authorized(BUILDER), controller.create)
|
||||
.put("/api/workflows", authorized(BUILDER), controller.update)
|
||||
.post("/api/workflows", authorized(BUILDER), controller.create)
|
||||
.post("/api/workflows/action", controller.executeAction)
|
||||
.delete(
|
||||
"/api/:instanceId/workflows/:id/:rev",
|
||||
authorized(BUILDER),
|
||||
controller.destroy
|
||||
)
|
||||
.delete("/api/workflows/:id/:rev", authorized(BUILDER), controller.destroy)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
const { resolve } = require("path")
|
||||
|
||||
async function runServer() {
|
||||
const budibaseDir = "~/.budibase"
|
||||
|
||||
process.env.BUDIBASE_DIR = resolve(budibaseDir)
|
||||
|
||||
const server = await require("./app")()
|
||||
server.on("close", () => console.log("Server Closed"))
|
||||
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
|
||||
}
|
||||
|
||||
runServer()
|
|
@ -1,10 +1,11 @@
|
|||
const jwt = require("jsonwebtoken")
|
||||
const STATUS_CODES = require("../utilities/statusCodes")
|
||||
const env = require("../environment")
|
||||
const accessLevelController = require("../api/controllers/accesslevel")
|
||||
const {
|
||||
ADMIN_LEVEL_ID,
|
||||
POWERUSER_LEVEL_ID,
|
||||
BUILDER_LEVEL_ID,
|
||||
ANON_LEVEL_ID,
|
||||
} = require("../utilities/accessLevels")
|
||||
|
||||
module.exports = async (ctx, next) => {
|
||||
|
@ -15,16 +16,21 @@ module.exports = async (ctx, next) => {
|
|||
|
||||
const appToken = ctx.cookies.get("budibase:token")
|
||||
const builderToken = ctx.cookies.get("builder:token")
|
||||
const isBuilderAgent = ctx.headers["x-user-agent"] === "Budibase Builder"
|
||||
|
||||
// all admin api access should auth with buildertoken and 'Budibase Builder user agent
|
||||
const shouldAuthAsBuilder = isBuilderAgent && builderToken
|
||||
|
||||
if (shouldAuthAsBuilder) {
|
||||
const builderTokenValid = builderToken === env.ADMIN_SECRET
|
||||
|
||||
ctx.isAuthenticated = builderTokenValid
|
||||
ctx.isBuilder = builderTokenValid
|
||||
if (builderToken) {
|
||||
try {
|
||||
const jwtPayload = jwt.verify(builderToken, ctx.config.jwtSecret)
|
||||
ctx.isAuthenticated = jwtPayload.accessLevelId === BUILDER_LEVEL_ID
|
||||
ctx.user = {
|
||||
...jwtPayload,
|
||||
accessLevel: await getAccessLevel(
|
||||
jwtPayload.instanceId,
|
||||
jwtPayload.accessLevelId
|
||||
),
|
||||
}
|
||||
} catch (_) {
|
||||
// empty: do nothing
|
||||
}
|
||||
|
||||
await next()
|
||||
return
|
||||
|
@ -46,7 +52,7 @@ module.exports = async (ctx, next) => {
|
|||
jwtPayload.accessLevelId
|
||||
),
|
||||
}
|
||||
ctx.isAuthenticated = true
|
||||
ctx.isAuthenticated = ctx.user.accessLevelId !== ANON_LEVEL_ID
|
||||
} catch (err) {
|
||||
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)
|
||||
}
|
||||
|
@ -57,7 +63,9 @@ module.exports = async (ctx, next) => {
|
|||
const getAccessLevel = async (instanceId, accessLevelId) => {
|
||||
if (
|
||||
accessLevelId === POWERUSER_LEVEL_ID ||
|
||||
accessLevelId === ADMIN_LEVEL_ID
|
||||
accessLevelId === ADMIN_LEVEL_ID ||
|
||||
accessLevelId === BUILDER_LEVEL_ID ||
|
||||
accessLevelId === ANON_LEVEL_ID
|
||||
) {
|
||||
return {
|
||||
_id: accessLevelId,
|
||||
|
@ -69,6 +77,8 @@ const getAccessLevel = async (instanceId, accessLevelId) => {
|
|||
const findAccessContext = {
|
||||
params: {
|
||||
levelId: accessLevelId,
|
||||
},
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ const {
|
|||
adminPermissions,
|
||||
ADMIN_LEVEL_ID,
|
||||
POWERUSER_LEVEL_ID,
|
||||
BUILDER_LEVEL_ID,
|
||||
BUILDER,
|
||||
} = require("../utilities/accessLevels")
|
||||
|
||||
|
@ -10,7 +11,11 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
|
|||
ctx.throw(403, "Session not authenticated")
|
||||
}
|
||||
|
||||
if (ctx.isBuilder) {
|
||||
if (!ctx.user) {
|
||||
ctx.throw(403, "User not found")
|
||||
}
|
||||
|
||||
if (ctx.user.accessLevel._id === BUILDER_LEVEL_ID) {
|
||||
await next()
|
||||
return
|
||||
}
|
||||
|
@ -20,10 +25,6 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
|
|||
return
|
||||
}
|
||||
|
||||
if (!ctx.user) {
|
||||
ctx.throw(403, "User not found")
|
||||
}
|
||||
|
||||
const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "")
|
||||
|
||||
if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) {
|
||||
|
|
|
@ -5,6 +5,8 @@ const workflowController = require("../api/controllers/workflow")
|
|||
// Access Level IDs
|
||||
const ADMIN_LEVEL_ID = "ADMIN"
|
||||
const POWERUSER_LEVEL_ID = "POWER_USER"
|
||||
const BUILDER_LEVEL_ID = "BUILDER"
|
||||
const ANON_LEVEL_ID = "ANON"
|
||||
|
||||
// Permissions
|
||||
const READ_MODEL = "read-model"
|
||||
|
@ -28,7 +30,7 @@ const generateAdminPermissions = async instanceId => [
|
|||
|
||||
const generatePowerUserPermissions = async instanceId => {
|
||||
const fetchModelsCtx = {
|
||||
params: {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
|
@ -36,7 +38,7 @@ const generatePowerUserPermissions = async instanceId => {
|
|||
const models = fetchModelsCtx.body
|
||||
|
||||
const fetchViewsCtx = {
|
||||
params: {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
|
@ -44,7 +46,7 @@ const generatePowerUserPermissions = async instanceId => {
|
|||
const views = fetchViewsCtx.body
|
||||
|
||||
const fetchWorkflowsCtx = {
|
||||
params: {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
|
@ -83,6 +85,8 @@ const generatePowerUserPermissions = async instanceId => {
|
|||
module.exports = {
|
||||
ADMIN_LEVEL_ID,
|
||||
POWERUSER_LEVEL_ID,
|
||||
BUILDER_LEVEL_ID,
|
||||
ANON_LEVEL_ID,
|
||||
READ_MODEL,
|
||||
WRITE_MODEL,
|
||||
READ_VIEW,
|
||||
|
@ -90,6 +94,7 @@ module.exports = {
|
|||
USER_MANAGEMENT,
|
||||
BUILDER,
|
||||
LIST_USERS,
|
||||
adminPermissions,
|
||||
generateAdminPermissions,
|
||||
generatePowerUserPermissions,
|
||||
}
|
||||
|
|
|
@ -45,18 +45,16 @@ const copyClientLib = async (appPath, pageName) => {
|
|||
|
||||
const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => {
|
||||
const appPublicPath = publicPath(appPath, pageName)
|
||||
const appRootPath = rootPath(config, appId)
|
||||
|
||||
const stylesheetUrl = s =>
|
||||
s.startsWith("http") ? s : `/${rootPath(config, appId)}/${s}`
|
||||
|
||||
const templateObj = {
|
||||
title: pkg.page.title || "Budibase App",
|
||||
favicon: `${appRootPath}/${pkg.page.favicon || "/_shared/favicon.png"}`,
|
||||
favicon: `${pkg.page.favicon || "/_shared/favicon.png"}`,
|
||||
stylesheets: (pkg.page.stylesheets || []).map(stylesheetUrl),
|
||||
screenStyles: pkg.screens.filter(s => s._css).map(s => s._css),
|
||||
pageStyle: pkg.page._css,
|
||||
appRootPath,
|
||||
}
|
||||
|
||||
const indexHtmlTemplate = await readFile(
|
||||
|
@ -74,7 +72,6 @@ const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => {
|
|||
const buildFrontendAppDefinition = async (config, appId, pageName, pkg) => {
|
||||
const appPath = appPackageFolder(config, appId)
|
||||
const appPublicPath = publicPath(appPath, pageName)
|
||||
const appRootPath = rootPath(config, appId)
|
||||
|
||||
const filename = join(appPublicPath, "clientFrontendDefinition.js")
|
||||
|
||||
|
@ -89,7 +86,6 @@ const buildFrontendAppDefinition = async (config, appId, pageName, pkg) => {
|
|||
}
|
||||
|
||||
const clientUiDefinition = JSON.stringify({
|
||||
appRootPath: appRootPath,
|
||||
page: pkg.page,
|
||||
screens: pkg.screens,
|
||||
libraries: [
|
||||
|
|
|
@ -24,15 +24,15 @@
|
|||
{{ /each }}
|
||||
|
||||
{{ each(options.screenStyles) }}
|
||||
<link rel='stylesheet' href='{{ appRootPath }}{{ @this }}'>
|
||||
<link rel='stylesheet' href='/assets{{ @this }}'>
|
||||
{{ /each }}
|
||||
|
||||
{{ if(options.pageStyle) }}
|
||||
<link rel='stylesheet' href='{{ appRootPath }}{{ pageStyle }}'>
|
||||
<link rel='stylesheet' href='/assets{{ pageStyle }}'>
|
||||
{{ /if }}
|
||||
|
||||
<script src='{{ appRootPath }}/clientFrontendDefinition.js'></script>
|
||||
<script src='{{ appRootPath }}/budibase-client.js'></script>
|
||||
<script src='/assets/clientFrontendDefinition.js'></script>
|
||||
<script src='/assets/budibase-client.js'></script>
|
||||
|
||||
</head>
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue