Merging master

This commit is contained in:
Conor_Mack 2020-05-29 10:53:06 +01:00
commit 976c188f1e
70 changed files with 1951 additions and 692 deletions

View File

@ -38,6 +38,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.3.5",
"@budibase/client": "^0.0.32", "@budibase/client": "^0.0.32",
"@nx-js/compiler-util": "^2.0.0", "@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0", "codemirror": "^5.51.0",
@ -79,7 +80,7 @@
"rollup-plugin-svelte": "^5.0.3", "rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^4.0.4", "rollup-plugin-terser": "^4.0.4",
"rollup-plugin-url": "^2.2.2", "rollup-plugin-url": "^2.2.2",
"svelte": "^3.0.0" "svelte": "3.23.x"
}, },
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072" "gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
} }

View File

@ -26,6 +26,4 @@
<AppNotification /> <AppNotification />
<Modal> <Router {routes} />
<Router {routes} />
</Modal>

View File

@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414
1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z" />
</svg>

After

Width:  |  Height:  |  Size: 292 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10
10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v2h-2V7zm0 4h2v6h-2v-6z" />
</svg>

After

Width:  |  Height:  |  Size: 271 B

View File

@ -29,3 +29,5 @@ export { default as ContributionIcon } from "./Contribution.svelte"
export { default as BugIcon } from "./Bug.svelte" export { default as BugIcon } from "./Bug.svelte"
export { default as EmailIcon } from "./Email.svelte" export { default as EmailIcon } from "./Email.svelte"
export { default as TwitterIcon } from "./Twitter.svelte" export { default as TwitterIcon } from "./Twitter.svelte"
export { default as InfoIcon } from "./Info.svelte"
export { default as CloseIcon } from "./Close.svelte"

View File

@ -11,12 +11,12 @@
input { input {
display: block; display: block;
font-size: 14px; font-size: 14px;
font-family: sans-serif;
font-weight: 500; font-weight: 500;
color: #163057; color: var(--ink);
line-height: 1.3; line-height: 1.3;
padding: 1em 2.6em 0.9em 1.4em; padding: 12px;
width: 100%; width: 164px;
float: right;
max-width: 100%; max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
@ -24,7 +24,7 @@
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
background: #fff; background: #fff;
border: 1px solid #ccc; border: 1px solid var(--grey-dark);
height: 35px; height: 32px;
} }
</style> </style>

View File

@ -29,7 +29,6 @@
<style> <style>
.input-container { .input-container {
display: flex;
} }
.label { .label {
@ -43,16 +42,18 @@
input { input {
width: 40px; width: 40px;
height: 32px; height: 32px;
font-size: 13px; font-size: 12px;
font-weight: 700; font-weight: 700;
margin: 0px 5px; margin: 0px 0px 0px 1px;
color: #163057; text-align: center;
color: var(--ink);
opacity: 0.7; opacity: 0.7;
padding: 5px 10px; padding: 0px 4px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid #dbdbdb; border: 1px solid var(--grey);
border-radius: 2px; border-radius: 2px;
outline: none; outline: none;
float: right;
} }
input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-inner-spin-button,

View File

@ -15,13 +15,13 @@ export async function createDatabase(appname, instanceName) {
} }
export async function deleteRecord(record, instanceId) { export async function deleteRecord(record, instanceId) {
const DELETE_RECORDS_URL = `/api/${instanceId}/records/${record._id}/${record._rev}` const DELETE_RECORDS_URL = `/api/${instanceId}/${record._modelId}/records/${record._id}/${record._rev}`
const response = await api.delete(DELETE_RECORDS_URL) const response = await api.delete(DELETE_RECORDS_URL)
return response return response
} }
export async function saveRecord(record, instanceId) { export async function saveRecord(record, instanceId, modelId) {
const SAVE_RECORDS_URL = `/api/${instanceId}/records` const SAVE_RECORDS_URL = `/api/${instanceId}/${modelId}/records`
const response = await api.post(SAVE_RECORDS_URL, record) const response = await api.post(SAVE_RECORDS_URL, record)
return await response.json() return await response.json()

View File

@ -42,7 +42,8 @@
...record, ...record,
modelId: $backendUiStore.selectedModel._id, modelId: $backendUiStore.selectedModel._id,
}, },
instanceId instanceId,
$backendUiStore.selectedModel._id
) )
if (recordResponse.errors) { if (recordResponse.errors) {
errors = recordResponse.errors errors = recordResponse.errors

View File

@ -46,7 +46,7 @@
function selectModel(model) { function selectModel(model) {
backendUiStore.update(state => { backendUiStore.update(state => {
state.selectedModel = model state.selectedModel = model
state.selectedView = `all_${model._id}` state.selectedView = `${model._id}`
return state return state
}) })
} }

View File

@ -0,0 +1,68 @@
<script>
import Button from "components/common/Button.svelte"
export let name,
description = `A minimalist CRM which removes the noise and allows you to focus
on your business.`,
_id
</script>
<div class="apps-card">
<h3 class="app-title">{name}</h3>
<p class="app-desc">{description}</p>
<div class="card-footer">
<div class="modified-date">Last Edited - 25th May 2020</div>
<a href={`/_builder/${_id}`} class="app-button">Open Web App</a>
</div>
</div>
<style>
.apps-card {
background-color: var(--white);
padding: 20px;
max-width: 400px;
max-height: 150px;
border-radius: 5px;
border: 1px solid var(--grey-medium);
}
.app-button:hover {
background-color: var(--grey-light);
text-decoration: none;
}
.app-title {
font-size: 18px;
font-weight: 700;
color: var(--ink);
text-transform: capitalize;
}
.app-desc {
color: var(--ink-light);
}
.card-footer {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
}
.modified-date {
font-size: 14px;
color: var(--ink-light);
}
.app-button {
background-color: var(--white);
color: var(--ink);
padding: 12px 20px;
border-radius: 5px;
border: 1px var(--grey) solid;
font-size: 14px;
font-weight: 400;
cursor: pointer;
transition: all 0.2s;
box-sizing: border-box;
}
</style>

View File

@ -1,5 +1,5 @@
<script> <script>
import Button from "components/common/Button.svelte" import AppCard from "./AppCard.svelte"
export let apps export let apps
function myFunction() { function myFunction() {
@ -13,27 +13,23 @@
<div> <div>
<div> <div>
<div class="app-section-title">Your Web Apps</div> <div class="app-section-title">Your Web Apps</div>
{#each apps as app} <div class="apps">
<div class="apps-card"> {#each apps as app}
<h3 class="app-title">{app.name}</h3> <AppCard {...app} />
<p class="app-desc"> {/each}
A minimalist CRM which removes the noise and allows you to focus </div>
on your business.
</p>
<div class="card-footer">
<div class="modified-date">Last Edited - 25th May 2020</div>
<a href={`/_builder/${app._id}`} class="app-button">
Open Web App
</a>
</div>
</div>
{/each}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<style> <style>
.apps {
display: grid;
grid-template-columns: repeat(auto-fill, 400px);
grid-gap: 40px 85px;
justify-content: start;
}
.root { .root {
margin: 40px 80px; margin: 40px 80px;
} }
@ -44,59 +40,4 @@
font-weight: 700; font-weight: 700;
margin-bottom: 20px; margin-bottom: 20px;
} }
.apps {
display: flex;
flex-wrap: wrap;
gap: 40px;
}
.apps-card {
background-color: var(--white);
padding: 20px;
max-width: 400px;
max-height: 150px;
border-radius: 5px;
border: 1px solid var(--grey-dark);
}
.app-button:hover {
background-color: var(--grey-light);
text-decoration: none;
}
.app-title {
font-size: 18px;
font-weight: 700;
color: var(--ink);
text-transform: capitalize;
}
.app-desc {
color: var(--ink-light);
}
.card-footer {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
}
.modified-date {
font-size: 14px;
color: var(--ink-light);
}
.app-button {
background-color: var(--white);
color: var(--ink);
padding: 12px 20px;
border-radius: 5px;
border: 1px var(--grey) solid;
font-size: 14px;
font-weight: 400;
cursor: pointer;
transition: all 0.2s;
box-sizing: border-box;
}
</style> </style>

View File

@ -0,0 +1,197 @@
<script>
import Spinner from "components/common/Spinner.svelte"
import { Input, TextArea, Button } from "@budibase/bbui"
import { goto } from "@sveltech/routify"
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
import { getContext } from "svelte"
import { fade } from "svelte/transition"
const { open, close } = getContext("simple-modal")
let name = ""
let description = ""
let loading = false
let error = {}
const createNewApp = async () => {
if ((name.length > 100 || name.length < 1) && description.length < 1) {
error = {
name: true,
description: true,
}
} else if (description.length < 1) {
error = {
name: false,
description: true,
}
} else if (name.length > 100 || name.length < 1) {
error = {
name: true,
}
} else {
error = {}
const data = { name, description }
loading = true
try {
const response = await fetch("/api/applications", {
method: "POST", // *GET, POST, PUT, DELETE, etc.
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "application/json",
// 'Content-Type': 'application/x-www-form-urlencoded',
},
body: JSON.stringify(data), // body data type must match "Content-Type" header
})
const res = await response.json()
$goto(`./${res._id}`)
} catch (error) {
console.error(error)
}
}
}
let value
let onChange = () => {}
function _onCancel() {
close()
}
async function _onOkay() {
await createNewApp()
}
</script>
<div class="container">
<div class="body">
<div class="heading">
<span class="icon">
<AppsIcon />
</span>
<h3>Create new web app</h3>
</div>
<Input
name="name"
label="Name"
placeholder="Enter application name"
on:change={e => (name = e.target.value)}
on:input={e => (name = e.target.value)} />
{#if error.name}
<span class="error">You need to enter a name for your application.</span>
{/if}
<TextArea
bind:value={description}
name="description"
label="Description"
placeholder="Describe your application" />
{#if error.description}
<span class="error">
Please enter a short description of your application
</span>
{/if}
</div>
<div class="footer">
<a href="./#" class="info">
<InfoIcon />
How to get started
</a>
<Button outline thin on:click={_onCancel}>Cancel</Button>
<Button primary thin on:click={_onOkay}>Save</Button>
</div>
<div class="close-button" on:click={_onCancel}>
<CloseIcon />
</div>
{#if loading}
<div in:fade class="spinner-container">
<Spinner />
<span class="spinner-text">Creating your app...</span>
</div>
{/if}
</div>
<style>
.container {
position: relative;
}
.close-button {
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
}
.close-button :global(svg) {
width: 24px;
height: 24px;
}
.heading {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
}
h3 {
margin: 0;
font-size: 24px;
font-weight: bold;
}
.icon {
display: grid;
border-radius: 3px;
align-content: center;
justify-content: center;
margin-right: 12px;
height: 20px;
width: 20px;
padding: 10px;
background-color: var(--blue-light);
}
.info {
color: var(--primary100);
text-decoration-color: var(--primary100);
}
.info :global(svg) {
fill: var(--primary100);
margin-right: 8px;
width: 24px;
height: 24px;
}
.body {
padding: 40px 40px 80px 40px;
display: grid;
grid-gap: 20px;
}
.footer {
display: grid;
grid-gap: 20px;
align-items: center;
grid-template-columns: 1fr auto auto;
padding: 30px 40px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 50px;
background-color: var(--grey-light);
}
.spinner-container {
background: white;
position: absolute;
border-radius: 5px;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: grid;
justify-items: center;
align-content: center;
grid-gap: 50px;
}
.spinner-text {
font-size: 2em;
}
.error {
color: var(--deletion100);
font-weight: bold;
font-size: 0.8em;
}
</style>

View File

@ -4,7 +4,7 @@
export let onClick = category => {} export let onClick = category => {}
</script> </script>
<ul class="tabs"> <div class="tabs">
{#each categories as category} {#each categories as category}
<li <li
on:click={() => onClick(category)} on:click={() => onClick(category)}
@ -13,31 +13,23 @@
</li> </li>
{/each} {/each}
</ul> </div>
<style> <style>
.tabs { .tabs {
display: flex; display: flex;
justify-content: center;
list-style: none; list-style: none;
margin: 0 auto; font-size: 18px;
padding: 0 30px; font-weight: 700;
border-bottom: 1px solid #d8d8d8;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.14px;
} }
li { li {
color: #808192; color: var(--ink-lighter);
margin: 0 5px;
padding: 0 8px;
cursor: pointer; cursor: pointer;
margin-right: 20px;
} }
.active { .active {
border-bottom: solid 3px #0055ff; color: var(--ink);
color: #393c44;
} }
</style> </style>

View File

@ -109,7 +109,7 @@
.title > div:nth-child(1) { .title > div:nth-child(1) {
grid-column-start: name; grid-column-start: name;
color: var(--secondary100); color: var(--ink);
} }
.title > div:nth-child(2) { .title > div:nth-child(2) {
@ -117,7 +117,7 @@
} }
.component-props-container { .component-props-container {
margin-top: 10px; margin-top: 20px;
flex: 1 1 auto; flex: 1 1 auto;
} }
</style> </style>

View File

@ -52,32 +52,9 @@
</div> </div>
<style> <style>
.tabs {
display: flex;
justify-content: center;
list-style: none;
margin: 0 auto;
padding: 0 30px;
border-bottom: 1px solid #d8d8d8;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.14px;
}
li {
color: #808192;
margin: 0 5px;
padding: 0 8px;
cursor: pointer;
}
.panel { .panel {
padding: 20px; padding: 20px 0px;
} display: flex;
flex-wrap: wrap;
.active {
border-bottom: solid 3px #0055ff;
color: #393c44;
} }
</style> </style>

View File

@ -54,13 +54,13 @@
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 20px 0; padding: 20px 20px;
border-left: solid 1px #e8e8ef; border-left: solid 1px var(--grey);
} }
.switcher { .switcher {
display: flex; display: flex;
margin: 0px 20px 20px 20px; margin: 0px 20px 20px 0px;
} }
.switcher > button { .switcher > button {

View File

@ -19,20 +19,20 @@
<style> <style>
.flatbutton { .flatbutton {
cursor: pointer; cursor: pointer;
padding: 5px; padding: 8px 2px;
text-align: center; text-align: center;
background: #ffffff; background: #ffffff;
color: #808192; color: var(--ink-light);
border-radius: 4px; border-radius: 5px;
font-family: Roboto; font-family: Roboto;
font-size: 11px; font-size: 14px;
font-weight: 500; font-weight: 400;
letter-spacing: 0.11px; transition: all 0.3s;
transition: background 0.5s, color 0.5s ease; text-rendering: optimizeLegibility;
} }
.selected { .selected {
background: #808192; background: var(--ink-light);
color: #ffffff; color: #ffffff;
} }
</style> </style>

View File

@ -1,6 +1,5 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import FlatButton from "./FlatButton.svelte" import FlatButton from "./FlatButton.svelte"
export let buttonProps = [] export let buttonProps = []
export let isMultiSelect = false export let isMultiSelect = false
@ -44,11 +43,9 @@
<style> <style>
.flatbutton-group { .flatbutton-group {
display: flex; display: flex;
flex-flow: row nowrap;
} }
.button-container { .button-container {
flex: 1; flex: 1;
margin: 5px;
} }
</style> </style>

View File

@ -1,66 +1,56 @@
<script> <script>
import { fly } from "svelte/transition"
export let item export let item
</script> </script>
<div class="item-item" on:click> <div class="item-item" transition:fly={{ y: 100, duration: 1000 }} on:click>
<div class="item-icon"> <div class="item-icon">
<i class={item.icon} /> <i class={item.icon} />
</div> </div>
<div class="item-text"> <div class="item-text">
<div class="item-name">{item.name}</div> <div class="item-name">{item.name}</div>
<div class="item-description">
<p>{item.description}</p>
</div>
</div> </div>
</div> </div>
<style> <style>
.item-item { .item-item {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
padding: 10px 0px 8px 10px;
align-items: center;
cursor: pointer; cursor: pointer;
margin-bottom: 8px;
padding: 8px 0px 16px 0px;
width: 120px;
height: 80px;
justify-content: center;
align-items: center;
margin-right: 8px;
background-color: var(--grey-light);
border-radius: 3px;
} }
.item-item:hover { .item-item:hover {
background: #fbfbfb; background: var(--grey);
border-radius: 5px; border-radius: 3px;
transition: all 0.2s;
} }
.item-icon { .item-icon {
flex: 0 0 40px; border-radius: 3px;
background: #f1f4fc;
height: 40px;
border-radius: 5px;
display: flex; display: flex;
justify-content: center;
align-items: center;
} }
.item-text { .item-text {
display: flex; display: flex;
padding-left: 16px;
padding-top: 8px;
flex-direction: column; flex-direction: column;
} }
.item-name { .item-name {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 400;
}
.item-description {
font-size: 12px;
color: #808192;
}
p {
line-height: 15px;
} }
i { i {
font-size: 24px; font-size: 24px;
color: #808192; color: var(--ink-light);
} }
</style> </style>

View File

@ -43,8 +43,9 @@
<style> <style>
.property-control { .property-control {
display: flex; display: flex;
flex-flow: row nowrap; flex-flow: row;
margin: 8px 0px; margin: 8px 0px;
align-items: center;
} }
.label { .label {
@ -53,13 +54,16 @@
align-items: center; align-items: center;
padding: 0px 5px; padding: 0px 5px;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 400;
letter-spacing: 0.12px;
text-align: left; text-align: left;
color: var(--ink);
margin-right: auto;
text-transform: capitalize;
} }
.control { .control {
flex: 1; flex: 1;
padding-left: 5px; padding-left: 2px;
max-width: 164px;
} }
</style> </style>

View File

@ -42,14 +42,15 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: auto; height: auto;
background: #fbfbfb; background: var(--grey-light);
margin: 5px; margin: 0px 0px 4px 0px;
padding: 5px; padding: 8px 12px;
justify-content: center;
border-radius: 4px;
} }
.property-group-name { .property-group-name {
cursor: pointer; cursor: pointer;
flex: 0 0 20px;
display: flex; display: flex;
flex-flow: row nowrap; flex-flow: row nowrap;
} }
@ -61,7 +62,7 @@
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
letter-spacing: 0.14px; letter-spacing: 0.14px;
color: #393c44; color: var(--ink);
} }
.icon { .icon {

View File

@ -7,20 +7,55 @@ import InputGroup from "../common/Inputs/InputGroup.svelte"
*/ */
export const layout = [ export const layout = [
{
label: "Display",
key: "display",
control: OptionSelect,
initialValue: "Flex",
options: [
{ label: "Flex", value: "flex" },
{ label: "Inline Flex", value: "inline-flex" },
],
},
{ {
label: "Direction", label: "Direction",
key: "flex-direction", key: "flex-direction",
control: OptionSelect, control: OptionSelect,
initialValue: "columnReverse", initialValue: "Row",
options: [ options: [
{ label: "row", value: "row" }, { label: "Row", value: "row" },
{ label: "row-reverse", value: "rowReverse" }, { label: "Row Reverse", value: "rowReverse" },
{ label: "column", value: "column" }, { label: "column", value: "column" },
{ label: "column-reverse", value: "columnReverse" }, { label: "Column Reverse", value: "columnReverse" },
],
},
{
label: "Justify",
key: "justify-content",
control: OptionSelect,
initialValue: "Flex Start",
options: [
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
{ label: "Space Between", value: "space-between" },
{ label: "Space Around", value: "space-around" },
{ label: "Space Evenly", value: "space-evenly" },
],
},
{
label: "Align",
key: "align-items",
control: OptionSelect,
initialValue: "Flex Start",
options: [
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
{ label: "Baseline", value: "baseline" },
{ label: "Stretch", value: "stretch" },
], ],
}, },
{ label: "Justify", key: "justify-content", control: Input },
{ label: "Align", key: "align-items", control: Input },
{ {
label: "Wrap", label: "Wrap",
key: "flex-wrap", key: "flex-wrap",
@ -33,19 +68,20 @@ export const layout = [
] ]
const spacingMeta = [ const spacingMeta = [
{ placeholder: "T" },
{ placeholder: "R" },
{ placeholder: "B" },
{ placeholder: "L" }, { placeholder: "L" },
{ placeholder: "B" },
{ placeholder: "R" },
{ placeholder: "T" },
] ]
export const spacing = [ export const spacing = [
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta },
{ {
label: "Padding", label: "Padding",
key: "padding", key: "padding",
control: InputGroup, control: InputGroup,
meta: spacingMeta, meta: spacingMeta,
}, },
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta },
] ]
export const size = [ export const size = [
@ -62,7 +98,39 @@ export const position = [
label: "Position", label: "Position",
key: "position", key: "position",
control: OptionSelect, control: OptionSelect,
options: ["static", "relative", "fixed", "absolute", "sticky"], initialValue: "Wrap",
options: [
{ label: "Static", value: "static" },
{ label: "Relative", value: "relative" },
{ label: "Fixed", value: "fixed" },
{ label: "Absolute", value: "absolute" },
{ label: "Sticky", value: "sticky" },
],
},
{
label: "Top",
key: "top",
control: Input,
},
{
label: "Right",
key: "right",
control: Input,
},
{
label: "Bottom",
key: "bottom",
control: Input,
},
{
label: "Left",
key: "left",
control: Input,
},
{
label: "Z-index",
key: "z-index",
control: Input,
}, },
] ]
@ -74,13 +142,21 @@ export const typography = [
defaultValue: "initial", defaultValue: "initial",
options: [ options: [
"initial", "initial",
"Times New Roman",
"Georgia",
"Arial", "Arial",
"Arial Black", "Arial Black",
"Cursive",
"Courier",
"Comic Sans MS", "Comic Sans MS",
"Helvetica",
"Impact", "Impact",
"Inter",
"Lucida Sans Unicode", "Lucida Sans Unicode",
"Open Sans",
"Playfair",
"Roboto",
"Roboto Mono",
"Times New Roman",
"Verdana",
], ],
styleBindingProperty: "font-family", styleBindingProperty: "font-family",
}, },
@ -95,8 +171,7 @@ export const typography = [
{ {
label: "Color", label: "Color",
key: "color", key: "color",
control: OptionSelect, control: Input,
options: ["black", "white", "red", "blue", "green"],
}, },
{ {
label: "align", label: "align",
@ -112,8 +187,7 @@ export const background = [
{ {
label: "Background", label: "Background",
key: "background", key: "background",
control: OptionSelect, control: Input,
options: ["black", "white", "red", "blue", "green"],
}, },
{ label: "Image", key: "image", control: Input }, //custom { label: "Image", key: "image", control: Input }, //custom
] ]
@ -124,15 +198,45 @@ export const border = [
{ {
label: "Color", label: "Color",
key: "border-color", key: "border-color",
control: OptionSelect, control: Input,
options: ["black", "white", "red", "blue", "green"], },
{
label: "Style",
key: "border-style",
control: OptionSelect,
options: [
"none",
"hidden",
"dotted",
"dashed",
"solid",
"double",
"groove",
"ridge",
"inset",
"outset",
],
}, },
{ label: "Style", key: "border-style", control: Input },
] ]
export const effects = [ export const effects = [
{ label: "Opacity", key: "opacity", control: Input }, { label: "Opacity", key: "opacity", control: Input },
{ label: "Rotate", key: "transform", control: Input }, //needs special control {
label: "Rotate",
key: "transform",
control: OptionSelect,
options: [
{ label: "None", value: "rotate(0deg)" },
{ label: "45 degrees", value: "rotate(45deg)" },
{ label: "90 degrees", value: "rotate(90deg)" },
{ label: "135 degrees", value: "rotate(135deg)" },
{ label: "180 degrees", value: "rotate(180deg)" },
{ label: "225 degrees", value: "rotate(225deg)" },
{ label: "270 degrees", value: "rotate(270deg)" },
{ label: "315 degrees", value: "rotate(315deg)" },
{ label: "360 degrees", value: "rotate(360deg)" },
],
}, //needs special control
{ label: "Shadow", key: "box-shadow", control: Input }, { label: "Shadow", key: "box-shadow", control: Input },
] ]

View File

@ -10,15 +10,6 @@ export default {
name: "Basic", name: "Basic",
isCategory: true, isCategory: true,
children: [ children: [
{
_component: "##builtin/screenslot",
name: "Screenslot",
description:
"This component is a placeholder for the rendering of a screen within a page.",
icon: "ri-crop-2-line",
commonProps: {},
children: [],
},
{ {
_component: "@budibase/standard-components/container", _component: "@budibase/standard-components/container",
name: "Container", name: "Container",
@ -119,7 +110,7 @@ export default {
{ {
name: "Input", name: "Input",
description: "These components handle user input.", description: "These components handle user input.",
icon: "ri-edit-box-line", icon: "ri-edit-box-fill",
commonProps: {}, commonProps: {},
children: [ children: [
{ {
@ -127,7 +118,7 @@ export default {
name: "Textfield", name: "Textfield",
description: description:
"A textfield component that allows the user to input text.", "A textfield component that allows the user to input text.",
icon: "ri-edit-box-line", icon: "ri-edit-box-fill",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [ settings: [
@ -145,7 +136,7 @@ export default {
_component: "@budibase/standard-components/checkbox", _component: "@budibase/standard-components/checkbox",
name: "Checkbox", name: "Checkbox",
description: "A selectable checkbox component", description: "A selectable checkbox component",
icon: "ri-checkbox-line", icon: "ri-checkbox-fill",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [{ label: "Label", key: "label", control: Input }], settings: [{ label: "Label", key: "label", control: Input }],
@ -166,7 +157,7 @@ export default {
name: "Select", name: "Select",
description: description:
"A select component for choosing from different options", "A select component for choosing from different options",
icon: "ri-file-list-line", icon: "ri-file-list-fill",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [], settings: [],
@ -236,7 +227,7 @@ export default {
name: "Card", name: "Card",
description: description:
"A basic card component that can contain content and actions.", "A basic card component that can contain content and actions.",
icon: "ri-layout-bottom-line", icon: "ri-layout-bottom-fill",
children: [], children: [],
properties: { design: { ...all } }, properties: { design: { ...all } },
}, },
@ -248,21 +239,6 @@ export default {
children: [], children: [],
properties: { design: { ...all } }, properties: { design: { ...all } },
}, },
{
name: "Navigation Bar",
_component: "@budibase/standard-components/Navigation",
description:
"A component for handling the navigation within your app.",
icon: "ri-navigation-fill",
children: [],
properties: { design: { ...all } },
},
],
},
{
name: "Data",
isCategory: true,
children: [
{ {
name: "Table", name: "Table",
description: "A component that generates a table from your data.", description: "A component that generates a table from your data.",
@ -283,27 +259,11 @@ export default {
}, },
children: [], children: [],
}, },
{
_component: "@budibase/standard-components/datatable",
name: "DataTable",
description: "A table for displaying data from the backend.",
icon: "ri-archive-drawer-fill",
properties: { design: { ...all } },
children: [],
},
{
_component: "@budibase/standard-components/dataform",
name: "DataForm",
description: "Form stuff",
icon: "ri-file-edit-fill",
properties: { design: { ...all } },
children: [],
},
{ {
name: "Chart", name: "Chart",
_component: "@budibase/standard-components/datachart", _component: "@budibase/standard-components/datachart",
description: "Shiny chart", description: "Shiny chart",
icon: "ri-bar-chart-line", icon: "ri-bar-chart-fill",
properties: { design: { ...all } }, properties: { design: { ...all } },
children: [], children: [],
}, },
@ -311,7 +271,7 @@ export default {
name: "List", name: "List",
_component: "@budibase/standard-components/datalist", _component: "@budibase/standard-components/datalist",
description: "Shiny list", description: "Shiny list",
icon: "ri-file-list-line", icon: "ri-file-list-fill",
properties: { design: { ...all } }, properties: { design: { ...all } },
children: [], children: [],
}, },
@ -319,11 +279,36 @@ export default {
name: "Map", name: "Map",
_component: "@budibase/standard-components/datamap", _component: "@budibase/standard-components/datamap",
description: "Shiny map", description: "Shiny map",
icon: "ri-map-pin-line", icon: "ri-map-pin-fill",
properties: { design: { ...all } }, properties: { design: { ...all } },
children: [], children: [],
}, },
], ],
}, },
{
name: "Layouts",
isCategory: true,
children: [
{
_component: "##builtin/screenslot",
name: "Screenslot",
description:
"This component is a placeholder for the rendering of a screen within a page.",
icon: "ri-crop-2-fill",
properties: { design: { ...all } },
commonProps: {},
children: [],
},
{
name: "Nav Bar",
_component: "@budibase/standard-components/Navigation",
description:
"A component for handling the navigation within your app.",
icon: "ri-navigation-fill",
children: [],
properties: { design: { ...all } },
},
],
},
], ],
} }

View File

@ -13,6 +13,7 @@
--grey: #F2F2F2; --grey: #F2F2F2;
--grey-light: #FBFBFB; --grey-light: #FBFBFB;
--grey-medium: #e8e8ef;
--grey-dark: #E6E6E6; --grey-dark: #E6E6E6;
--primary100: #0055ff; --primary100: #0055ff;
@ -125,6 +126,10 @@ h5 {
color: var(--darkslate); color: var(--darkslate);
} }
textarea {
font-family: var(--fontnormal);
}
.hoverable:hover { .hoverable:hover {
cursor: pointer; cursor: pointer;
} }

View File

@ -1,4 +1,5 @@
<script> <script>
import Modal from "svelte-simple-modal"
import { store } from "builderStore" import { store } from "builderStore"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
@ -25,56 +26,58 @@
} }
</script> </script>
<div class="root"> <Modal>
<div class="root">
<div class="top-nav"> <div class="top-nav">
<div class="topleftnav"> <div class="topleftnav">
<button class="home-logo"> <button class="home-logo">
<img <img
src="/_builder/assets/bb-logo.svg" src="/_builder/assets/bb-logo.svg"
alt="budibase icon" alt="budibase icon"
on:click={() => $goto(`/`)} /> on:click={() => $goto(`/`)} />
</button> </button>
<!-- This gets all indexable subroutes and sticks them in the top nav. --> <!-- This gets all indexable subroutes and sticks them in the top nav. -->
{#each $layout.children as { path, title }} {#each $layout.children as { path, title }}
<span <span
class:active={$isActive(path)} class:active={$isActive(path)}
class="topnavitem" class="topnavitem"
on:click={() => $goto(path)}> on:click={() => $goto(path)}>
{title} {title}
</span> </span>
{/each} {/each}
<!-- <IconButton icon="home" <!-- <IconButton icon="home"
color="var(--slate)" color="var(--slate)"
hoverColor="var(--secondary75)"/> --> hoverColor="var(--secondary75)"/> -->
</div>
<div class="toprightnav">
<span
class:active={$isActive(`/settings`)}
class="topnavitemright"
on:click={() => $goto(`/settings`)}>
<SettingsIcon />
</span>
<span
class:active={false}
class="topnavitemright"
on:click={() => (location = `/${application}`)}>
<PreviewIcon />
</span>
</div>
</div> </div>
<div class="toprightnav">
<span {#await promise}
class:active={$isActive(`/settings`)} <!-- This should probably be some kind of loading state? -->
class="topnavitemright" <div />
on:click={() => $goto(`/settings`)}> {:then}
<SettingsIcon /> <slot />
</span> {:catch error}
<span <p>Something went wrong: {error.message}</p>
class:active={false} {/await}
class="topnavitemright"
on:click={() => (location = `/${application}`)}>
<PreviewIcon />
</span>
</div>
</div> </div>
</Modal>
{#await promise}
<!-- This should probably be some kind of loading state? -->
<div />
{:then}
<slot />
{:catch error}
<p>Something went wrong: {error.message}</p>
{/await}
</div>
<style> <style>
.root { .root {
@ -147,7 +150,7 @@
} }
.topnavitemright:hover { .topnavitemright:hover {
color: rgb(255, 255, 255, 0.8); color: var(--ink);
font-weight: 500; font-weight: 500;
} }

View File

@ -127,7 +127,6 @@
.root { .root {
display: grid; display: grid;
grid-template-columns: 275px 1fr 275px; grid-template-columns: 275px 1fr 275px;
height: 100%;
width: 100%; width: 100%;
background: var(--grey-light); background: var(--grey-light);
} }
@ -136,7 +135,6 @@
.root { .root {
display: grid; display: grid;
grid-template-columns: 300px 1fr 300px; grid-template-columns: 300px 1fr 300px;
height: 100%;
width: 100%; width: 100%;
background: var(--grey-light); background: var(--grey-light);
} }

View File

@ -0,0 +1,217 @@
<script>
import Modal from "svelte-simple-modal"
import {
SettingsIcon,
AppsIcon,
UpdatesIcon,
HostingIcon,
DocumentationIcon,
TutorialsIcon,
CommunityIcon,
ContributionIcon,
BugIcon,
EmailIcon,
TwitterIcon,
} from "components/common/Icons/"
</script>
<Modal>
<div class="root">
<div class="ui-nav">
<div class="home-logo">
<img src="/_builder/assets/bb-logo.svg" alt="Budibase icon" />
</div>
<div class="nav-section">
<div class="nav-section-title">Build</div>
<div class="nav-item-home">
<span class="nav-item-icon">
<AppsIcon />
</span>
<div class="nav-item-title">Apps</div>
</div>
<div class="nav-item">
<span class="nav-item-icon">
<SettingsIcon />
</span>
<div class="nav-item-title">Settings</div>
</div>
<a href="https://budibase.con/login" target="_blank" class="nav-item">
<span class="nav-item-icon">
<UpdatesIcon />
</span>
<div class="nav-item-title">Updates</div>
</a>
<a href="https://budibase.con/login" target="_blank" class="nav-item">
<span class="nav-item-icon">
<HostingIcon />
</span>
<div class="nav-item-title">Hosting</div>
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Learn</div>
<a href="https://docs.budibase.com/" target="_blank" class="nav-item">
<span class="nav-item-icon">
<DocumentationIcon />
</span>
<div class="nav-item-title">Documentation</div>
</a>
<a
href="https://docs.budibase.com/tutorial/quick-start"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<TutorialsIcon />
</span>
<div class="nav-item-title">Tutorials</div>
</a>
<a href="https://forum.budibase.com/" target="_blank" class="nav-item">
<span class="nav-item-icon">
<CommunityIcon />
</span>
<div class="nav-item-title">Community</div>
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Contact</div>
<a
href="https://github.com/Budibase/budibase/blob/master/CONTRIBUTING.md"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<ContributionIcon />
</span>
<div class="nav-item-title">Contribute to our product</div>
</a>
<a
href="https://github.com/Budibase/budibase/issues"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<BugIcon />
</span>
<div class="nav-item-title">Report bug</div>
</a>
<a href="mailto:support@budibase.com" target="_blank" class="nav-item">
<span class="nav-item-icon">
<EmailIcon />
</span>
<div class="nav-item-title">Email</div>
</a>
<a href="https://twitter.com/budibase" target="_blank" class="nav-item">
<span class="nav-item-icon">
<TwitterIcon />
</span>
<div class="nav-item-title">Twitter</div>
</a>
</div>
</div>
<div class="main">
<slot />
</div>
</div>
</Modal>
<style>
.root {
display: grid;
grid-template-columns: 275px 1fr;
height: 100%;
width: 100%;
background: var(--grey-light);
}
@media only screen and (min-width: 1800px) {
.root {
display: grid;
grid-template-columns: 300px 1fr;
height: 100%;
width: 100%;
background: var(--grey-light);
}
}
.main {
grid-column: 2;
}
.ui-nav {
grid-column: 1;
background-color: var(--white);
padding: 20px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--grey-medium);
}
.home-logo {
cursor: pointer;
height: 40px;
margin-bottom: 20px;
}
.home-logo img {
height: 40px;
}
.nav-section {
margin: 20px 0px;
display: flex;
flex-direction: column;
}
.nav-section-title {
font-size: 20px;
color: var(--ink);
font-weight: 700;
margin-bottom: 12px;
}
.nav-item {
cursor: pointer;
margin: 0px 0px 4px 0px;
padding: 0px 0px 0px 12px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
}
.nav-item-home {
cursor: pointer;
margin: 0px 0px 4px 0px;
padding: 0px 0px 0px 12px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
background-color: var(--blue-light);
}
.nav-item:hover {
background-color: var(--grey-light);
border-radius: 3px;
}
.nav-item::selection {
background-color: var(--blue-light);
border-radius: 3px;
}
.nav-item-title {
font-size: 14px;
color: var(--ink);
font-weight: 500;
margin-left: 12px;
}
.nav-item-icon {
color: var(--ink-light);
}
</style>

View File

@ -1,23 +1,13 @@
<script> <script>
import { getContext } from "svelte"
import { store } from "builderStore" import { store } from "builderStore"
import AppList from "components/start/AppList.svelte" import AppList from "components/start/AppList.svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import IconButton from "components/common/IconButton.svelte" import IconButton from "components/common/IconButton.svelte"
import {
SettingsIcon,
AppsIcon,
UpdatesIcon,
HostingIcon,
DocumentationIcon,
TutorialsIcon,
CommunityIcon,
ContributionIcon,
BugIcon,
EmailIcon,
TwitterIcon,
} from "components/common/Icons/"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte"
let promise = getApps() let promise = getApps()
@ -31,229 +21,54 @@
throw new Error(json) throw new Error(json)
} }
} }
// Handle create app modal
const { open } = getContext("simple-modal")
const showCreateAppModal = () => {
open(
CreateAppModal,
{
message: "What is your name?",
hasForm: true,
},
{
closeButton: false,
closeOnEsc: false,
closeOnOuterClick: false,
styleContent: { padding: 0 },
closeOnOuterClick: true,
}
)
}
</script> </script>
<div class="root"> <div class="welcome">Welcome to Budibase</div>
<div class="ui-nav"> <div class="banner">
<div class="home-logo"> <div class="banner-content">
<img src="/_builder/assets/bb-logo.svg" alt="Budibase icon" /> <div class="banner-header">
</div> Every accomplishment starts with a decision to try.
<div class="nav-section">
<div class="nav-section-title">Build</div>
<div class="nav-item-home">
<span class="nav-item-icon">
<AppsIcon />
</span>
<div class="nav-item-title">Apps</div>
</div>
<div class="nav-item">
<span class="nav-item-icon">
<SettingsIcon />
</span>
<div class="nav-item-title">Settings</div>
</div>
<a href="https://budibase.con/login" target="_blank" class="nav-item">
<span class="nav-item-icon">
<UpdatesIcon />
</span>
<div class="nav-item-title">Updates</div>
</a>
<a href="https://budibase.con/login" target="_blank" class="nav-item">
<span class="nav-item-icon">
<HostingIcon />
</span>
<div class="nav-item-title">Hosting</div>
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Learn</div>
<a href="https://docs.budibase.com/" target="_blank" class="nav-item">
<span class="nav-item-icon">
<DocumentationIcon />
</span>
<div class="nav-item-title">Documentation</div>
</a>
<a
href="https://docs.budibase.com/tutorial/quick-start"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<TutorialsIcon />
</span>
<div class="nav-item-title">Tutorials</div>
</a>
<a href="https://forum.budibase.com/" target="_blank" class="nav-item">
<span class="nav-item-icon">
<CommunityIcon />
</span>
<div class="nav-item-title">Community</div>
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Contact</div>
<a
href="https://github.com/Budibase/budibase/blob/master/CONTRIBUTING.md"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<ContributionIcon />
</span>
<div class="nav-item-title">Contribute to our product</div>
</a>
<a
href="https://github.com/Budibase/budibase/issues"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<BugIcon />
</span>
<div class="nav-item-title">Report bug</div>
</a>
<a href="mailto:support@budibase.com" target="_blank" class="nav-item">
<span class="nav-item-icon">
<EmailIcon />
</span>
<div class="nav-item-title">Email</div>
</a>
<a href="https://twitter.com/budibase" target="_blank" class="nav-item">
<span class="nav-item-icon">
<TwitterIcon />
</span>
<div class="nav-item-title">Twitter</div>
</a>
</div> </div>
<button class="banner-button" type="button" on:click={showCreateAppModal}>
<i class="ri-add-circle-fill" />
Create New Web App
</button>
</div> </div>
<div class="banner-image">
<div class="main"> <img src="/_builder/assets/banner-image.png" alt="Bannerimage" />
<div class="welcome">Welcome to Budibase</div>
<div class="banner">
<div class="banner-content">
<div class="banner-header">
Every accomplishment starts with a decision to try.
</div>
<button class="banner-button" type="button">
<i class="ri-add-circle-fill" />
Create New Web App
</button>
</div>
<div class="banner-image">
<img src="/_builder/assets/banner-image.png" alt="Bannerimage" />
</div>
</div>
{#await promise}
<div class="spinner-container">
<Spinner />
</div>
{:then result}
<AppList apps={result} />
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
</div> </div>
</div> </div>
{#await promise}
<div class="spinner-container">
<Spinner />
</div>
{:then result}
<AppList apps={result} />
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
<style> <style>
.root {
display: grid;
grid-template-columns: 275px 1fr;
height: 100%;
width: 100%;
background: var(--grey-light);
}
@media only screen and (min-width: 1800px) {
.root {
display: grid;
grid-template-columns: 300px 1fr;
height: 100%;
width: 100%;
background: var(--grey-light);
}
}
.main {
grid-column: 2;
}
.ui-nav {
grid-column: 1;
background-color: var(--white);
padding: 20px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--grey-dark);
}
.home-logo {
cursor: pointer;
height: 40px;
margin-bottom: 20px;
}
.home-logo img {
height: 40px;
}
.nav-section {
margin: 20px 0px;
display: flex;
flex-direction: column;
}
.nav-section-title {
font-size: 20px;
color: var(--ink);
font-weight: 700;
margin-bottom: 12px;
}
.nav-item {
cursor: pointer;
margin: 0px 0px 4px 0px;
padding: 0px 0px 0px 12px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
}
.nav-item-home {
cursor: pointer;
margin: 0px 0px 4px 0px;
padding: 0px 0px 0px 12px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
background-color: var(--blue-light);
}
.nav-item:hover {
background-color: var(--grey-light);
border-radius: 3px;
}
.nav-item::selection {
background-color: var(--blue-light);
border-radius: 3px;
}
.nav-item-title {
font-size: 14px;
color: var(--ink);
font-weight: 500;
margin-left: 12px;
}
.nav-item-icon {
color: var(--ink-light);
}
.welcome { .welcome {
margin: 60px 80px 0px 80px; margin: 60px 80px 0px 80px;
font-size: 42px; font-size: 42px;

View File

@ -11,13 +11,6 @@ module.exports = {
default: "~/.budibase", default: "~/.budibase",
alias: "d", alias: "d",
}) })
yargs.positional("database", {
type: "string",
describe: "use a local (PouchDB) or remote (CouchDB) database",
alias: "b",
default: "local",
choices: ["local", "remote"],
})
yargs.positional("clientId", { yargs.positional("clientId", {
type: "string", type: "string",
describe: "used to determine the name of the global databse", describe: "used to determine the name of the global databse",
@ -28,7 +21,7 @@ module.exports = {
type: "string", type: "string",
describe: describe:
"connection string for couch db, format: https://username:password@localhost:5984", "connection string for couch db, format: https://username:password@localhost:5984",
alias: "x", alias: "u",
default: "", default: "",
}) })
yargs.positional("quiet", { yargs.positional("quiet", {

View File

@ -14,7 +14,6 @@ const run = async opts => {
try { try {
await ensureAppDir(opts) await ensureAppDir(opts)
await setEnvironmentVariables(opts) await setEnvironmentVariables(opts)
await prompts(opts)
await createClientDatabase(opts) await createClientDatabase(opts)
await createDevEnvFile(opts) await createDevEnvFile(opts)
console.log(chalk.green("Budibase successfully initialised.")) console.log(chalk.green("Budibase successfully initialised."))
@ -24,13 +23,13 @@ const run = async opts => {
} }
const setEnvironmentVariables = async opts => { const setEnvironmentVariables = async opts => {
if (opts.database === "local") { if (opts.couchDbUrl) {
process.env.COUCH_DB_URL = opts.couchDbUrl
} else {
const dataDir = join(opts.dir, ".data") const dataDir = join(opts.dir, ".data")
await ensureDir(dataDir) await ensureDir(dataDir)
process.env.COUCH_DB_URL = process.env.COUCH_DB_URL =
dataDir + (dataDir.endsWith("/") || dataDir.endsWith("\\") ? "" : "/") dataDir + (dataDir.endsWith("/") || dataDir.endsWith("\\") ? "" : "/")
} else {
process.env.COUCH_DB_URL = opts.couchDbUrl
} }
} }
@ -39,25 +38,6 @@ const ensureAppDir = async opts => {
await ensureDir(opts.dir) await ensureDir(opts.dir)
} }
const prompts = async opts => {
const questions = [
{
type: "input",
name: "couchDbUrl",
message:
"CouchDB Connection String (e.g. https://user:password@localhost:5984): ",
validate: function(value) {
return !!value || "Please enter connection string"
},
},
]
if (opts.database === "remote" && !opts.couchDbUrl) {
const answers = await inquirer.prompt(questions)
opts.couchDbUrl = answers.couchDbUrl
}
}
const createClientDatabase = async opts => { const createClientDatabase = async opts => {
// cannot be a top level require as it // cannot be a top level require as it
// will cause environment module to be loaded prematurely // will cause environment module to be loaded prematurely

View File

@ -49,6 +49,19 @@
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
} }
}, },
{
"type": "node",
"request": "launch",
"name": "Jest - Access Levels",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["accesslevel.spec", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",

View File

@ -24,7 +24,7 @@
}, },
"scripts": { "scripts": {
"test": "jest routes --runInBand", "test": "jest routes --runInBand",
"test:integration": "jest routes --runInBand", "test:integration": "jest workflow --runInBand",
"test:watch": "jest -w", "test:watch": "jest -w",
"initialise": "node ../cli/bin/budi init -b local -q", "initialise": "node ../cli/bin/budi init -b local -q",
"budi": "node ../cli/bin/budi", "budi": "node ../cli/bin/budi",

View File

@ -0,0 +1,108 @@
const CouchDB = require("../../db")
const newid = require("../../db/newid")
const {
generateAdminPermissions,
generatePowerUserPermissions,
POWERUSER_LEVEL_ID,
ADMIN_LEVEL_ID,
} = require("../../utilities/accessLevels")
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const body = await db.query("database/by_type", {
include_docs: true,
key: ["accesslevel"],
})
const customAccessLevels = body.rows.map(row => row.doc)
const staticAccessLevels = [
{
_id: ADMIN_LEVEL_ID,
name: "Admin",
permissions: await generateAdminPermissions(ctx.params.instanceId),
},
{
_id: POWERUSER_LEVEL_ID,
name: "Power User",
permissions: await generatePowerUserPermissions(ctx.params.instanceId),
},
]
ctx.body = [...staticAccessLevels, ...customAccessLevels]
}
exports.find = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
ctx.body = await db.get(ctx.params.levelId)
}
exports.update = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const level = await db.get(ctx.params.levelId)
level.name = ctx.body.name
level.permissions = ctx.request.body.permissions
const result = await db.put(level)
level._rev = result.rev
ctx.body = level
ctx.message = `Level ${level.name} updated successfully.`
}
exports.patch = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const level = await db.get(ctx.params.levelId)
const { removedPermissions, addedPermissions, _rev } = ctx.request.body
if (!_rev) throw new Error("Must supply a _rev to update an access level")
level._rev = _rev
if (removedPermissions) {
level.permissions = level.permissions.filter(
p =>
!removedPermissions.some(
rem => rem.name === p.name && rem.itemId === p.itemId
)
)
}
if (addedPermissions) {
level.permissions = [
...level.permissions.filter(
p =>
!addedPermissions.some(
add => add.name === p.name && add.itemId === p.itemId
)
),
...addedPermissions,
]
}
const result = await db.put(level)
level._rev = result.rev
ctx.body = level
ctx.message = `Access Level ${level.name} updated successfully.`
}
exports.create = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const level = {
name: ctx.request.body.name,
_rev: ctx.request.body._rev,
permissions: ctx.request.body.permissions || [],
_id: newid(),
type: "accesslevel",
}
const result = await db.put(level)
level._rev = result.rev
ctx.body = level
ctx.message = `Access Level '${level.name}' created successfully.`
}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
await db.remove(ctx.params.levelId, ctx.params.rev)
ctx.message = `Access Level ${ctx.params.id} deleted successfully`
ctx.status = 200
}

View File

@ -3,6 +3,10 @@ const ClientDb = require("../../db/clientDb")
const { getPackageForBuilder } = require("../../utilities/builder") const { getPackageForBuilder } = require("../../utilities/builder")
const newid = require("../../db/newid") const newid = require("../../db/newid")
const env = require("../../environment") const env = require("../../environment")
const instanceController = require("./instance")
const { resolve, join } = require("path")
const { copy, readJSON, writeJSON, exists } = require("fs-extra")
const { exec } = require("child_process")
exports.fetch = async function(ctx) { exports.fetch = async function(ctx) {
const db = new CouchDB(ClientDb.name(env.CLIENT_ID)) const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
@ -32,12 +36,77 @@ exports.create = async function(ctx) {
"@budibase/standard-components", "@budibase/standard-components",
"@budibase/materialdesign-components", "@budibase/materialdesign-components",
], ],
...ctx.request.body, name: ctx.request.body.name,
description: ctx.request.body.description,
} }
const { rev } = await db.post(newApplication) const { rev } = await db.post(newApplication)
newApplication._rev = rev newApplication._rev = rev
const createInstCtx = {
params: {
clientId: env.CLIENT_ID,
applicationId: newApplication._id,
},
request: {
body: { name: `dev-${env.CLIENT_ID}` },
},
}
await instanceController.create(createInstCtx)
if (ctx.isDev) {
const newAppFolder = await createEmptyAppPackage(ctx, newApplication)
await runNpmInstall(newAppFolder)
}
ctx.body = newApplication ctx.body = newApplication
ctx.message = `Application ${ctx.request.body.name} created successfully` ctx.message = `Application ${ctx.request.body.name} created successfully`
} }
const createEmptyAppPackage = async (ctx, app) => {
const templateFolder = resolve(
__dirname,
"..",
"..",
"utilities",
"appDirectoryTemplate"
)
const appsFolder = env.BUDIBASE_DIR
const newAppFolder = resolve(appsFolder, app._id)
if (await exists(newAppFolder)) {
ctx.throw(400, "App folder already exists for this application")
return
}
await copy(templateFolder, newAppFolder)
const packageJsonPath = join(appsFolder, app._id, "package.json")
const packageJson = await readJSON(packageJsonPath)
packageJson.name = npmFriendlyAppName(app.name)
await writeJSON(packageJsonPath, packageJson)
return newAppFolder
}
const runNpmInstall = async newAppFolder => {
return new Promise((resolve, reject) => {
const cmd = `cd ${newAppFolder} && npm install`
exec(cmd, (error, stdout, stderr) => {
if (error) {
reject(error)
}
resolve(stdout ? stdout : stderr)
})
})
}
const npmFriendlyAppName = name =>
name
.replace(/_/g, "")
.replace(/./g, "")
.replace(/ /g, "")
.toLowerCase()

View File

@ -37,7 +37,7 @@ exports.authenticate = async ctx => {
if (await bcrypt.compare(password, dbUser.password)) { if (await bcrypt.compare(password, dbUser.password)) {
const payload = { const payload = {
userId: dbUser._id, userId: dbUser._id,
accessLevel: "", accessLevelId: dbUser.accessLevelId,
instanceId: instanceId, instanceId: instanceId,
} }

View File

@ -7,6 +7,7 @@ const ajv = new Ajv()
exports.save = async function(ctx) { exports.save = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.params.instanceId)
const record = ctx.request.body const record = ctx.request.body
record.modelId = ctx.params.modelId
if (!record._rev && !record._id) { if (!record._rev && !record._id) {
record._id = newid() record._id = newid()
@ -43,16 +44,12 @@ exports.save = async function(ctx) {
record.type = "record" record.type = "record"
const response = await db.post(record) const response = await db.post(record)
record._rev = response.rev record._rev = response.rev
// await ctx.publish(events.recordApi.save.onRecordCreated, {
// record: record,
// })
ctx.body = record ctx.body = record
ctx.status = 200 ctx.status = 200
ctx.message = `${model.name} created successfully` ctx.message = `${model.name} created successfully`
} }
exports.fetch = async function(ctx) { exports.fetchView = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.params.instanceId)
const response = await db.query(`database/${ctx.params.viewName}`, { const response = await db.query(`database/${ctx.params.viewName}`, {
include_docs: true, include_docs: true,
@ -60,13 +57,30 @@ exports.fetch = async function(ctx) {
ctx.body = response.rows.map(row => row.doc) ctx.body = response.rows.map(row => row.doc)
} }
exports.fetchModel = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const response = await db.query(`database/all_${ctx.params.modelId}`, {
include_docs: true,
})
ctx.body = response.rows.map(row => row.doc)
}
exports.find = async function(ctx) { exports.find = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.params.instanceId)
ctx.body = await db.get(ctx.params.recordId) 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")
return
}
ctx.body = record
} }
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
const databaseId = ctx.params.instanceId const db = new CouchDB(ctx.params.instanceId)
const db = new CouchDB(databaseId) 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")
return
}
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId) ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
} }

View File

@ -2,8 +2,11 @@ const CouchDB = require("../../db")
const clientDb = require("../../db/clientDb") const clientDb = require("../../db/clientDb")
const bcrypt = require("../../utilities/bcrypt") const bcrypt = require("../../utilities/bcrypt")
const env = require("../../environment") const env = require("../../environment")
const getUserId = userName => `user_${userName}` const getUserId = userName => `user_${userName}`
const {
POWERUSER_LEVEL_ID,
ADMIN_LEVEL_ID,
} = require("../../utilities/accessLevels")
exports.fetch = async function(ctx) { exports.fetch = async function(ctx) {
const database = new CouchDB(ctx.params.instanceId) const database = new CouchDB(ctx.params.instanceId)
@ -18,17 +21,26 @@ exports.fetch = async function(ctx) {
exports.create = async function(ctx) { exports.create = async function(ctx) {
const database = new CouchDB(ctx.params.instanceId) const database = new CouchDB(ctx.params.instanceId)
const appId = (await database.get("_design/database")).metadata.applicationId const appId = (await database.get("_design/database")).metadata.applicationId
const { username, password, name } = ctx.request.body const { username, password, name, accessLevelId } = ctx.request.body
if (!username || !password) ctx.throw(400, "Username and Password Required.") if (!username || !password) {
ctx.throw(400, "Username and Password Required.")
}
const response = await database.post({ const accessLevel = await checkAccessLevel(database, accessLevelId)
if (!accessLevel) ctx.throw(400, "Invalid Access Level")
const user = {
_id: getUserId(username), _id: getUserId(username),
username, username,
password: await bcrypt.hash(password), password: await bcrypt.hash(password),
name: name || username, name: name || username,
type: "user", type: "user",
}) accessLevelId,
}
const response = await database.post(user)
// the clientDB needs to store a map of users against the app // the clientDB needs to store a map of users against the app
const db = new CouchDB(clientDb.name(env.CLIENT_ID)) const db = new CouchDB(clientDb.name(env.CLIENT_ID))
@ -49,6 +61,8 @@ exports.create = async function(ctx) {
} }
} }
exports.update = async function() {}
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
const database = new CouchDB(ctx.params.instanceId) const database = new CouchDB(ctx.params.instanceId)
await database.destroy(getUserId(ctx.params.username)) await database.destroy(getUserId(ctx.params.username))
@ -65,3 +79,18 @@ exports.find = async function(ctx) {
_rev: user._rev, _rev: user._rev,
} }
} }
const checkAccessLevel = async (db, accessLevelId) => {
if (!accessLevelId) return
if (
accessLevelId === POWERUSER_LEVEL_ID ||
accessLevelId === ADMIN_LEVEL_ID
) {
return {
_id: accessLevelId,
name: accessLevelId,
permissions: [],
}
}
return await db.get(accessLevelId)
}

View File

@ -0,0 +1,64 @@
const CouchDB = require("../../db")
const newid = require("../../db/newid")
exports.create = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const workflow = ctx.request.body
workflow._id = newid()
// TODO: Possibly validate the workflow against a schema
// // validation with ajv
// const model = await db.get(record.modelId)
// const validate = ajv.compile({
// properties: model.schema,
// })
// const valid = validate(record)
// if (!valid) {
// ctx.status = 400
// ctx.body = {
// status: 400,
// errors: validate.errors,
// }
// return
// }
workflow.type = "workflow"
const response = await db.post(workflow)
workflow._rev = response.rev
ctx.status = 200
ctx.body = {
message: "Workflow created successfully",
workflow: {
...workflow,
...response,
},
}
}
exports.update = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
ctx.body = await db.get(ctx.params.recordId)
}
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const response = await db.query(`database/by_type`, {
type: "workflow",
include_docs: true,
})
ctx.body = response.rows.map(row => row.doc)
}
exports.find = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
ctx.body = await db.get(ctx.params.id)
}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
}

View File

@ -7,7 +7,6 @@ const {
authRoutes, authRoutes,
pageRoutes, pageRoutes,
userRoutes, userRoutes,
recordRoutes,
instanceRoutes, instanceRoutes,
clientRoutes, clientRoutes,
applicationRoutes, applicationRoutes,
@ -15,6 +14,8 @@ const {
viewRoutes, viewRoutes,
staticRoutes, staticRoutes,
componentRoutes, componentRoutes,
workflowRoutes,
accesslevelRoutes,
} = require("./routes") } = require("./routes")
const router = new Router() const router = new Router()
@ -70,11 +71,11 @@ router.use(modelRoutes.allowedMethods())
router.use(userRoutes.routes()) router.use(userRoutes.routes())
router.use(userRoutes.allowedMethods()) router.use(userRoutes.allowedMethods())
router.use(recordRoutes.routes())
router.use(recordRoutes.allowedMethods())
router.use(instanceRoutes.routes()) router.use(instanceRoutes.routes())
router.use(instanceRoutes.allowedMethods()) router.use(instanceRoutes.allowedMethods())
router.use(workflowRoutes.routes())
router.use(workflowRoutes.allowedMethods())
// end auth routes // end auth routes
router.use(pageRoutes.routes()) router.use(pageRoutes.routes())
@ -89,6 +90,9 @@ router.use(componentRoutes.allowedMethods())
router.use(clientRoutes.routes()) router.use(clientRoutes.routes())
router.use(clientRoutes.allowedMethods()) router.use(clientRoutes.allowedMethods())
router.use(accesslevelRoutes.routes())
router.use(accesslevelRoutes.allowedMethods())
router.use(staticRoutes.routes()) router.use(staticRoutes.routes())
router.use(staticRoutes.allowedMethods()) router.use(staticRoutes.allowedMethods())

View File

@ -0,0 +1,14 @@
const Router = require("@koa/router")
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)
module.exports = router

View File

@ -1,11 +1,17 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/application") const controller = require("../controllers/application")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const router = Router() const router = Router()
router router
.get("/api/applications", controller.fetch) .get("/api/applications", authorized(BUILDER), controller.fetch)
.get("/api/:applicationId/appPackage", controller.fetchAppPackage) .get(
.post("/api/applications", controller.create) "/api/:applicationId/appPackage",
authorized(BUILDER),
controller.fetchAppPackage
)
.post("/api/applications", authorized(BUILDER), controller.create)
module.exports = router module.exports = router

View File

@ -1,8 +1,10 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/client") const controller = require("../controllers/client")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const router = Router() const router = Router()
router.get("/api/client/id", controller.getClientId) router.get("/api/client/id", authorized(BUILDER), controller.getClientId)
module.exports = router module.exports = router

View File

@ -1,10 +1,13 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/component") const controller = require("../controllers/component")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const router = Router() const router = Router()
router.get( router.get(
"/:appId/components/definitions", "/:appId/components/definitions",
authorized(BUILDER),
controller.fetchAppComponentDefinitions controller.fetchAppComponentDefinitions
) )

View File

@ -1,7 +1,6 @@
const authRoutes = require("./auth") const authRoutes = require("./auth")
const pageRoutes = require("./pages") const pageRoutes = require("./pages")
const userRoutes = require("./user") const userRoutes = require("./user")
const recordRoutes = require("./record")
const instanceRoutes = require("./instance") const instanceRoutes = require("./instance")
const clientRoutes = require("./client") const clientRoutes = require("./client")
const applicationRoutes = require("./application") const applicationRoutes = require("./application")
@ -9,12 +8,13 @@ const modelRoutes = require("./model")
const viewRoutes = require("./view") const viewRoutes = require("./view")
const staticRoutes = require("./static") const staticRoutes = require("./static")
const componentRoutes = require("./component") const componentRoutes = require("./component")
const workflowRoutes = require("./workflow")
const accesslevelRoutes = require("./accesslevel")
module.exports = { module.exports = {
authRoutes, authRoutes,
pageRoutes, pageRoutes,
userRoutes, userRoutes,
recordRoutes,
instanceRoutes, instanceRoutes,
clientRoutes, clientRoutes,
applicationRoutes, applicationRoutes,
@ -22,4 +22,6 @@ module.exports = {
viewRoutes, viewRoutes,
staticRoutes, staticRoutes,
componentRoutes, componentRoutes,
workflowRoutes,
accesslevelRoutes,
} }

View File

@ -1,10 +1,12 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/instance") const controller = require("../controllers/instance")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const router = Router() const router = Router()
router router
.post("/api/:applicationId/instances", controller.create) .post("/api/:applicationId/instances", authorized(BUILDER), controller.create)
.delete("/api/instances/:instanceId", controller.destroy) .delete("/api/instances/:instanceId", authorized(BUILDER), controller.destroy)
module.exports = router module.exports = router

View File

@ -1,12 +1,49 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/model") 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 router = Router() const router = Router()
// records
router router
.get("/api/:instanceId/models", controller.fetch) .get(
.post("/api/:instanceId/models", controller.create) "/api/:instanceId/:modelId/records",
authorized(READ_MODEL, ctx => ctx.params.modelId),
recordController.fetchModel
)
.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
)
.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)
.post("/api/:instanceId/models", authorized(BUILDER), modelController.create)
// .patch("/api/:instanceId/models", controller.update) // .patch("/api/:instanceId/models", controller.update)
.delete("/api/:instanceId/models/:modelId/:revId", controller.destroy) .delete(
"/api/:instanceId/models/:modelId/:revId",
authorized(BUILDER),
modelController.destroy
)
module.exports = router module.exports = router

View File

@ -7,63 +7,85 @@ const {
renameScreen, renameScreen,
deleteScreen, deleteScreen,
} = require("../../utilities/builder") } = require("../../utilities/builder")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const router = Router() const router = Router()
router.post("/_builder/api/:appId/pages/:pageName", async ctx => { router.post(
await buildPage( "/_builder/api/:appId/pages/:pageName",
ctx.config, authorized(BUILDER),
ctx.params.appId, async ctx => {
ctx.params.pageName, await buildPage(
ctx.request.body ctx.config,
) ctx.params.appId,
ctx.response.status = StatusCodes.OK ctx.params.pageName,
}) ctx.request.body
)
ctx.response.status = StatusCodes.OK
}
)
router.get("/_builder/api/:appId/pages/:pagename/screens", async ctx => { router.get(
ctx.body = await listScreens( "/_builder/api/:appId/pages/:pagename/screens",
ctx.config, authorized(BUILDER),
ctx.params.appId, async ctx => {
ctx.params.pagename ctx.body = await listScreens(
) ctx.config,
ctx.response.status = StatusCodes.OK ctx.params.appId,
}) ctx.params.pagename
)
ctx.response.status = StatusCodes.OK
}
)
router.post("/_builder/api/:appId/pages/:pagename/screen", async ctx => { router.post(
ctx.body = await saveScreen( "/_builder/api/:appId/pages/:pagename/screen",
ctx.config, authorized(BUILDER),
ctx.params.appId, async ctx => {
ctx.params.pagename, ctx.body = await saveScreen(
ctx.request.body ctx.config,
) ctx.params.appId,
ctx.response.status = StatusCodes.OK ctx.params.pagename,
}) ctx.request.body
)
ctx.response.status = StatusCodes.OK
}
)
router.patch("/_builder/api/:appname/pages/:pagename/screen", async ctx => { router.patch(
await renameScreen( "/_builder/api/:appname/pages/:pagename/screen",
ctx.config, authorized(BUILDER),
ctx.params.appname, async ctx => {
ctx.params.pagename, await renameScreen(
ctx.request.body.oldname, ctx.config,
ctx.request.body.newname ctx.params.appname,
) ctx.params.pagename,
ctx.response.status = StatusCodes.OK ctx.request.body.oldname,
}) ctx.request.body.newname
)
ctx.response.status = StatusCodes.OK
}
)
router.delete("/_builder/api/:appname/pages/:pagename/screen/*", async ctx => { router.delete(
const name = ctx.request.path.replace( "/_builder/api/:appname/pages/:pagename/screen/*",
`/_builder/api/${ctx.params.appname}/pages/${ctx.params.pagename}/screen/`, authorized(BUILDER),
"" async ctx => {
) const name = ctx.request.path.replace(
`/_builder/api/${ctx.params.appname}/pages/${ctx.params.pagename}/screen/`,
""
)
await deleteScreen( await deleteScreen(
ctx.config, ctx.config,
ctx.params.appname, ctx.params.appname,
ctx.params.pagename, ctx.params.pagename,
decodeURI(name) decodeURI(name)
) )
ctx.response.status = StatusCodes.OK ctx.response.status = StatusCodes.OK
}) }
)
module.exports = router module.exports = router

View File

@ -1,12 +0,0 @@
const Router = require("@koa/router")
const controller = require("../controllers/record")
const router = Router()
router
.get("/api/:instanceId/:viewName/records", controller.fetch)
.get("/api/:instanceId/records/:recordId", controller.find)
.post("/api/:instanceId/records", controller.save)
.delete("/api/:instanceId/records/:recordId/:revId", controller.destroy)
module.exports = router

View File

@ -1,11 +1,17 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/screen") const controller = require("../controllers/screen")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const router = Router() const router = Router()
router router
.get("/api/:instanceId/screens", controller.fetch) .get("/api/:instanceId/screens", authorized(BUILDER), controller.fetch)
.post("/api/:instanceId/screens", controller.save) .post("/api/:instanceId/screens", authorized(BUILDER), controller.save)
.delete("/api/:instanceId/:screenId/:revId", controller.destroy) .delete(
"/api/:instanceId/:screenId/:revId",
authorized(BUILDER),
controller.destroy
)
module.exports = router module.exports = router

View File

@ -0,0 +1,184 @@
const {
createInstance,
createClientDatabase,
createApplication,
createModel,
createView,
supertest,
defaultHeaders
} = require("./couchTestUtils")
const {
generateAdminPermissions,
generatePowerUserPermissions,
POWERUSER_LEVEL_ID,
ADMIN_LEVEL_ID,
READ_MODEL,
WRITE_MODEL,
} = require("../../../utilities/accessLevels")
describe("/accesslevels", () => {
let appId
let server
let request
let instanceId
let model
let view
beforeAll(async () => {
({ request, server } = await supertest())
await createClientDatabase(request);
appId = (await createApplication(request))._id
});
afterAll(async () => {
server.close();
})
beforeEach(async () => {
instanceId = (await createInstance(request, appId))._id
model = await createModel(request, instanceId)
view = await createView(request, instanceId)
})
describe("create", () => {
it("returns a success message when level is successfully created", async () => {
const res = await request
.post(`/api/${instanceId}/accesslevels`)
.send({ name: "user" })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(res.res.statusMessage).toEqual("Access Level 'user' created successfully.")
expect(res.body._id).toBeDefined()
expect(res.body._rev).toBeDefined()
expect(res.body.permissions).toEqual([])
})
});
describe("fetch", () => {
it("should list custom levels, plus 2 default levels", async () => {
const createRes = await request
.post(`/api/${instanceId}/accesslevels`)
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
const customLevel = createRes.body
const res = await request
.get(`/api/${instanceId}/accesslevels`)
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.length).toBe(3)
const adminLevel = res.body.find(r => r._id === ADMIN_LEVEL_ID)
expect(adminLevel).toBeDefined()
expect(adminLevel.permissions).toEqual(await generateAdminPermissions(instanceId))
const powerUserLevel = res.body.find(r => r._id === POWERUSER_LEVEL_ID)
expect(powerUserLevel).toBeDefined()
expect(powerUserLevel.permissions).toEqual(await generatePowerUserPermissions(instanceId))
const customLevelFetched = res.body.find(r => r._id === customLevel._id)
expect(customLevelFetched.permissions).toEqual(customLevel.permissions)
})
});
describe("destroy", () => {
it("should delete custom access level", async () => {
const createRes = await request
.post(`/api/${instanceId}/accesslevels`)
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL } ] })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
const customLevel = createRes.body
await request
.delete(`/api/${instanceId}/accesslevels/${customLevel._id}/${customLevel._rev}`)
.set(defaultHeaders)
.expect(200)
await request
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`)
.set(defaultHeaders)
.expect(404)
})
})
describe("patch", () => {
it("should add given permissions", async () => {
const createRes = await request
.post(`/api/${instanceId}/accesslevels`)
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
const customLevel = createRes.body
await request
.patch(`/api/${instanceId}/accesslevels/${customLevel._id}`)
.send({
_rev: customLevel._rev,
addedPermissions: [ { itemId: model._id, name: WRITE_MODEL } ]
})
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
const finalRes = await request
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`)
.set(defaultHeaders)
.expect(200)
expect(finalRes.body.permissions.length).toBe(2)
expect(finalRes.body.permissions.some(p => p.name === WRITE_MODEL)).toBe(true)
expect(finalRes.body.permissions.some(p => p.name === READ_MODEL)).toBe(true)
})
it("should remove given permissions", async () => {
const createRes = await request
.post(`/api/${instanceId}/accesslevels`)
.send({
name: "user",
permissions: [
{ itemId: model._id, name: READ_MODEL },
{ itemId: model._id, name: WRITE_MODEL },
]
})
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
const customLevel = createRes.body
await request
.patch(`/api/${instanceId}/accesslevels/${customLevel._id}`)
.send({
_rev: customLevel._rev,
removedPermissions: [ { itemId: model._id, name: WRITE_MODEL }]
})
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
const finalRes = await request
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`)
.set(defaultHeaders)
.expect(200)
expect(finalRes.body.permissions.length).toBe(1)
expect(finalRes.body.permissions.some(p => p.name === READ_MODEL)).toBe(true)
})
})
});

View File

@ -2,6 +2,7 @@ const CouchDB = require("../../../db")
const { create, destroy } = require("../../../db/clientDb") const { create, destroy } = require("../../../db/clientDb")
const supertest = require("supertest") const supertest = require("supertest")
const app = require("../../../app") const app = require("../../../app")
const { POWERUSER_LEVEL_ID } = require("../../../utilities/accessLevels")
const TEST_CLIENT_ID = "test-client-id" const TEST_CLIENT_ID = "test-client-id"
@ -17,7 +18,7 @@ exports.supertest = async () => {
exports.defaultHeaders = { exports.defaultHeaders = {
Accept: "application/json", Accept: "application/json",
Authorization: "Basic test-admin-secret", Cookie: ["builder:token=test-admin-secret"],
} }
exports.createModel = async (request, instanceId, model) => { exports.createModel = async (request, instanceId, model) => {
@ -37,6 +38,18 @@ exports.createModel = async (request, instanceId, model) => {
return res.body return res.body
} }
exports.createView = async (request, instanceId, view) => {
view = view || {
map: "function(doc) { emit(doc[doc.key], doc._id); } ",
}
const res = await request
.post(`/api/${instanceId}/views`)
.set(exports.defaultHeaders)
.send(view)
return res.body
}
exports.createClientDatabase = async () => await create(TEST_CLIENT_ID) exports.createClientDatabase = async () => await create(TEST_CLIENT_ID)
exports.createApplication = async (request, name = "test_application") => { exports.createApplication = async (request, name = "test_application") => {
@ -70,20 +83,20 @@ exports.createUser = async (
const res = await request const res = await request
.post(`/api/${instanceId}/users`) .post(`/api/${instanceId}/users`)
.set(exports.defaultHeaders) .set(exports.defaultHeaders)
.send({ name: "Bill", username, password }) .send({
name: "Bill",
username,
password,
accessLevelId: POWERUSER_LEVEL_ID,
})
return res.body return res.body
} }
exports.insertDocument = async (databaseId, document) => { exports.insertDocument = async (databaseId, document) => {
const { id, ...documentFields } = document const { id, ...documentFields } = document
await new CouchDB(databaseId).put({ _id: id, ...documentFields }) return await new CouchDB(databaseId).put({ _id: id, ...documentFields })
} }
exports.createSchema = async (request, instanceId, schema) => { exports.destroyDocument = async (databaseId, documentId) => {
for (let model of schema.models) { return await new CouchDB(databaseId).destroy(documentId)
await request.post(`/api/${instanceId}/models`).send(model)
}
for (let view of schema.views) {
await request.post(`/api/${instanceId}/views`).send(view)
}
} }

View File

@ -3,7 +3,8 @@ const {
createModel, createModel,
supertest, supertest,
createClientDatabase, createClientDatabase,
createApplication createApplication ,
defaultHeaders
} = require("./couchTestUtils") } = require("./couchTestUtils")
describe("/models", () => { describe("/models", () => {
@ -38,7 +39,7 @@ describe("/models", () => {
name: { type: "string" } name: { type: "string" }
} }
}) })
.set("Accept", "application/json") .set(defaultHeaders)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (err, res) => { .end(async (err, res) => {
@ -60,7 +61,7 @@ describe("/models", () => {
it("returns all the models for that instance in the response body", done => { it("returns all the models for that instance in the response body", done => {
request request
.get(`/api/${instance._id}/models`) .get(`/api/${instance._id}/models`)
.set("Accept", "application/json") .set(defaultHeaders)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (_, res) => { .end(async (_, res) => {
@ -83,7 +84,7 @@ describe("/models", () => {
it("returns a success response when a model is deleted.", done => { it("returns a success response when a model is deleted.", done => {
request request
.delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`) .delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`)
.set("Accept", "application/json") .set(defaultHeaders)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (_, res) => { .end(async (_, res) => {

View File

@ -3,7 +3,8 @@ const {
createClientDatabase, createClientDatabase,
createInstance, createInstance,
createModel, createModel,
supertest supertest,
defaultHeaders,
} = require("./couchTestUtils"); } = require("./couchTestUtils");
describe("/records", () => { describe("/records", () => {
@ -38,9 +39,9 @@ describe("/records", () => {
const createRecord = async r => const createRecord = async r =>
await request await request
.post(`/api/${instance._id}/records`) .post(`/api/${instance._id}/${model._id}/records`)
.send(r || record) .send(r || record)
.set("Accept", "application/json") .set(defaultHeaders)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -56,14 +57,14 @@ describe("/records", () => {
const existing = rec.body const existing = rec.body
const res = await request const res = await request
.post(`/api/${instance._id}/records`) .post(`/api/${instance._id}/${model._id}/records`)
.send({ .send({
_id: existing._id, _id: existing._id,
_rev: existing._rev, _rev: existing._rev,
modelId: model._id, modelId: model._id,
name: "Updated Name", name: "Updated Name",
}) })
.set("Accept", "application/json") .set(defaultHeaders)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -76,8 +77,8 @@ describe("/records", () => {
const existing = rec.body const existing = rec.body
const res = await request const res = await request
.get(`/api/${instance._id}/records/${existing._id}`) .get(`/api/${instance._id}/${model._id}/records/${existing._id}`)
.set("Accept", "application/json") .set(defaultHeaders)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -99,8 +100,8 @@ describe("/records", () => {
await createRecord(newRecord) await createRecord(newRecord)
const res = await request const res = await request
.get(`/api/${instance._id}/all_${newRecord.modelId}/records`) .get(`/api/${instance._id}/${model._id}/records`)
.set("Accept", "application/json") .set(defaultHeaders)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -112,8 +113,8 @@ describe("/records", () => {
it("load should return 404 when record does not exist", async () => { it("load should return 404 when record does not exist", async () => {
await createRecord() await createRecord()
await request await request
.get(`/api/${instance._id}/records/not-a-valid-id`) .get(`/api/${instance._id}/${model._id}/records/not-a-valid-id`)
.set("Accept", "application/json") .set(defaultHeaders)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(404) .expect(404)
}) })

View File

@ -6,6 +6,7 @@ const {
defaultHeaders, defaultHeaders,
createUser, createUser,
} = require("./couchTestUtils") } = require("./couchTestUtils")
const { POWERUSER_LEVEL_ID } = require("../../../utilities/accessLevels")
describe("/users", () => { describe("/users", () => {
let request let request
@ -51,7 +52,7 @@ describe("/users", () => {
const res = await request const res = await request
.post(`/api/${instance._id}/users`) .post(`/api/${instance._id}/users`)
.set(defaultHeaders) .set(defaultHeaders)
.send({ name: "Bill", username: "bill", password: "bills_password" }) .send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: POWERUSER_LEVEL_ID })
.expect(200) .expect(200)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)

View File

@ -0,0 +1,114 @@
const {
createClientDatabase,
createApplication,
createInstance,
defaultHeaders,
supertest,
insertDocument,
destroyDocument
} = require("./couchTestUtils")
const TEST_WORKFLOW = {
_id: "Test Workflow",
name: "My Workflow",
pageId: "123123123",
screenId: "kasdkfldsafkl",
live: true,
uiTree: {
},
definition: {
triggers: [
],
next: {
actionId: "abc123",
type: "SERVER",
conditions: {
}
}
}
}
describe("/workflows", () => {
let request
let server
let app
let instance
let workflow
beforeAll(async () => {
({ request, server } = await supertest())
await createClientDatabase(request)
app = await createApplication(request)
})
beforeEach(async () => {
instance = await createInstance(request, app._id)
if (workflow) await destroyDocument(workflow.id);
})
afterAll(async () => {
server.close()
})
const createWorkflow = async () => {
workflow = await insertDocument(instance._id, {
type: "workflow",
...TEST_WORKFLOW
});
}
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)
.send(TEST_WORKFLOW)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.message).toEqual("Workflow created successfully");
expect(res.body.workflow.name).toEqual("My Workflow");
})
})
describe("fetch", () => {
it("return all the workflows for an instance", async () => {
await createWorkflow();
const res = await request
.get(`/api/${instance._id}/workflows`)
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body[0]).toEqual(expect.objectContaining(TEST_WORKFLOW));
})
})
describe("find", () => {
it("returns a workflow when queried by ID", async () => {
await createWorkflow();
const res = await request
.get(`/api/${instance._id}/workflows/${workflow.id}`)
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body).toEqual(expect.objectContaining(TEST_WORKFLOW));
})
})
describe("destroy", () => {
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)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.id).toEqual(TEST_WORKFLOW._id);
})
})
});

View File

@ -1,12 +1,26 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/user") const controller = require("../controllers/user")
const authorized = require("../../middleware/authorized")
const { USER_MANAGEMENT, LIST_USERS } = require("../../utilities/accessLevels")
const router = Router() const router = Router()
router router
.get("/api/:instanceId/users", controller.fetch) .get("/api/:instanceId/users", authorized(LIST_USERS), controller.fetch)
.get("/api/:instanceId/users/:username", controller.find) .get(
.post("/api/:instanceId/users", controller.create) "/api/:instanceId/users/:username",
.delete("/api/:instanceId/users/:username", controller.destroy) authorized(USER_MANAGEMENT),
controller.find
)
.post(
"/api/:instanceId/users",
authorized(USER_MANAGEMENT),
controller.create
)
.delete(
"/api/:instanceId/users/:username",
authorized(USER_MANAGEMENT),
controller.destroy
)
module.exports = router module.exports = router

View File

@ -1,12 +1,20 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/view") const viewController = require("../controllers/view")
const recordController = require("../controllers/record")
const authorized = require("../../middleware/authorized")
const { BUILDER, READ_VIEW } = require("../../utilities/accessLevels")
const router = Router() const router = Router()
router router
.get("/api/:instanceId/views", controller.fetch) .get(
"/api/:instanceId/view/:viewName",
authorized(READ_VIEW, ctx => ctx.params.viewName),
recordController.fetchView
)
.get("/api/:instanceId/views", authorized(BUILDER), viewController.fetch)
// .patch("/api/:databaseId/views", controller.update); // .patch("/api/:databaseId/views", controller.update);
// .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy); // .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy);
.post("/api/:instanceId/views", controller.create) .post("/api/:instanceId/views", authorized(BUILDER), viewController.create)
module.exports = router module.exports = router

View File

@ -0,0 +1,13 @@
const Router = require("@koa/router")
const controller = require("../controllers/workflow")
const router = Router()
router
.get("/api/:instanceId/workflows", controller.fetch)
.get("/api/:instanceId/workflows/:id", controller.find)
.post("/api/:instanceId/workflows", controller.create)
.put("/api/:instanceId/workflows/:id", controller.update)
.delete("/api/:instanceId/workflows/:id/:rev", controller.destroy)
module.exports = router

View File

@ -1,6 +1,11 @@
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const STATUS_CODES = require("../utilities/statusCodes") const STATUS_CODES = require("../utilities/statusCodes")
const env = require("../environment") const env = require("../environment")
const accessLevelController = require("../api/controllers/accesslevel")
const {
ADMIN_LEVEL_ID,
POWERUSER_LEVEL_ID,
} = require("../utilities/accessLevels")
module.exports = async (ctx, next) => { module.exports = async (ctx, next) => {
if (ctx.path === "/_builder") { if (ctx.path === "/_builder") {
@ -8,8 +13,9 @@ module.exports = async (ctx, next) => {
return return
} }
if (ctx.isDev && ctx.cookies.get("builder:token") === env.ADMIN_SECRET) { if (ctx.cookies.get("builder:token") === env.ADMIN_SECRET) {
ctx.isAuthenticated = true ctx.isAuthenticated = true
ctx.isBuilder = true
await next() await next()
return return
} }
@ -23,7 +29,12 @@ module.exports = async (ctx, next) => {
} }
try { try {
ctx.jwtPayload = jwt.verify(token, ctx.config.jwtSecret) const jwtPayload = jwt.verify(token, ctx.config.jwtSecret)
ctx.user = {
...jwtPayload,
accessLevel: await getAccessLevel(jwtPayload.accessLevelId),
}
ctx.isAuthenticated = true ctx.isAuthenticated = true
} catch (err) { } catch (err) {
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text) ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)
@ -31,3 +42,22 @@ module.exports = async (ctx, next) => {
await next() await next()
} }
const getAccessLevel = async accessLevelId => {
if (
accessLevelId === POWERUSER_LEVEL_ID ||
accessLevelId === ADMIN_LEVEL_ID
) {
return {
_id: accessLevelId,
name: accessLevelId,
permissions: [],
}
}
const findAccessContext = {
params: { levelId: accessLevelId },
}
await accessLevelController.find(findAccessContext)
return findAccessContext.body
}

View File

@ -0,0 +1,58 @@
const {
adminPermissions,
ADMIN_LEVEL_ID,
POWERUSER_LEVEL_ID,
BUILDER,
} = require("../utilities/accessLevels")
module.exports = (permName, getItemId) => async (ctx, next) => {
if (!ctx.isAuthenticated) {
ctx.throw(403, "Session not authenticated")
}
if (ctx.isBuilder) {
await next()
return
}
if (permName === BUILDER) {
ctx.throw(403, "Not Authorized")
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) {
await next()
return
}
const thisPermissionId = {
name: permName,
itemId: getItemId && getItemId(ctx),
}
// power user has everything, except the admin specific perms
if (
ctx.user.accessLevel._id === POWERUSER_LEVEL_ID &&
!adminPermissions.map(permissionId).includes(thisPermissionId)
) {
await next()
return
}
if (
ctx.user.accessLevel.permissions
.map(permissionId)
.includes(thisPermissionId)
) {
await next()
return
}
ctx.throw(403, "Not Authorized")
}

View File

@ -0,0 +1,38 @@
const WORKFLOW_SCHEMA = {
properties: {
type: "workflow",
pageId: {
type: "string",
},
screenId: {
type: "string",
},
live: {
type: "boolean",
},
uiTree: {
type: "object",
},
definition: {
type: "object",
properties: {
triggers: { type: "array" },
next: {
type: "object",
properties: {
type: { type: "string" },
actionId: { type: "string" },
args: { type: "object" },
conditions: { type: "array" },
errorHandling: { type: "object" },
next: { type: "object" },
},
},
},
},
},
}
module.exports = {
WORKFLOW_SCHEMA,
}

View File

@ -0,0 +1,64 @@
const viewController = require("../api/controllers/view")
const modelController = require("../api/controllers/model")
exports.ADMIN_LEVEL_ID = "ADMIN"
exports.POWERUSER_LEVEL_ID = "POWER_USER"
exports.READ_MODEL = "read-model"
exports.WRITE_MODEL = "write-model"
exports.READ_VIEW = "read-view"
exports.EXECUTE_WORKFLOW = "execute-workflow"
exports.USER_MANAGEMENT = "user-management"
exports.BUILDER = "builder"
exports.LIST_USERS = "list-users"
exports.adminPermissions = [
{
name: exports.USER_MANAGEMENT,
},
]
exports.generateAdminPermissions = async instanceId => [
...exports.adminPermissions,
...(await exports.generatePowerUserPermissions(instanceId)),
]
exports.generatePowerUserPermissions = async instanceId => {
const fetchModelsCtx = {
params: {
instanceId,
},
}
await modelController.fetch(fetchModelsCtx)
const models = fetchModelsCtx.body
const fetchViewsCtx = {
params: {
instanceId,
},
}
await viewController.fetch(fetchViewsCtx)
const views = fetchViewsCtx.body
const readModelPermissions = models.map(m => ({
itemId: m._id,
name: exports.READ_MODEL,
}))
const writeModelPermissions = models.map(m => ({
itemId: m._id,
name: exports.WRITE_MODEL,
}))
const viewPermissions = views.map(v => ({
itemId: v.name,
name: exports.READ_VIEW,
}))
return [
...readModelPermissions,
...writeModelPermissions,
...viewPermissions,
{ name: exports.LIST_USERS },
]
}

View File

@ -0,0 +1 @@
dist/

View File

@ -0,0 +1,11 @@
{
"name": "name",
"version": "1.0.0",
"description": "",
"author": "",
"license": "ISC",
"dependencies": {
"@budibase/standard-components": "0.x",
"@budibase/materialdesign-components": "0.x"
}
}

View File

@ -0,0 +1,19 @@
{
"title": "Test App",
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
"props" : {
"_component": "@budibase/standard-components/container",
"_children": [],
"_id": 0,
"type": "div",
"_styles": {
"layout": {},
"position": {}
},
"_code": ""
},
"_css": "",
"uiFunctions": ""
}

View File

@ -0,0 +1,19 @@
{
"title": "Test App",
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
"props" : {
"_component": "@budibase/standard-components/container",
"_children": [],
"_id": 1,
"type": "div",
"_styles": {
"layout": {},
"position": {}
},
"_code": ""
},
"_css": "",
"uiFunctions": ""
}

View File

@ -0,0 +1 @@
module.exports = () => ({})