This commit is contained in:
Conor_Mack 2020-03-30 11:30:35 +01:00
commit bc60d2dfc8
119 changed files with 2774 additions and 1520 deletions

View File

@ -103,6 +103,11 @@ const lodash_fp_exports = [
"toNumber", "toNumber",
"takeRight", "takeRight",
"toPairs", "toPairs",
"remove",
"findIndex",
"compose",
"get",
"tap",
] ]
const lodash_exports = [ const lodash_exports = [
@ -159,13 +164,15 @@ export default {
}), }),
replace({ replace({
"process.env.NODE_ENV": JSON.stringify(production ? "production" : "development") "process.env.NODE_ENV": JSON.stringify(
production ? "production" : "development"
),
}), }),
svelte({ svelte({
// enable run-time checks when not in production // enable run-time checks when not in production
dev: !production, dev: !production,
include: "src/**/*.svelte", include: ["src/**/*.svelte", "node_modules/**/*.svelte"],
// we'll extract any component CSS out into // we'll extract any component CSS out into
// a separate file — better for performance // a separate file — better for performance
css: css => { css: css => {

View File

@ -29,16 +29,16 @@
{/await} {/await}
<!-- <!--
<div class="settings"> <div class="settings">
<IconButton icon="settings" <IconButton icon="settings"
on:click={store.showSettings}/> on:click={store.showSettings}/>
</div> </div>
{#if $store.useAnalytics} {#if $store.useAnalytics}
<iframe src="https://marblekirby.github.io/bb-analytics.html" width="0" height="0" style="visibility:hidden;display:none"/> <iframe src="https://marblekirby.github.io/bb-analytics.html" width="0" height="0" style="visibility:hidden;display:none"/>
{/if} {/if}
--> -->
</main> </main>
<style> <style>

View File

@ -1,14 +1,13 @@
<script> <script>
import BackendNav from "./nav/BackendNav.svelte" import BackendNav from "./nav/BackendNav.svelte"
import SchemaManagementDrawer from "./nav/SchemaManagementDrawer.svelte"
import Database from "./database/DatabaseRoot.svelte" import Database from "./database/DatabaseRoot.svelte"
import UserInterface from "./userInterface/UserInterfaceRoot.svelte" import UserInterface from "./userInterface/UserInterfaceRoot.svelte"
import ActionsAndTriggers from "./actionsAndTriggers/ActionsAndTriggersRoot.svelte" import ActionsAndTriggers from "./actionsAndTriggers/ActionsAndTriggersRoot.svelte"
import AccessLevels from "./accessLevels/AccessLevelsRoot.svelte" import AccessLevels from "./accessLevels/AccessLevelsRoot.svelte"
import ComingSoon from "./common/ComingSoon.svelte" import ComingSoon from "./common/ComingSoon.svelte"
import { store } from "./builderStore" import { store, backendUiStore } from "./builderStore"
export let navWidth = "50px"
</script> </script>
<div class="root"> <div class="root">
@ -16,29 +15,29 @@
<BackendNav /> <BackendNav />
</div> </div>
<div class="content"> <div class="content">
{#if $store.activeNav === 'database'} {#if $backendUiStore.leftNavItem === 'DATABASE'}
<Database /> <Database />
{:else if $store.activeNav === 'actions'} {:else if $backendUiStore.leftNavItem === 'ACTIONS'}
<ActionsAndTriggers /> <ActionsAndTriggers />
{:else if $store.activeNav === 'access levels'} {:else if $backendUiStore.leftNavItem === 'ACCESS_LEVELS'}
<AccessLevels /> <AccessLevels />
{/if} {/if}
</div> </div>
<div class="nav">
<SchemaManagementDrawer />
</div>
</div> </div>
<style> <style>
.root { .root {
height: 100%; height: 100%;
display: flex; display: flex;
background: #fafafa;
} }
.content { .content {
flex: 1 1 auto; flex: 1 1 auto;
height: 100%; margin: 80px 60px;
background-color: var(--white);
margin: 0;
overflow-y: auto;
overflow-x: hidden;
} }
.nav { .nav {

View File

@ -4,52 +4,58 @@
import UserInterfaceRoot from "./userInterface/UserInterfaceRoot.svelte" import UserInterfaceRoot from "./userInterface/UserInterfaceRoot.svelte"
import BackendRoot from "./BackendRoot.svelte" import BackendRoot from "./BackendRoot.svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { SettingsIcon, PreviewIcon, HelpIcon } from "./common/Icons/" import { SettingsIcon, PreviewIcon } from "./common/Icons/"
const TABS = {
BACKEND: "backend",
FRONTEND: "frontend",
}
let selectedTab = TABS.BACKEND
</script> </script>
<div class="root"> <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 src="/_builder/assets/budibase-emblem-white.svg" /> <img src="/_builder/assets/budibase-emblem-white.svg" />
</button> </button>
<!-- <IconButton icon="home" <!-- <IconButton icon="home"
color="var(--slate)" color="var(--slate)"
hoverColor="var(--secondary75)"/> --> hoverColor="var(--secondary75)"/> -->
<span <span
class:active={$store.isBackend} class:active={selectedTab === TABS.BACKEND}
class="topnavitem" class="topnavitem"
on:click={store.showBackend}> on:click={() => (selectedTab = TABS.BACKEND)}>
Backend Backend
</span> </span>
<span <span
class:active={!$store.isBackend} class:active={selectedTab === TABS.FRONTEND}
class="topnavitem" class="topnavitem"
on:click={store.showFrontend}> on:click={() => (selectedTab = TABS.FRONTEND)}>
Frontend Frontend
</span> </span>
</div> </div>
<div class="toprightnav"> <div class="toprightnav">
<span <span
class:active={!$store.isBackend} class:active={selectedTab === TABS.FRONTEND}
class="topnavitemright" class="topnavitemright"
on:click={store.showFrontend}> on:click={() => selectedTab === TABS.FRONTEND}>
<SettingsIcon /> <SettingsIcon />
</span> </span>
<span <span
class:active={!$store.isBackend} class:active={selectedTab === TABS.FRONTEND}
class="topnavitemright" class="topnavitemright"
on:click={store.showFrontend}> on:click={() => selectedTab === TABS.FRONTEND}>
<PreviewIcon /> <PreviewIcon />
</span> </span>
</div> </div>
</div> </div>
<div class="content"> <div class="content">
{#if $store.isBackend} {#if selectedTab === TABS.BACKEND}
<div in:fade out:fade> <div in:fade out:fade>
<BackendRoot /> <BackendRoot />
</div> </div>
@ -133,7 +139,7 @@
font-size: 1rem; font-size: 1rem;
height: 100%; height: 100%;
display: flex; display: flex;
flex:1; flex: 1;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
} }

View File

@ -2,7 +2,7 @@
import ButtonGroup from "../common/ButtonGroup.svelte" import ButtonGroup from "../common/ButtonGroup.svelte"
import Button from "../common/Button.svelte" import Button from "../common/Button.svelte"
import ActionButton from "../common/ActionButton.svelte" import ActionButton from "../common/ActionButton.svelte"
import { store } from "../builderStore" import { store, backendUiStore } from "../builderStore"
import { generateFullPermissions, getNewAccessLevel } from "../common/core" import { generateFullPermissions, getNewAccessLevel } from "../common/core"
import getIcon from "../common/icon" import getIcon from "../common/icon"
import AccessLevelView from "./AccessLevelView.svelte" import AccessLevelView from "./AccessLevelView.svelte"
@ -10,7 +10,12 @@
let editingLevel = null let editingLevel = null
let editingLevelIsNew = false let editingLevelIsNew = false
$: isEditing = editingLevel !== null $: {
if (editingLevel !== null) {
backendUiStore.actions.modals.show("ACCESS_LEVELS")
}
}
$: modalOpen = $backendUiStore.visibleModal === "ACCESS_LEVELS"
let allPermissions = [] let allPermissions = []
store.subscribe(db => { store.subscribe(db => {
@ -40,6 +45,7 @@
store.saveLevel(level, editingLevelIsNew, editingLevel) store.saveLevel(level, editingLevelIsNew, editingLevel)
} }
editingLevel = null editingLevel = null
backendUiStore.actions.modals.hide()
} }
const getPermissionsString = perms => { const getPermissionsString = perms => {
@ -83,19 +89,17 @@
{:else}(no actions added){/if} {:else}(no actions added){/if}
<Modal <Modal
onClosed={() => (isEditing = false)} onClosed={backendUiStore.actions.modals.hide}
bind:isOpen={isEditing} bind:isOpen={modalOpen}
title={isEditing ? 'Edit Access Level' : 'Create Access Level'}> title={modalOpen ? 'Edit Access Level' : 'Create Access Level'}>
{#if isEditing} <AccessLevelView
<AccessLevelView level={editingLevel}
level={editingLevel} {allPermissions}
{allPermissions} onFinished={onEditingFinished}
onFinished={onEditingFinished} isNew={editingLevelIsNew}
isNew={editingLevelIsNew} allLevels={$store.accessLevels.levels}
allLevels={$store.accessLevels.levels} hierarchy={$store.hierarchy}
hierarchy={$store.hierarchy} actions={$store.actions} />
actions={$store.actions} />
{/if}
</Modal> </Modal>
</div> </div>
@ -106,13 +110,4 @@
position: relative; position: relative;
padding: 1.5rem; padding: 1.5rem;
} }
.actions-header {
flex: 0 1 auto;
}
.node-view {
overflow-y: auto;
flex: 1 1 auto;
}
</style> </style>

View File

@ -91,4 +91,38 @@
.uk-text-right { .uk-text-right {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
}
.preview-pane {
grid-column: 2;
margin: 80px 60px;
background: #fff;
border-radius: 5px;
box-shadow: 0 0px 6px rgba(0, 0, 0, 0.05);
}
.budibase__table {
border: 1px solid #ccc;
background: #fff;
border-radius: 2px;
}
.budibase__table thead {
background: #fafafa;
}
.budibase__table thead > tr > th {
color: var(--button-text);
text-transform: capitalize;
font-weight: 500;
}
.budibase__table tr {
border-bottom: 1px solid #ccc;
}
.button--toggled {
background: #fafafa;
color: var(--button-text);
padding: 10px;
} }

View File

@ -39,7 +39,7 @@ const css_map = {
}, },
direction: { direction: {
name: "flex-direction", name: "flex-direction",
generate: self generate: self,
}, },
gridarea: { gridarea: {
name: "grid-area", name: "grid-area",
@ -113,7 +113,7 @@ const object_to_css_string = [
export const generate_css = ({ layout, position }) => { export const generate_css = ({ layout, position }) => {
let _layout = pipe(layout, object_to_css_string) let _layout = pipe(layout, object_to_css_string)
if (_layout.length) { if (_layout.length) {
_layout += `\ndisplay: ${_layout.includes("flex") ? "flex" : "grid"};`; _layout += `\ndisplay: ${_layout.includes("flex") ? "flex" : "grid"};`
} }
return { return {

View File

@ -1,12 +1,14 @@
import getStore from "./store" import { getStore } from "./store"
import LogRocket from "logrocket"; import { getBackendUiStore } from "./store/backend"
import LogRocket from "logrocket"
export const store = getStore() export const store = getStore()
export const backendUiStore = getBackendUiStore()
export const initialise = async () => { export const initialise = async () => {
try { try {
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
LogRocket.init("knlald/budibase"); LogRocket.init("knlald/budibase")
} }
setupRouter(store) setupRouter(store)
await store.initialise() await store.initialise()

View File

@ -0,0 +1,355 @@
import { writable } from "svelte/store"
import api from "../api"
import { cloneDeep, sortBy, find, remove } from "lodash/fp"
import { hierarchy as hierarchyFunctions } from "../../../../core/src"
import {
getNode,
validate,
constructHierarchy,
templateApi,
isIndex,
canDeleteIndex,
canDeleteRecord,
} from "../../common/core"
export const getBackendUiStore = () => {
const INITIAL_BACKEND_UI_STATE = {
leftNavItem: "DATABASE",
selectedView: {
records: [],
name: "",
},
breadcrumbs: [],
selectedDatabase: {},
selectedModel: {},
}
const store = writable(INITIAL_BACKEND_UI_STATE)
store.actions = {
navigate: name => store.update(state => ({ ...state, leftNavItem: name })),
database: {
select: db =>
store.update(state => {
state.selectedDatabase = db
state.breadcrumbs = [db.name]
return state
}),
},
records: {
delete: () =>
store.update(state => {
state.selectedView = state.selectedView
return state
}),
view: record =>
store.update(state => {
state.breadcrumbs = [state.selectedDatabase.name, record.id]
return state
}),
select: record =>
store.update(state => {
state.selectedRecord = record
return state
}),
},
views: {
select: view =>
store.update(state => {
state.selectedView = view
return state
}),
},
modals: {
show: modal => store.update(state => ({ ...state, visibleModal: modal })),
hide: () => store.update(state => ({ ...state, visibleModal: null })),
},
users: {
create: user =>
store.update(state => {
state.users.push(user)
state.users = state.users
return state
}),
},
}
return store
}
// Store Actions
export const createShadowHierarchy = hierarchy =>
constructHierarchy(JSON.parse(JSON.stringify(hierarchy)))
export const createDatabaseForApp = store => appInstance => {
store.update(state => {
state.appInstances.push(appInstance)
return state
})
}
export const saveBackend = async state => {
await api.post(`/_builder/api/${state.appname}/backend`, {
appDefinition: {
hierarchy: state.hierarchy,
actions: state.actions,
triggers: state.triggers,
},
accessLevels: state.accessLevels,
})
const instances_currentFirst = state.selectedDatabase
? [
state.appInstances.find(i => i.id === state.selectedDatabase.id),
...state.appInstances.filter(i => i.id !== state.selectedDatabase.id),
]
: state.appInstances
for (let instance of instances_currentFirst) {
await api.post(
`/_builder/instance/${state.appname}/${instance.id}/api/upgradeData`,
{ newHierarchy: state.hierarchy, accessLevels: state.accessLevels }
)
}
}
export const newRecord = (store, useRoot) => () => {
store.update(state => {
state.currentNodeIsNew = true
const shadowHierarchy = createShadowHierarchy(state.hierarchy)
const parent = useRoot
? shadowHierarchy
: getNode(shadowHierarchy, state.currentNode.nodeId)
state.errors = []
state.currentNode = templateApi(shadowHierarchy).getNewRecordTemplate(
parent,
"",
true
)
return state
})
}
export const selectExistingNode = store => nodeId => {
store.update(state => {
state.currentNode = getNode(state.hierarchy, nodeId)
state.currentNodeIsNew = false
state.errors = []
return state
})
}
export const newIndex = (store, useRoot) => () => {
store.update(state => {
state.shadowHierarchy = createShadowHierarchy(state.hierarchy)
state.currentNodeIsNew = true
state.errors = []
const parent = useRoot
? state.shadowHierarchy
: getNode(state.shadowHierarchy, state.currentNode.nodeId)
state.currentNode = templateApi(state.shadowHierarchy).getNewIndexTemplate(
parent
)
return state
})
}
export const saveCurrentNode = store => () => {
store.update(state => {
const errors = validate.node(state.currentNode)
state.errors = errors
if (errors.length > 0) {
return state
}
const parentNode = getNode(
state.hierarchy,
state.currentNode.parent().nodeId
)
const existingNode = getNode(state.hierarchy, state.currentNode.nodeId)
let index = parentNode.children.length
if (existingNode) {
// remove existing
index = existingNode.parent().children.indexOf(existingNode)
if (isIndex(existingNode)) {
parentNode.indexes = parentNode.indexes.filter(
node => node.nodeId !== existingNode.nodeId
)
} else {
parentNode.children = parentNode.children.filter(
node => node.nodeId !== existingNode.nodeId
)
}
}
// should add node into existing hierarchy
const cloned = cloneDeep(state.currentNode)
templateApi(state.hierarchy).constructNode(parentNode, cloned)
if (isIndex(existingNode)) {
parentNode.children = sortBy("name", parentNode.children)
} else {
parentNode.indexes = sortBy("name", parentNode.indexes)
}
if (!existingNode && state.currentNode.type === "record") {
const defaultIndex = templateApi(state.hierarchy).getNewIndexTemplate(
cloned.parent()
)
defaultIndex.name = `all_${cloned.name}s`
defaultIndex.allowedRecordNodeIds = [cloned.nodeId]
}
state.currentNodeIsNew = false
saveBackend(state)
return state
})
}
export const deleteCurrentNode = store => () => {
store.update(state => {
const nodeToDelete = getNode(state.hierarchy, state.currentNode.nodeId)
state.currentNode = hierarchyFunctions.isRoot(nodeToDelete.parent())
? state.hierarchy.children.find(node => node !== state.currentNode)
: nodeToDelete.parent()
const isRecord = hierarchyFunctions.isRecord(nodeToDelete)
const check = isRecord
? canDeleteRecord(nodeToDelete)
: canDeleteIndex(nodeToDelete)
if (!check.canDelete) {
state.errors = check.errors.map(e => ({ error: e }))
return state
}
const recordOrIndexKey = isRecord ? "children" : "indexes"
// remove the selected record or index
const newCollection = remove(
node => node.nodeId === nodeToDelete.nodeId,
nodeToDelete.parent()[recordOrIndexKey]
)
nodeToDelete.parent()[recordOrIndexKey] = newCollection
state.errors = []
saveBackend(state)
return state
})
}
export const saveField = store => field => {
store.update(state => {
state.currentNode.fields = state.currentNode.fields.filter(
f => f.id !== field.id
)
templateApi(state.hierarchy).addField(state.currentNode, field)
return state
})
}
export const deleteField = store => field => {
store.update(state => {
state.currentNode.fields = state.currentNode.fields.filter(
f => f.name !== field.name
)
return state
})
}
const incrementAccessLevelsVersion = state => {
state.accessLevels.version = state.accessLevels.version
? state.accessLevels.version + 1
: 1
return state
}
export const saveLevel = store => (newLevel, isNew, oldLevel = null) => {
store.update(state => {
const levels = state.accessLevels.levels
const existingLevel = isNew
? null
: find(a => a.name === oldLevel.name)(levels)
if (existingLevel) {
state.accessLevels.levels = levels.map(level =>
level === existingLevel ? newLevel : level
)
} else {
state.accessLevels.levels.push(newLevel)
}
incrementAccessLevelsVersion(state)
saveBackend(state)
return state
})
}
export const deleteLevel = store => level => {
store.update(state => {
state.accessLevels.levels = state.accessLevels.levels.filter(
t => t.name !== level.name
)
incrementAccessLevelsVersion(s)
saveBackend(state)
return state
})
}
export const saveAction = store => (newAction, isNew, oldAction = null) => {
store.update(s => {
const existingAction = isNew
? null
: find(a => a.name === oldAction.name)(s.actions)
if (existingAction) {
s.actions = s.actions.map(action =>
action === existingAction ? newAction : action
)
} else {
s.actions.push(newAction)
}
saveBackend(s)
return s
})
}
export const deleteAction = store => action => {
store.update(state => {
state.actions = state.actions.filter(a => a.name !== action.name)
saveBackend(state)
return state
})
}
export const saveTrigger = store => (newTrigger, isNew, oldTrigger = null) => {
store.update(s => {
const existingTrigger = isNew
? null
: s.triggers.find(a => a.name === oldTrigger.name)
if (existingTrigger) {
s.triggers = s.triggers.map(a => (a === existingTrigger ? newTrigger : a))
} else {
s.triggers.push(newTrigger)
}
saveBackend(s)
return s
})
}
export const deleteTrigger = store => trigger => {
store.update(s => {
s.triggers = s.triggers.filter(t => t.name !== trigger.name)
return s
})
}

View File

@ -1,39 +1,23 @@
import { hierarchy as hierarchyFunctions } from "../../../core/src" import { filter, cloneDeep, last, concat, isEmpty, values } from "lodash/fp"
import { import { pipe, getNode, constructHierarchy } from "../../common/core"
filter, import * as backendStoreActions from "./backend"
cloneDeep,
sortBy,
map,
last,
concat,
find,
isEmpty,
values,
} from "lodash/fp"
import {
pipe,
getNode,
validate,
constructHierarchy,
templateApi,
} from "../common/core"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { defaultPagesObject } from "../userInterface/pagesParsing/defaultPagesObject" import { defaultPagesObject } from "../../userInterface/pagesParsing/defaultPagesObject"
import api from "./api" import api from "../api"
import { getExactComponent } from "../userInterface/pagesParsing/searchComponents" import { getExactComponent } from "../../userInterface/pagesParsing/searchComponents"
import { rename } from "../userInterface/pagesParsing/renameScreen" import { rename } from "../../userInterface/pagesParsing/renameScreen"
import { import {
getNewScreen, getNewScreen,
createProps, createProps,
makePropsSafe, makePropsSafe,
getBuiltin, getBuiltin,
} from "../userInterface/pagesParsing/createProps" } from "../../userInterface/pagesParsing/createProps"
import { expandComponentDefinition } from "../userInterface/pagesParsing/types" import { expandComponentDefinition } from "../../userInterface/pagesParsing/types"
import { loadLibs, libUrlsForPreview } from "./loadComponentLibraries" import { loadLibs, libUrlsForPreview } from "../loadComponentLibraries"
import { buildCodeForScreens } from "./buildCodeForScreens" import { buildCodeForScreens } from "../buildCodeForScreens"
import { generate_screen_css } from "./generate_css" import { generate_screen_css } from "../generate_css"
import { insertCodeMetadata } from "./insertCodeMetadata" import { insertCodeMetadata } from "../insertCodeMetadata"
import { uuid } from "./uuid" import { uuid } from "../uuid"
let appname = "" let appname = ""
@ -55,8 +39,6 @@ export const getStore = () => {
currentComponentProps: null, currentComponentProps: null,
currentNodeIsNew: false, currentNodeIsNew: false,
errors: [], errors: [],
activeNav: "database",
isBackend: true,
hasAppPackage: false, hasAppPackage: false,
accessLevels: { version: 0, levels: [] }, accessLevels: { version: 0, levels: [] },
currentNode: null, currentNode: null,
@ -68,23 +50,25 @@ export const getStore = () => {
const store = writable(initial) const store = writable(initial)
store.initialise = initialise(store, initial) store.initialise = initialise(store, initial)
store.newChildRecord = newRecord(store, false)
store.newRootRecord = newRecord(store, true) store.newChildRecord = backendStoreActions.newRecord(store, false)
store.selectExistingNode = selectExistingNode(store) store.newRootRecord = backendStoreActions.newRecord(store, true)
store.newChildIndex = newIndex(store, false) store.selectExistingNode = backendStoreActions.selectExistingNode(store)
store.newRootIndex = newIndex(store, true) store.newChildIndex = backendStoreActions.newIndex(store, false)
store.saveCurrentNode = saveCurrentNode(store) store.newRootIndex = backendStoreActions.newIndex(store, true)
store.saveCurrentNode = backendStoreActions.saveCurrentNode(store)
store.deleteCurrentNode = backendStoreActions.deleteCurrentNode(store)
store.saveField = backendStoreActions.saveField(store)
store.deleteField = backendStoreActions.deleteField(store)
store.saveLevel = backendStoreActions.saveLevel(store)
store.deleteLevel = backendStoreActions.deleteLevel(store)
store.createDatabaseForApp = backendStoreActions.createDatabaseForApp(store)
store.saveAction = backendStoreActions.saveAction(store)
store.deleteAction = backendStoreActions.deleteAction(store)
store.saveTrigger = backendStoreActions.saveTrigger(store)
store.deleteTrigger = backendStoreActions.deleteTrigger(store)
store.importAppDefinition = importAppDefinition(store) store.importAppDefinition = importAppDefinition(store)
store.deleteCurrentNode = deleteCurrentNode(store)
store.saveField = saveField(store)
store.deleteField = deleteField(store)
store.saveAction = saveAction(store)
store.deleteAction = deleteAction(store)
store.saveTrigger = saveTrigger(store)
store.deleteTrigger = deleteTrigger(store)
store.saveLevel = saveLevel(store)
store.deleteLevel = deleteLevel(store)
store.setActiveNav = setActiveNav(store)
store.saveScreen = saveScreen(store) store.saveScreen = saveScreen(store)
store.addComponentLibrary = addComponentLibrary(store) store.addComponentLibrary = addComponentLibrary(store)
store.renameScreen = renameScreen(store) store.renameScreen = renameScreen(store)
@ -96,8 +80,6 @@ export const getStore = () => {
store.addStylesheet = addStylesheet(store) store.addStylesheet = addStylesheet(store)
store.removeStylesheet = removeStylesheet(store) store.removeStylesheet = removeStylesheet(store)
store.savePage = savePage(store) store.savePage = savePage(store)
store.showFrontend = showFrontend(store)
store.showBackend = showBackend(store)
store.showSettings = showSettings(store) store.showSettings = showSettings(store)
store.useAnalytics = useAnalytics(store) store.useAnalytics = useAnalytics(store)
store.createGeneratedComponents = createGeneratedComponents(store) store.createGeneratedComponents = createGeneratedComponents(store)
@ -159,10 +141,6 @@ const initialise = (store, initial) => async () => {
} }
initial.appname = appname initial.appname = appname
initial.pages = pkg.pages initial.pages = pkg.pages
initial.currentInstanceId =
pkg.application.instances && pkg.application.instances.length > 0
? pkg.application.instances[0].id
: ""
initial.hasAppPackage = true initial.hasAppPackage = true
initial.hierarchy = pkg.appDefinition.hierarchy initial.hierarchy = pkg.appDefinition.hierarchy
initial.accessLevels = pkg.accessLevels initial.accessLevels = pkg.accessLevels
@ -174,12 +152,15 @@ const initialise = (store, initial) => async () => {
initial.builtins = [getBuiltin("##builtin/screenslot")] initial.builtins = [getBuiltin("##builtin/screenslot")]
initial.actions = values(pkg.appDefinition.actions) initial.actions = values(pkg.appDefinition.actions)
initial.triggers = pkg.appDefinition.triggers initial.triggers = pkg.appDefinition.triggers
initial.appInstances = pkg.application.instances
initial.appId = pkg.application.id
if (!!initial.hierarchy && !isEmpty(initial.hierarchy)) { if (!!initial.hierarchy && !isEmpty(initial.hierarchy)) {
initial.hierarchy = constructHierarchy(initial.hierarchy) initial.hierarchy = constructHierarchy(initial.hierarchy)
const shadowHierarchy = createShadowHierarchy(initial.hierarchy) const shadowHierarchy = createShadowHierarchy(initial.hierarchy)
if (initial.currentNode !== null) if (initial.currentNode !== null) {
initial.currentNode = getNode(shadowHierarchy, initial.currentNode.nodeId) initial.currentNode = getNode(shadowHierarchy, initial.currentNode.nodeId)
}
} }
store.set(initial) store.set(initial)
@ -187,121 +168,16 @@ const initialise = (store, initial) => async () => {
} }
const showSettings = store => () => { const showSettings = store => () => {
store.update(s => { store.update(state => {
s.showSettings = !s.showSettings state.showSettings = !state.showSettings
return s return state
}) })
} }
const useAnalytics = store => () => { const useAnalytics = store => () => {
store.update(s => { store.update(state => {
s.useAnalytics = !s.useAnalytics state.useAnalytics = !state.useAnalytics
return s return state
})
}
const showBackend = store => () => {
store.update(s => {
s.isBackend = true
return s
})
}
const showFrontend = store => () => {
store.update(s => {
s.isBackend = false
return s
})
}
const newRecord = (store, useRoot) => () => {
store.update(s => {
s.currentNodeIsNew = true
const shadowHierarchy = createShadowHierarchy(s.hierarchy)
const parent = useRoot
? shadowHierarchy
: getNode(shadowHierarchy, s.currentNode.nodeId)
s.errors = []
s.currentNode = templateApi(shadowHierarchy).getNewRecordTemplate(
parent,
"",
true
)
return s
})
}
const selectExistingNode = store => nodeId => {
store.update(s => {
const shadowHierarchy = createShadowHierarchy(s.hierarchy)
s.currentNode = getNode(shadowHierarchy, nodeId)
s.currentNodeIsNew = false
s.errors = []
s.activeNav = "database"
return s
})
}
const newIndex = (store, useRoot) => () => {
store.update(s => {
s.currentNodeIsNew = true
s.errors = []
const shadowHierarchy = createShadowHierarchy(s.hierarchy)
const parent = useRoot
? shadowHierarchy
: getNode(shadowHierarchy, s.currentNode.nodeId)
s.currentNode = templateApi(shadowHierarchy).getNewIndexTemplate(parent)
return s
})
}
const saveCurrentNode = store => () => {
store.update(s => {
const errors = validate.node(s.currentNode)
s.errors = errors
if (errors.length > 0) {
return s
}
const parentNode = getNode(s.hierarchy, s.currentNode.parent().nodeId)
const existingNode = getNode(s.hierarchy, s.currentNode.nodeId)
let index = parentNode.children.length
if (existingNode) {
// remove existing
index = existingNode.parent().children.indexOf(existingNode)
existingNode.parent().children = pipe(existingNode.parent().children, [
filter(c => c.nodeId !== existingNode.nodeId),
])
}
// should add node into existing hierarchy
const cloned = cloneDeep(s.currentNode)
templateApi(s.hierarchy).constructNode(parentNode, cloned)
const newIndexOfChild = child => {
if (child === cloned) return index
const currentIndex = parentNode.children.indexOf(child)
return currentIndex >= index ? currentIndex + 1 : currentIndex
}
parentNode.children = pipe(parentNode.children, [sortBy(newIndexOfChild)])
if (!existingNode && s.currentNode.type === "record") {
const defaultIndex = templateApi(s.hierarchy).getNewIndexTemplate(
cloned.parent()
)
defaultIndex.name = `all_${cloned.collectionName}`
defaultIndex.allowedRecordNodeIds = [cloned.nodeId]
}
s.currentNodeIsNew = false
saveBackend(s)
return s
}) })
} }
@ -319,143 +195,6 @@ const importAppDefinition = store => appDefinition => {
}) })
} }
const deleteCurrentNode = store => () => {
store.update(s => {
const nodeToDelete = getNode(s.hierarchy, s.currentNode.nodeId)
s.currentNode = hierarchyFunctions.isRoot(nodeToDelete.parent())
? find(n => n != s.currentNode)(s.hierarchy.children)
: nodeToDelete.parent()
if (hierarchyFunctions.isRecord(nodeToDelete)) {
nodeToDelete.parent().children = filter(
c => c.nodeId !== nodeToDelete.nodeId
)(nodeToDelete.parent().children)
} else {
nodeToDelete.parent().indexes = filter(
c => c.nodeId !== nodeToDelete.nodeId
)(nodeToDelete.parent().indexes)
}
s.errors = []
saveBackend(s)
return s
})
}
const saveField = databaseStore => field => {
databaseStore.update(db => {
db.currentNode.fields = filter(f => f.name !== field.name)(
db.currentNode.fields
)
templateApi(db.hierarchy).addField(db.currentNode, field)
return db
})
}
const deleteField = databaseStore => field => {
databaseStore.update(db => {
db.currentNode.fields = filter(f => f.name !== field.name)(
db.currentNode.fields
)
return db
})
}
const saveAction = store => (newAction, isNew, oldAction = null) => {
store.update(s => {
const existingAction = isNew
? null
: find(a => a.name === oldAction.name)(s.actions)
if (existingAction) {
s.actions = pipe(s.actions, [
map(a => (a === existingAction ? newAction : a)),
])
} else {
s.actions.push(newAction)
}
saveBackend(s)
return s
})
}
const deleteAction = store => action => {
store.update(s => {
s.actions = filter(a => a.name !== action.name)(s.actions)
saveBackend(s)
return s
})
}
const saveTrigger = store => (newTrigger, isNew, oldTrigger = null) => {
store.update(s => {
const existingTrigger = isNew
? null
: find(a => a.name === oldTrigger.name)(s.triggers)
if (existingTrigger) {
s.triggers = pipe(s.triggers, [
map(a => (a === existingTrigger ? newTrigger : a)),
])
} else {
s.triggers.push(newTrigger)
}
saveBackend(s)
return s
})
}
const deleteTrigger = store => trigger => {
store.update(s => {
s.triggers = filter(t => t.name !== trigger.name)(s.triggers)
return s
})
}
const incrementAccessLevelsVersion = s =>
(s.accessLevels.version = (s.accessLevels.version || 0) + 1)
const saveLevel = store => (newLevel, isNew, oldLevel = null) => {
store.update(s => {
const levels = s.accessLevels.levels
const existingLevel = isNew
? null
: find(a => a.name === oldLevel.name)(levels)
if (existingLevel) {
s.accessLevels.levels = pipe(levels, [
map(a => (a === existingLevel ? newLevel : a)),
])
} else {
s.accessLevels.levels.push(newLevel)
}
incrementAccessLevelsVersion(s)
saveBackend(s)
return s
})
}
const deleteLevel = store => level => {
store.update(s => {
s.accessLevels.levels = filter(t => t.name !== level.name)(
s.accessLevels.levels
)
incrementAccessLevelsVersion(s)
saveBackend(s)
return s
})
}
const setActiveNav = store => navName => {
store.update(s => {
s.activeNav = navName
return s
})
}
const createShadowHierarchy = hierarchy => const createShadowHierarchy = hierarchy =>
constructHierarchy(JSON.parse(JSON.stringify(hierarchy))) constructHierarchy(JSON.parse(JSON.stringify(hierarchy)))
@ -474,55 +213,27 @@ const _saveScreen = async (store, s, screen) => {
screen screen
) )
.then(() => { .then(() => {
if (currentPageScreens.includes(screen)) return
if(currentPageScreens.includes(screen)) return const screens = [...currentPageScreens, screen]
const screens = [
...currentPageScreens,
screen,
]
store.update(innerState => { store.update(innerState => {
innerState.pages[s.currentPageName]._screens = screens innerState.pages[s.currentPageName]._screens = screens
innerState.screens = screens innerState.screens = screens
innerState.currentPreviewItem = screen innerState.currentPreviewItem = screen
const safeProps = makePropsSafe( const safeProps = makePropsSafe(
getComponentDefinition(innerState.components, screen.props._component), getComponentDefinition(
innerState.components,
screen.props._component
),
screen.props screen.props
) )
innerState.currentComponentInfo = safeProps innerState.currentComponentInfo = safeProps
screen.props = safeProps screen.props = safeProps
_savePage(innerState) _savePage(innerState)
return innerState return innerState
}) })
/*const updatedScreen = await savedScreen.json()
const screens = [
...currentPageScreens.filter(
storeScreen => storeScreen.name !== updatedScreen.name
),
updatedScreen,
]
store.update(innerState => {
innerState.pages[s.currentPageName]._screens = screens
innerState.screens = screens
let curentComponentId
walkProps(screen.props, p => {
if(p === innerState.currentComponentInfo)
currentComponentId = p._id
})
innerState.currentPreviewItem = updatedScreen
innerState.currentComponentInfo = makePropsSafe(componentDef, component)
_savePage(innerState)
return innerState
})
*/
}) })
return s return s
@ -732,17 +443,6 @@ const _savePage = async s => {
}) })
} }
const saveBackend = async state => {
await api.post(`/_builder/api/${appname}/backend`, {
appDefinition: {
hierarchy: state.hierarchy,
actions: state.actions,
triggers: state.triggers,
},
accessLevels: state.accessLevels,
})
}
const setCurrentPage = store => pageName => { const setCurrentPage = store => pageName => {
store.update(s => { store.update(s => {
const current_screens = s.pages[pageName]._screens const current_screens = s.pages[pageName]._screens
@ -772,10 +472,7 @@ const setCurrentPage = store => pageName => {
}) })
} }
const getContainerComponent = components => const getComponentDefinition = (components, name) =>
getComponentDefinition(components, "@budibase/standard-components/container")
const getComponentDefinition = (components, name) =>
components.find(c => c.name === name) components.find(c => c.name === name)
/** /**
@ -834,8 +531,11 @@ const addTemplatedComponent = store => props => {
state.currentComponentInfo._children = state.currentComponentInfo._children.concat( state.currentComponentInfo._children = state.currentComponentInfo._children.concat(
props props
) )
state.currentPreviewItem._css = generate_screen_css([state.currentPreviewItem.props]) state.currentPreviewItem._css = generate_screen_css([
state.currentPreviewItem.props,
])
setCurrentPageFunctions(state)
_saveCurrentPreviewItem(state) _saveCurrentPreviewItem(state)
return state return state

View File

@ -2,36 +2,20 @@
import { JavaScriptIcon } from "../common/Icons" import { JavaScriptIcon } from "../common/Icons"
// todo: use https://ace.c9.io // todo: use https://ace.c9.io
export let text = "" export let text = ""
export let label = ""
export let javascript = false
</script> </script>
<div class="header">
{#if javascript}
<JavaScriptIcon />
{/if}
<span>{label}</span>
</div>
<textarea class="uk-textarea" bind:value={text} /> <textarea class="uk-textarea" bind:value={text} />
<style> <style>
textarea { textarea {
padding: 3px; padding: 10px;
margin-top: 5px; margin-top: 5px;
margin-bottom: 10px; margin-bottom: 10px;
background: var(--lightslate); background: var(--secondary80);
color: var(--white);
font-family: "Courier New", Courier, monospace; font-family: "Courier New", Courier, monospace;
width: 95%; width: 95%;
height: 100px; height: 100px;
border-radius: 5px; border-radius: 5px;
} }
span {
margin-left: 5px;
}
.header {
display: flex;
align-items: center;
}
</style> </style>

View File

@ -53,14 +53,12 @@
</div> </div>
<style> <style>
.uk-modal-footer {
background: var(--lightslate);
}
.uk-modal-footer { .uk-modal-dialog {
background: var(--lightslate); width: 400px;
} border-radius: 5px;
}
.uk-modal-dialog {
width: 400px;
border-radius: 5px;
}
</style> </style>

View File

@ -5,25 +5,9 @@
</script> </script>
{#if hasErrors} {#if hasErrors}
<div class="error-container"> <div uk-alert class="uk-alert-danger">
{#each errors as error} {#each errors as error}
<div class="error-row"> <div>{error.field ? `${error.field}: ` : ''}{error.error}</div>
{error.field ? `${error.field}: ` : ''}{error.error}
</div>
{/each} {/each}
</div> </div>
{/if} {/if}
<style>
.error-container {
padding: 10px;
border-style: solid;
border-color: var(--deletion100);
border-radius: var(--borderradiusall);
background: var(--deletion75);
}
.error-row {
padding: 5px 0px;
}
</style>

View File

@ -1,9 +1,12 @@
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="24" width="24"
height="24"> height="24">
<path fill="none" d="M0 0h24v24H0z"/> <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 10zM10.622 8.415a.4.4 0 00-.622.332v6.506a.4.4 0 00.622.332l4.879-3.252a.4.4 0 000-.666l-4.88-3.252z" <path
fill="currentColor"/> d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10
</svg> 10zM10.622 8.415a.4.4 0 00-.622.332v6.506a.4.4 0 00.622.332l4.879-3.252a.4.4
0 000-.666l-4.88-3.252z"
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 349 B

After

Width:  |  Height:  |  Size: 362 B

View File

@ -3,6 +3,9 @@
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="24" width="24"
height="24"> height="24">
<path d="M0 0h24v24H0z" fill="none"/> <path d="M0 0h24v24H0z" fill="none" />
<path d="M12 1l9.5 5.5v11L12 23l-9.5-5.5v-11L12 1zm0 14a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" fill="currentColor"/> <path
</svg> d="M12 1l9.5 5.5v11L12 23l-9.5-5.5v-11L12 1zm0 14a3 3 0 1 0 0-6 3 3 0 0 0 0
6z"
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 244 B

After

Width:  |  Height:  |  Size: 263 B

View File

@ -18,4 +18,3 @@ export { default as AddIcon } from "./Add.svelte"
export { default as JavaScriptIcon } from "./JavaScript.svelte" export { default as JavaScriptIcon } from "./JavaScript.svelte"
export { default as PreviewIcon } from "./Preview.svelte" export { default as PreviewIcon } from "./Preview.svelte"
export { default as SettingsIcon } from "./Settings.svelte" export { default as SettingsIcon } from "./Settings.svelte"

View File

@ -3,7 +3,7 @@
import ActionButton from "../common/ActionButton.svelte" import ActionButton from "../common/ActionButton.svelte"
export let isOpen = false export let isOpen = false
export let onClosed = () => {} export let onClosed
export let id = "" export let id = ""
export let title export let title
@ -27,19 +27,24 @@
</script> </script>
<div bind:this={ukModal} uk-modal {id}> <div bind:this={ukModal} uk-modal {id}>
<div class="uk-modal-dialog" uk-overflow-auto> {#if isOpen}
{#if title} <div class="uk-modal-dialog" uk-overflow-auto>
<div class="uk-modal-header"> {#if title}
<h4 class="budibase__title--4">{title}</h4> <div class="uk-modal-header">
</div> <h4 class="budibase__title--4">{title}</h4>
{/if} </div>
<div class="uk-modal-body">
{#if onClosed}
<button class="uk-modal-close-default" type="button" uk-close />
{/if} {/if}
<slot /> <div class="uk-modal-body">
{#if onClosed}
<button class="uk-modal-close-default" type="button" uk-close />
{/if}
<slot />
</div>
<div class="uk-modal-footer">
<slot name="footer" />
</div>
</div> </div>
</div> {/if}
</div> </div>
<style> <style>
@ -49,5 +54,6 @@
height: 80vh; height: 80vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0;
} }
</style> </style>

View File

@ -1,10 +1,15 @@
<script> <script>
import getIcon from "./icon" import getIcon from "./icon"
export let icon
export let value export let value
</script> </script>
<div class="select-container"> <div class="select-container">
<select on:change bind:value> {#if icon}
<i class={icon} />
{/if}
<select class:adjusted={icon} on:change bind:value>
<slot /> <slot />
</select> </select>
<span class="arrow"> <span class="arrow">
@ -14,12 +19,22 @@
<style> <style>
.select-container { .select-container {
padding-bottom: 10px;
font-size: 0.9rem; font-size: 0.9rem;
color: var(--secondary50); color: var(--secondary50);
font-weight: bold; font-weight: bold;
position: relative; position: relative;
max-width: 300px; max-width: 300px;
min-width: 200px;
}
.adjusted {
padding-left: 2.5em;
}
i {
position: absolute;
left: 8px;
top: 8px;
} }
select { select {
@ -42,7 +57,6 @@
.arrow { .arrow {
position: absolute; position: absolute;
right: 10px; right: 10px;
top: 0;
bottom: 0; bottom: 0;
margin: auto; margin: auto;
width: 30px; width: 30px;

View File

@ -25,6 +25,6 @@
<style> <style>
textarea { textarea {
width: 300px; width: 300px;
height: 200px; height: 100px;
} }
</style> </style>

View File

@ -9,8 +9,11 @@ import { find, filter, keyBy, flatten, map } from "lodash/fp"
import { generateSchema } from "../../../core/src/indexing/indexSchemaCreator" import { generateSchema } from "../../../core/src/indexing/indexSchemaCreator"
import { generate } from "shortid" import { generate } from "shortid"
export { canDeleteIndex } from "../../../core/src/templateApi/canDeleteIndex"
export { canDeleteRecord } from "../../../core/src/templateApi/canDeleteRecord"
export { userWithFullAccess } from "../../../core/src/index" export { userWithFullAccess } from "../../../core/src/index"
export { joinKey } from "../../../core/src/common"
export { getExactNodeForKey } from "../../../core/src/templateApi/hierarchy"
export const pipe = common.$ export const pipe = common.$
export const events = common.eventsList export const events = common.eventsList
@ -74,6 +77,9 @@ export const getPotentialReferenceIndexes = (hierarchy, record) =>
), ),
]) ])
export const isIndex = hierarchyFunctions.isIndex
export const isRecord = hierarchyFunctions.isRecord
export const getDefaultTypeOptions = type => export const getDefaultTypeOptions = type =>
!type ? {} : allTypes[type].getDefaultOptions() !type ? {} : allTypes[type].getDefaultOptions()
@ -118,6 +124,7 @@ export const getNewInstance = (appId, name) => {
version: { key: "" }, version: { key: "" },
isNew: true, isNew: true,
type: "instance", type: "instance",
datastoreconfig: "",
id, id,
name, name,
} }

View File

@ -1,70 +0,0 @@
<script>
import Button from "../common/Button.svelte"
import ActionButton from "../common/ActionButton.svelte"
import ButtonGroup from "../common/ButtonGroup.svelte"
import { store } from "../builderStore"
import Modal from "../common/Modal.svelte"
import ErrorsBox from "../common/ErrorsBox.svelte"
export let left
let confirmDelete = false
const openConfirmDelete = () => {
confirmDelete = true
}
const deleteCurrentNode = () => {
confirmDelete = false
store.deleteCurrentNode()
}
</script>
<div class="root" style="left: {left}">
<ButtonGroup>
<ActionButton
color="secondary"
grouped
on:click={store.saveCurrentNode}>
{#if $store.currentNodeIsNew}Create{:else}Update{/if}
</ActionButton>
{#if !$store.currentNodeIsNew}
<ActionButton alert grouped on:click={openConfirmDelete}>
Delete
</ActionButton>
{/if}
</ButtonGroup>
{#if !!$store.errors && $store.errors.length > 0}
<div style="width: 500px">
<ErrorsBox errors={$store.errors} />
</div>
{/if}
<Modal onClosed={() => (confirmDelete = false)} bind:isOpen={confirmDelete}>
<span>Are you sure you want to delete {$store.currentNode.name}?</span>
<div class="uk-modal-footer uk-text-right">
<ButtonGroup>
<ActionButton alert on:click={deleteCurrentNode}>Yes</ActionButton>
<ActionButton primary on:click={() => (confirmDelete = false)}>
No
</ActionButton>
</ButtonGroup>
</div>
</Modal>
</div>
<style>
.root {
padding: 1.5rem;
width: 100%;
align-items: right;
box-sizing: border-box;
}
.actions-modal-body {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -1,70 +1,83 @@
<script> <script>
import HierarchyRow from "./HierarchyRow.svelte" import ModelView from "./ModelView.svelte"
import RecordView from "./RecordView.svelte"
import IndexView from "./IndexView.svelte" import IndexView from "./IndexView.svelte"
import ActionsHeader from "./ActionsHeader.svelte" import ModelDataTable from "./ModelDataTable"
import { store } from "../builderStore" import { store, backendUiStore } from "../builderStore"
import getIcon from "../common/icon" import getIcon from "../common/icon"
import DropdownButton from "../common/DropdownButton.svelte" import DropdownButton from "../common/DropdownButton.svelte"
import { hierarchy as hierarchyFunctions } from "../../../core/src" import ActionButton from "../common/ActionButton.svelte"
import Modal from "../common/Modal.svelte"
import * as api from "./ModelDataTable/api"
import {
CreateEditRecordModal,
CreateEditModelModal,
CreateEditViewModal,
CreateDatabaseModal,
DeleteRecordModal,
CreateUserModal,
} from "./ModelDataTable/modals"
const hierarchyWidth = "200px" let selectedRecord
const defaultNewIndexActions = [ async function selectRecord(record) {
{ selectedRecord = await api.loadRecord(record.key, {
label: "New Root Index", appname: $store.appname,
onclick: store.newRootIndex, instanceId: $backendUiStore.selectedDatabase.id,
}, })
] }
const defaultNewRecordActions = [ function onClosed() {
{ backendUiStore.actions.modals.hide()
label: "New Root Record", }
onclick: store.newRootRecord,
},
]
let newIndexActions = defaultNewIndexActions $: recordOpen = $backendUiStore.visibleModal === "RECORD"
let newRecordActions = defaultNewRecordActions $: modelOpen = $backendUiStore.visibleModal === "MODEL"
$: viewOpen = $backendUiStore.visibleModal === "VIEW"
store.subscribe(db => { $: databaseOpen = $backendUiStore.visibleModal === "DATABASE"
if (!db.currentNode || hierarchyFunctions.isIndex(db.currentNode)) { $: deleteRecordOpen = $backendUiStore.visibleModal === "DELETE_RECORD"
newRecordActions = defaultNewRecordActions $: userOpen = $backendUiStore.visibleModal === "USER"
newIndexActions = defaultNewIndexActions $: breadcrumbs = $backendUiStore.breadcrumbs.join(" / ")
} else {
newRecordActions = [
...defaultNewRecordActions,
{
label: `New Child Record of ${db.currentNode.name}`,
onclick: store.newChildRecord,
},
]
newIndexActions = [
...defaultNewIndexActions,
{
label: `New Index on ${db.currentNode.name}`,
onclick: store.newChildIndex,
},
]
}
})
</script> </script>
<Modal isOpen={!!$backendUiStore.visibleModal} {onClosed}>
{#if recordOpen}
<CreateEditRecordModal record={selectedRecord} {onClosed} />
{/if}
{#if modelOpen}
<CreateEditModelModal {onClosed} />
{/if}
{#if viewOpen}
<CreateEditViewModal {onClosed} />
{/if}
{#if databaseOpen}
<CreateDatabaseModal {onClosed} />
{/if}
{#if deleteRecordOpen}
<DeleteRecordModal record={selectedRecord} {onClosed} />
{/if}
{#if userOpen}
<CreateUserModal {onClosed} />
{/if}
</Modal>
<div class="root"> <div class="root">
<div class="actions-header">
{#if $store.currentNode}
<ActionsHeader left={hierarchyWidth} />
{/if}
</div>
<div class="node-view"> <div class="node-view">
{#if !$store.currentNode} <div class="database-actions">
<h1 style="margin-left: 100px">:)</h1> <div class="budibase__label--big">{breadcrumbs}</div>
{:else if $store.currentNode.type === 'record'} {#if $backendUiStore.selectedDatabase.id}
<RecordView /> <ActionButton
{:else} primary
<IndexView /> on:click={() => {
{/if} selectedRecord = null
backendUiStore.actions.modals.show('RECORD')
}}>
Create new record
</ActionButton>
{/if}
</div>
{#if $backendUiStore.selectedDatabase.id}
<ModelDataTable {selectRecord} />
{:else}Please select a database{/if}
</div> </div>
</div> </div>
@ -74,12 +87,13 @@
position: relative; position: relative;
} }
.actions-header {
flex: 0 1 auto;
}
.node-view { .node-view {
overflow-y: auto; overflow-y: auto;
flex: 1 1 auto; flex: 1 1 auto;
} }
.database-actions {
display: flex;
justify-content: space-between;
}
</style> </style>

View File

@ -11,7 +11,6 @@
import DatePicker from "../common/DatePicker.svelte" import DatePicker from "../common/DatePicker.svelte"
import { import {
cloneDeep, cloneDeep,
assign,
keys, keys,
isNumber, isNumber,
includes, includes,
@ -60,7 +59,7 @@
errors = validate.field(allFields)(clonedField) errors = validate.field(allFields)(clonedField)
if (errors.length > 0) return if (errors.length > 0) return
field.typeOptions = cloneDeep(clonedField.typeOptions) field.typeOptions = cloneDeep(clonedField.typeOptions)
onFinished(assign(field)(clonedField)) onFinished({ ...field, ...clonedField })
} }
</script> </script>
@ -68,20 +67,14 @@
<ErrorsBox {errors} /> <ErrorsBox {errors} />
<form class="uk-form-horizontal"> <form class="uk-form-stacked">
<Textbox label="Name" bind:text={clonedField.name} />
<Dropdown <Dropdown
label="Type" label="Type"
bind:selected={clonedField.type} bind:selected={clonedField.type}
options={keys(allTypes)} options={keys(allTypes)}
on:change={typeChanged} /> on:change={typeChanged} />
{#if isNew}
<Textbox label="Field Name" bind:text={clonedField.name} />
{:else}
<div style="font-weight: bold">{clonedField.name}</div>
{/if}
<Textbox label="Label" bind:text={clonedField.label} /> <Textbox label="Label" bind:text={clonedField.label} />
{#if clonedField.type === 'string'} {#if clonedField.type === 'string'}
@ -89,7 +82,7 @@
label="Max Length" label="Max Length"
bind:value={clonedField.typeOptions.maxLength} /> bind:value={clonedField.typeOptions.maxLength} />
<ValuesList <ValuesList
label="Values (options)" label="Categories"
bind:values={clonedField.typeOptions.values} /> bind:values={clonedField.typeOptions.values} />
<Checkbox <Checkbox
label="Declared Values Only" label="Declared Values Only"
@ -144,12 +137,19 @@
{/if} {/if}
</form> </form>
<div class="uk-modal-footer uk-text-right"> <footer>
<ButtonGroup> <ActionButton primary on:click={save}>Save</ActionButton>
<ActionButton primary on:click={save}>Save</ActionButton> <ActionButton alert on:click={() => onFinished(false)}>Cancel</ActionButton>
<ActionButton alert on:click={() => onFinished(false)}> </footer>
Cancel
</ActionButton>
</ButtonGroup>
</div>
</div> </div>
<style>
footer {
position: absolute;
padding: 20px;
width: 100%;
bottom: 0;
left: 0;
background: #fafafa;
}
</style>

View File

@ -1,42 +0,0 @@
<script>
import { store } from "../builderStore"
import { cloneDeep } from "lodash/fp"
export let level = 0
export let node
</script>
<div class="root">
<div
class="title"
on:click={() => store.selectExistingNode(node.nodeId)}
style="padding-left: {20 + level * 20}px">
{node.name}
</div>
{#if node.children}
{#each node.children as child}
<svelte:self node={child} level={level + 1} />
{/each}
{/if}
</div>
<style>
.root {
display: block;
font-size: 0.9rem;
width: 100%;
cursor: pointer;
font-weight: bold;
}
.title {
font: var(--fontblack);
padding-top: 10px;
padding-right: 5px;
padding-bottom: 10px;
color: var(--secondary100);
}
.title:hover {
background-color: var(--secondary10);
}
</style>

View File

@ -4,31 +4,42 @@
import Button from "../common/Button.svelte" import Button from "../common/Button.svelte"
import Dropdown from "../common/Dropdown.svelte" import Dropdown from "../common/Dropdown.svelte"
import { store } from "../builderStore" import { store } from "../builderStore"
import { filter, some, map } from "lodash/fp" import { filter, some, map, compose } from "lodash/fp"
import { hierarchy as hierarchyFunctions, common } from "../../../core/src" import { hierarchy as hierarchyFunctions, common } from "../../../core/src"
import ErrorsBox from "../common/ErrorsBox.svelte"
import ActionButton from "../common/ActionButton.svelte"
const pipe = common.$ const SNIPPET_EDITORS = {
MAP: "Map",
FILTER: "Filter",
SHARD: "Shard Name",
}
let index let index
let indexableRecords = [] let indexableRecords = []
let currentSnippetEditor = SNIPPET_EDITORS.MAP
const indexableRecordsFromIndex = compose(
map(node => ({
node,
isallowed:
index.allowedRecordNodeIds &&
index.allowedRecordNodeIds.some(id => node.nodeId === id),
})),
filter(hierarchyFunctions.isRecord),
filter(hierarchyFunctions.isDecendant($store.currentNode.parent())),
hierarchyFunctions.getFlattenedHierarchy
)
store.subscribe($store => { store.subscribe($store => {
index = $store.currentNode index = $store.currentNode
indexableRecords = pipe($store.hierarchy, [ indexableRecords = indexableRecordsFromIndex($store.hierarchy)
hierarchyFunctions.getFlattenedHierarchy,
filter(hierarchyFunctions.isDecendant(index.parent())),
filter(hierarchyFunctions.isRecord),
map(n => ({
node: n,
isallowed: some(id => n.nodeId === id)(index.allowedRecordNodeIds),
})),
])
}) })
const toggleAllowedRecord = record => { const toggleAllowedRecord = record => {
if (record.isallowed) { if (record.isallowed) {
index.allowedRecordNodeIds = filter(id => id !== record.node.nodeId)( index.allowedRecordNodeIds = index.allowedRecordNodeIds.filter(
index.allowedRecordNodeIds id => id !== record.node.nodeId
) )
} else { } else {
index.allowedRecordNodeIds.push(record.node.nodeId) index.allowedRecordNodeIds.push(record.node.nodeId)
@ -36,28 +47,65 @@
} }
</script> </script>
<form class="uk-form-horizontal root"> <heading>
<Textbox bind:text={index.name} label="Name" /> <i class="ri-eye-line button--toggled" />
<h3 class="budibase__title--3">Create / Edit View</h3>
</heading>
<form class="uk-form-stacked root">
<h4 class="budibase__label--big">Settings</h4>
{#if $store.errors && $store.errors.length > 0}
<ErrorsBox errors={$store.errors} />
{/if}
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-2@s">
<Textbox bind:text={index.name} label="Name" />
</div>
<div class="uk-width-1-2@s">
<Dropdown
label="View Type"
bind:selected={index.indexType}
options={['ancestor', 'reference']} />
</div>
</div>
<div class="allowed-records"> <div class="allowed-records">
<div class="index-label">Records to Index</div> <div class="budibase__label--big">
Which models would you like to add to this view?
</div>
{#each indexableRecords as rec} {#each indexableRecords as rec}
<input <input
class="uk-checkbox"
type="checkbox" type="checkbox"
checked={rec.isallowed} checked={rec.isallowed}
on:change={() => toggleAllowedRecord(rec)} /> on:change={() => toggleAllowedRecord(rec)} />
<span>{rec.node.name}</span> <span class="checkbox-model-label">{rec.node.name}</span>
{/each} {/each}
</div> </div>
<Dropdown <h4 class="budibase__label--big">Snippets</h4>
label="Index Type" {#each Object.values(SNIPPET_EDITORS) as snippetType}
bind:selected={index.indexType} <span
options={['ancestor', 'reference']} /> class="snippet-selector__heading hoverable"
class:highlighted={currentSnippetEditor === snippetType}
on:click={() => (currentSnippetEditor = snippetType)}>
{snippetType}
</span>
{/each}
{#if currentSnippetEditor === SNIPPET_EDITORS.MAP}
<CodeArea bind:text={index.map} label="Map" />
{:else if currentSnippetEditor === SNIPPET_EDITORS.FILTER}
<CodeArea bind:text={index.filter} label="Filter" />
{:else if currentSnippetEditor === SNIPPET_EDITORS.SHARD}
<CodeArea bind:text={index.getShardName} label="Shard Name" />
{/if}
<CodeArea bind:text={index.map} javascript label="Map" /> <ActionButton color="secondary" on:click={store.saveCurrentNode}>
<CodeArea bind:text={index.filter} javascript label="Filter" /> Save
<CodeArea javascript bind:text={index.getShardName} label="Shard Name" /> </ActionButton>
{#if !$store.currentNodeIsNew}
<ActionButton alert on:click={store.deleteCurrentNode}>Delete</ActionButton>
{/if}
</form> </form>
@ -75,8 +123,25 @@
margin-right: 30px; margin-right: 30px;
} }
.index-label { .snippet-selector__heading {
color: #333; margin-right: 20px;
font-size: 0.875rem; opacity: 0.7;
}
.highlighted {
opacity: 1;
}
.checkbox-model-label {
text-transform: capitalize;
}
h3 {
margin: 0 0 0 10px;
}
heading {
display: flex;
align-items: center;
} }
</style> </style>

View File

@ -0,0 +1,203 @@
<script>
import { onMount } from "svelte"
import { store, backendUiStore } from "../../builderStore"
import {
tap,
get,
find,
last,
compose,
flatten,
map,
remove,
keys,
} from "lodash/fp"
import Select from "../../common/Select.svelte"
import { getIndexSchema } from "../../common/core"
import ActionButton from "../../common/ActionButton.svelte"
import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal } from "./modals"
import * as api from "./api"
export let selectRecord
const ITEMS_PER_PAGE = 10
// Internal headers we want to hide from the user
const INTERNAL_HEADERS = ["key", "sortKey", "type", "id", "isNew"]
let modalOpen = false
let data = []
let headers = []
let views = []
let currentPage = 0
$: views = $backendUiStore.selectedRecord
? childViewsForRecord($store.hierarchy)
: $store.hierarchy.indexes
$: currentAppInfo = {
appname: $store.appname,
instanceId: $backendUiStore.selectedDatabase.id,
}
$: fetchRecordsForView(
$backendUiStore.selectedView,
$backendUiStore.selectedDatabase
).then(records => {
data = records || []
headers = hideInternalHeaders($backendUiStore.selectedView)
})
$: paginatedData = data.slice(
currentPage * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
)
const getSchema = getIndexSchema($store.hierarchy)
const childViewsForRecord = compose(flatten, map("indexes"), get("children"))
const hideInternalHeaders = compose(
remove(headerName => INTERNAL_HEADERS.includes(headerName)),
map(get("name")),
getSchema
)
async function fetchRecordsForView(view, instance) {
if (!view || !view.name) return
const viewName = $backendUiStore.selectedRecord
? `${$backendUiStore.selectedRecord.key}/${view.name}`
: view.name
return await api.fetchDataForView(viewName, {
appname: $store.appname,
instanceId: instance.id,
})
}
function drillIntoRecord(record) {
backendUiStore.update(state => {
state.selectedRecord = record
state.breadcrumbs = [state.selectedDatabase.name, record.id]
state.selectedView = childViewsForRecord($store.hierarchy)[0]
return state
})
}
onMount(() => {
if (views.length) {
backendUiStore.actions.views.select(views[0])
}
})
</script>
<section>
<div class="table-controls">
<h4 class="budibase__title--3">{last($backendUiStore.breadcrumbs)}</h4>
<Select icon="ri-eye-line" bind:value={$backendUiStore.selectedView}>
{#each views as view}
<option value={view}>{view.name}</option>
{/each}
</Select>
</div>
<table class="uk-table">
<thead>
<tr>
<th>Edit</th>
{#each headers as header}
<th>{header}</th>
{/each}
</tr>
</thead>
<tbody>
{#if paginatedData.length === 0}
<div class="no-data">No Data.</div>
{/if}
{#each paginatedData as row}
<tr class="hoverable">
<td>
<div class="uk-inline">
<i class="ri-more-line" />
<div uk-dropdown="mode: click">
<ul class="uk-nav uk-dropdown-nav">
<li>
<div on:click={() => drillIntoRecord(row)}>View</div>
</li>
<li
on:click={() => {
selectRecord(row)
backendUiStore.actions.modals.show('RECORD')
}}>
<div>Edit</div>
</li>
<li>
<div
on:click={() => {
selectRecord(row)
backendUiStore.actions.modals.show('DELETE_RECORD')
}}>
Delete
</div>
</li>
</ul>
</div>
</div>
</td>
{#each headers as header}
<td>{row[header]}</td>
{/each}
</tr>
{/each}
</tbody>
</table>
<TablePagination
{data}
bind:currentPage
pageItemCount={data.length}
{ITEMS_PER_PAGE} />
</section>
<style>
table {
border: 1px solid #ccc;
background: #fff;
border-radius: 3px;
border-collapse: collapse;
}
thead {
background: var(--background-button);
}
thead th {
color: var(--button-text);
text-transform: capitalize;
font-weight: 500;
}
tbody tr {
border-bottom: 1px solid #ccc;
transition: 0.3s background-color;
color: var(--darkslate);
}
tbody tr:hover {
background: #fafafa;
}
.table-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
.ri-more-line:hover,
.uk-dropdown-nav li:hover {
cursor: pointer;
}
.no-data {
padding: 20px;
}
</style>

View File

@ -0,0 +1,77 @@
<script>
import { backendUiStore } from "../../builderStore"
export let data
export let currentPage
export let pageItemCount
export let ITEMS_PER_PAGE
let numPages = 0
$: numPages = Math.ceil(data.length / ITEMS_PER_PAGE)
const next = () => {
if (currentPage + 1 === numPages) return
currentPage = currentPage + 1
}
const previous = () => {
if (currentPage == 0) return
currentPage = currentPage - 1
}
const selectPage = page => {
currentPage = page
}
</script>
<div class="pagination">
<div class="pagination__buttons">
<button on:click={previous}>Previous</button>
<button on:click={next}>Next</button>
{#each Array(numPages) as _, idx}
<button
class:selected={idx === currentPage}
on:click={() => selectPage(idx)}>
{idx + 1}
</button>
{/each}
</div>
<p>Showing {pageItemCount} of {data.length} entries</p>
</div>
<style>
.pagination {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.pagination__buttons {
display: flex;
}
.pagination__buttons button {
display: inline-block;
padding: 10px;
margin: 0;
background: #fff;
border: 1px solid #ccc;
text-transform: capitalize;
border-radius: 3px;
font-family: Roboto;
min-width: 20px;
transition: 0.3s background-color;
}
.pagination__buttons button:hover {
cursor: pointer;
background-color: #fafafa;
}
.selected {
color: var(--button-text);
}
</style>

View File

@ -0,0 +1,59 @@
import api from "../../builderStore/api"
import { getNewRecord, getNewInstance } from "../../common/core"
export async function createUser(password, user, { appname, instanceId }) {
const CREATE_USER_URL = `/_builder/instance/${appname}/${instanceId}/api/createUser`
const response = await api.post(CREATE_USER_URL, { user, password })
return await response.json()
}
export async function createDatabase(appname, instanceName) {
const CREATE_DATABASE_URL = `/_builder/instance/_master/0/api/record/`
const database = getNewInstance(appname, instanceName)
const response = await api.post(CREATE_DATABASE_URL, database)
return await response.json()
}
export async function deleteRecord(record, { appname, instanceId }) {
const DELETE_RECORDS_URL = `/_builder/instance/${appname}/${instanceId}/api/record${record.key}`
const response = await api.delete(DELETE_RECORDS_URL)
return response
}
export async function loadRecord(key, { appname, instanceId }) {
const LOAD_RECORDS_URL = `/_builder/instance/${appname}/${instanceId}/api/record${key}`
const response = await api.get(LOAD_RECORDS_URL)
return await response.json()
}
export async function saveRecord(record, { appname, instanceId }) {
let recordBase = { ...record }
// brand new record
// car-model-id or name/specific-car-id/manus
if (record.collectionName) {
const collectionKey = `/${record.collectionName}`
recordBase = getNewRecord(recordBase, collectionKey)
recordBase = overwritePresentProperties(recordBase, record)
}
const SAVE_RECORDS_URL = `/_builder/instance/${appname}/${instanceId}/api/record/`
const response = await api.post(SAVE_RECORDS_URL, recordBase)
return await response.json()
}
export async function fetchDataForView(viewName, { appname, instanceId }) {
const FETCH_RECORDS_URL = `/_builder/instance/${appname}/${instanceId}/api/listRecords/${viewName}`
const response = await api.get(FETCH_RECORDS_URL)
return await response.json()
}
function overwritePresentProperties(baseObj, overwrites) {
const base = { ...baseObj }
for (let key in base) {
if (overwrites[key]) base[key] = overwrites[key]
}
return base
}

View File

@ -0,0 +1 @@
export { default } from "./ModelDataTable.svelte"

View File

@ -0,0 +1,38 @@
<script>
import Modal from "../../../common/Modal.svelte"
import { store } from "../../../builderStore"
import ActionButton from "../../../common/ActionButton.svelte"
import * as api from "../api"
export let onClosed
let databaseName
async function createDatabase() {
const response = await api.createDatabase($store.appId, databaseName)
store.createDatabaseForApp(response)
onClosed()
}
</script>
<section>
Database Name
<input class="uk-input" type="text" bind:value={databaseName} />
<footer>
<ActionButton alert on:click={onClosed}>Cancel</ActionButton>
<ActionButton disabled={!databaseName} on:click={createDatabase}>
Save
</ActionButton>
</footer>
</section>
<style>
footer {
position: absolute;
padding: 20px;
width: 100%;
bottom: 0;
left: 0;
background: #fafafa;
}
</style>

View File

@ -0,0 +1,11 @@
<script>
import Modal from "../../../common/Modal.svelte"
import ActionButton from "../../../common/ActionButton.svelte"
import { backendUiStore } from "../../../builderStore"
import ModelView from "../../ModelView.svelte"
import * as api from "../api"
</script>
<section>
<ModelView />
</section>

View File

@ -0,0 +1,107 @@
<script>
import { onMount } from "svelte"
import { store, backendUiStore } from "../../../builderStore"
import { compose, map, get, flatten } from "lodash/fp"
import Modal from "../../../common/Modal.svelte"
import ActionButton from "../../../common/ActionButton.svelte"
import Select from "../../../common/Select.svelte"
import {
getNewRecord,
joinKey,
getExactNodeForKey,
} from "../../../common/core"
import RecordFieldControl from "./RecordFieldControl.svelte"
import * as api from "../api"
import ErrorsBox from "../../../common/ErrorsBox.svelte"
export let record
export let onClosed
let errors = []
const childModelsForModel = compose(flatten, map("children"), get("children"))
$: currentAppInfo = {
appname: $store.appname,
instanceId: $backendUiStore.selectedDatabase.id,
}
$: models = $backendUiStore.selectedRecord
? childModelsForModel($store.hierarchy)
: $store.hierarchy.children
let selectedModel
$: {
if (record) {
selectedModel = getExactNodeForKey($store.hierarchy)(record.key)
} else {
selectedModel = selectedModel || models[0]
}
}
$: modelFields = selectedModel ? selectedModel.fields : []
function getCurrentCollectionKey(selectedRecord) {
return selectedRecord
? joinKey(selectedRecord.key, selectedModel.collectionName)
: joinKey(selectedModel.collectionName)
}
$: editingRecord =
record ||
editingRecord ||
getNewRecord(
selectedModel,
getCurrentCollectionKey($backendUiStore.selectedRecord)
)
function closed() {
editingRecord = null
onClosed()
}
async function saveRecord() {
const recordResponse = await api.saveRecord(editingRecord, currentAppInfo)
backendUiStore.update(state => {
state.selectedView = state.selectedView
return state
})
closed()
}
</script>
<div>
<h4 class="budibase__title--4">Create / Edit Record</h4>
<ErrorsBox {errors} />
<div class="actions">
<form class="uk-form-stacked">
{#if !record}
<div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Model</label>
<Select bind:value={selectedModel}>
{#each models as model}
<option value={model}>{model.name}</option>
{/each}
</Select>
</div>
{/if}
{#each modelFields || [] as field}
<RecordFieldControl record={editingRecord} {field} {errors} />
{/each}
</form>
<footer>
<ActionButton alert on:click={onClosed}>Cancel</ActionButton>
<ActionButton on:click={saveRecord}>Save</ActionButton>
</footer>
</div>
</div>
<style>
footer {
position: absolute;
padding: 20px;
width: 100%;
bottom: 0;
left: 0;
background: #fafafa;
}
</style>

View File

@ -0,0 +1,8 @@
<script>
import IndexView from "../../IndexView.svelte"
import * as api from "../api"
</script>
<section>
<IndexView />
</section>

View File

@ -0,0 +1,64 @@
<script>
import Modal from "../../../common/Modal.svelte"
import { store, backendUiStore } from "../../../builderStore"
import ActionButton from "../../../common/ActionButton.svelte"
import * as api from "../api"
export let onClosed
let username
let password
let accessLevels = []
$: valid = username && password && accessLevels.length
$: currentAppInfo = {
appname: $store.appname,
instanceId: $backendUiStore.selectedDatabase.id,
}
async function createUser() {
const user = {
name: username,
accessLevels,
enabled: true,
temporaryAccessId: "",
}
const response = await api.createUser(password, user, currentAppInfo)
backendUiStore.actions.users.save(user)
onClosed()
}
</script>
<form class="uk-form-stacked">
<label class="uk-form-label" for="form-stacked-text">Username</label>
<input class="uk-input" type="text" bind:value={username} />
<label class="uk-form-label" for="form-stacked-text">Password</label>
<input class="uk-input" type="password" bind:value={password} />
<label class="uk-form-label" for="form-stacked-text">Access Levels</label>
<select multiple bind:value={accessLevels}>
{#each $store.accessLevels.levels as level}
<option value={level.name}>{level.name}</option>
{/each}
</select>
<footer>
<ActionButton alert on:click={onClosed}>Cancel</ActionButton>
<ActionButton disabled={!valid} on:click={createUser}>Save</ActionButton>
</footer>
</form>
<style>
footer {
position: absolute;
padding: 20px;
width: 100%;
bottom: 0;
left: 0;
background: #fafafa;
}
select {
width: 100%;
}
option {
padding: 10px;
}
</style>

View File

@ -0,0 +1,67 @@
<script>
import Modal from "../../../common/Modal.svelte"
import ActionButton from "../../../common/ActionButton.svelte"
import { store, backendUiStore } from "../../../builderStore"
import * as api from "../api"
export let record
$: currentAppInfo = {
appname: $store.appname,
instanceId: $backendUiStore.selectedDatabase.id,
}
function onClosed() {
backendUiStore.actions.modals.hide()
}
</script>
<section>
<heading>
<i class="ri-information-line alert" />
<h4 class="budibase__title--4">Delete Record</h4>
</heading>
<p>
Are you sure you want to delete this record? All of your data will be
permanently removed. This action cannot be undone.
</p>
<div class="modal-actions">
<ActionButton on:click={onClosed}>Cancel</ActionButton>
<ActionButton
alert
on:click={async () => {
await api.deleteRecord(record, currentAppInfo)
backendUiStore.actions.records.delete(record)
onClosed()
}}>
Delete
</ActionButton>
</div>
</section>
<style>
.alert {
color: rgba(255, 0, 31, 1);
background: #fafafa;
padding: 5px;
}
.modal-actions {
padding: 10px;
position: absolute;
bottom: 0;
left: 0;
background: #fafafa;
border-top: 1px solid #ccc;
width: 100%;
}
heading {
display: flex;
align-items: center;
}
h4 {
margin: 0 0 0 10px;
}
</style>

View File

@ -0,0 +1,66 @@
<script>
import Select from "../../../common/Select.svelte"
export let record
export let field
export let errors
$: isDropdown =
field.type === "string" &&
field.typeOptions.values &&
field.typeOptions.values.length > 0
$: isNumber = field.type === "number"
$: isText = field.type === "string" && !isDropdown
$: isCheckbox = field.type === "bool"
$: isError = errors && errors.some(e => e.field && e.field === field.name)
$: isDatetime = field.type === "datetime"
</script>
<div class="uk-margin">
{#if !isCheckbox}
<label class="uk-form-label" for={field.name}>{field.label}</label>
{/if}
<div class="uk-form-controls">
{#if isDropdown}
<Select bind:value={record[field.name]}>
<option value="" />
{#each field.typeOptions.values as val}
<option value={val}>{val}</option>
{/each}
</Select>
{:else if isText}
<input
class="uk-input"
class:uk-form-danger={isError}
id={field.name}
type="text"
bind:value={record[field.name]} />
{:else if isNumber}
<input
class="uk-input"
class:uk-form-danger={isError}
type="number"
bind:value={record[field.name]} />
{:else if isDatetime}
<input
class="uk-input"
class:uk-form-danger={isError}
type="date"
bind:value={record[field.name]} />
{:else if isCheckbox}
<label>
<input
class="uk-checkbox"
class:uk-form-danger={isError}
type="checkbox"
bind:checked={record[field.name]} />
{field.label}
</label>
{/if}
</div>
</div>

View File

@ -0,0 +1,6 @@
export { default as DeleteRecordModal } from "./DeleteRecord.svelte"
export { default as CreateEditRecordModal } from "./CreateEditRecord.svelte"
export { default as CreateEditModelModal } from "./CreateEditModel.svelte"
export { default as CreateEditViewModal } from "./CreateEditView.svelte"
export { default as CreateDatabaseModal } from "./CreateDatabase.svelte"
export { default as CreateUserModal } from "./CreateUser.svelte"

View File

@ -0,0 +1,238 @@
<script>
import { tick } from "svelte"
import Textbox from "../common/Textbox.svelte"
import Button from "../common/Button.svelte"
import Select from "../common/Select.svelte"
import ActionButton from "../common/ActionButton.svelte"
import getIcon from "../common/icon"
import FieldView from "./FieldView.svelte"
import Modal from "../common/Modal.svelte"
import {
get,
compose,
map,
join,
filter,
some,
find,
keys,
isDate,
} from "lodash/fp"
import { store, backendUiStore } from "../builderStore"
import { common, hierarchy } from "../../../core/src"
import { getNode } from "../common/core"
import { templateApi, pipe, validate } from "../common/core"
import ErrorsBox from "../common/ErrorsBox.svelte"
let record
let getIndexAllowedRecords
let editingField = false
let fieldToEdit
let isNewField = false
let newField
let editField
let deleteField
let onFinishedFieldEdit
let editIndex
$: models = $store.hierarchy.children
$: parent = record && record.parent()
$: isChildModel = parent && parent.name !== "root"
$: modelExistsInHierarchy =
$store.currentNode && getNode($store.hierarchy, $store.currentNode.nodeId)
store.subscribe($store => {
record = $store.currentNode
const flattened = hierarchy.getFlattenedHierarchy($store.hierarchy)
getIndexAllowedRecords = compose(
join(", "),
map(id => flattened.find(n => n.nodeId === id).name),
filter(id => flattened.some(n => n.nodeId === id)),
get("allowedRecordNodeIds")
)
newField = () => {
isNewField = true
fieldToEdit = templateApi($store.hierarchy).getNewField("string")
editingField = true
}
onFinishedFieldEdit = field => {
if (field) {
store.saveField(field)
}
editingField = false
}
editField = field => {
isNewField = false
fieldToEdit = field
editingField = true
}
deleteField = field => {
store.deleteField(field)
}
editIndex = index => {
store.selectExistingNode(index.nodeId)
}
})
let getTypeOptionsValueText = value => {
if (
value === Number.MAX_SAFE_INTEGER ||
value === Number.MIN_SAFE_INTEGER ||
new Date(value).getTime() === new Date(8640000000000000).getTime() ||
new Date(value).getTime() === new Date(-8640000000000000).getTime()
)
return "(any)"
if (value === null) return "(not set)"
return value
}
const nameChanged = ev => {
const pluralName = n => `${n}s`
if (record.collectionName === "") {
record.collectionName = pluralName(ev.target.value)
}
}
</script>
<div class="root">
<heading>
{#if !editingField}
<i class="ri-list-settings-line button--toggled" />
<h3 class="budibase__title--3">Create / Edit Model</h3>
{:else}
<i class="ri-file-list-line button--toggled" />
<h3 class="budibase__title--3">Create / Edit Field</h3>
{/if}
</heading>
{#if !editingField}
<h4 class="budibase__label--big">Settings</h4>
{#if $store.errors && $store.errors.length > 0}
<ErrorsBox errors={$store.errors} />
{/if}
<form class="uk-form-stacked">
<Textbox label="Name" bind:text={record.name} on:change={nameChanged} />
{#if isChildModel}
<div>
<label class="uk-form-label">Parent</label>
<div class="uk-form-controls parent-name">{parent.name}</div>
</div>
{/if}
</form>
<div class="table-controls">
<span class="budibase__label--big">Fields</span>
<h4 class="hoverable new-field" on:click={newField}>Add new field</h4>
</div>
<table class="uk-table fields-table budibase__table">
<thead>
<tr>
<th>Edit</th>
<th>Name</th>
<th>Type</th>
<th>Values</th>
<th />
</tr>
</thead>
<tbody>
{#each record ? record.fields : [] as field}
<tr>
<td>
<i class="ri-more-line" on:click={() => editField(field)} />
</td>
<td>
<div>{field.name}</div>
</td>
<td>{field.type}</td>
<td>{field.typeOptions.values || ""}</td>
<td>
<i
class="ri-delete-bin-6-line hoverable"
on:click={() => deleteField(field)} />
</td>
</tr>
{/each}
</tbody>
</table>
<div class="uk-margin">
<ActionButton color="secondary" on:click={store.saveCurrentNode}>
Save
</ActionButton>
{#if modelExistsInHierarchy}
<ActionButton color="primary" on:click={store.newChildRecord}>
Create Child Model on {record.name}
</ActionButton>
<ActionButton
color="primary"
on:click={async () => {
backendUiStore.actions.modals.show('VIEW')
await tick()
store.newChildIndex()
}}>
Create Child View on {record.name}
</ActionButton>
<ActionButton alert on:click={store.deleteCurrentNode}>Delete</ActionButton>
{/if}
</div>
{:else}
<FieldView
field={fieldToEdit}
onFinished={onFinishedFieldEdit}
allFields={record.fields}
store={$store} />
{/if}
</div>
<style>
.root {
height: 100%;
}
.new-field {
font-size: 16px;
font-weight: bold;
color: var(--button-text);
}
.fields-table {
margin: 1rem 1rem 0rem 0rem;
border-collapse: collapse;
}
tbody > tr:hover {
background-color: var(--primary10);
}
.table-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
.ri-more-line:hover {
cursor: pointer;
}
heading {
display: flex;
align-items: center;
}
h3 {
margin: 0 0 0 10px;
}
.parent-name {
font-weight: bold;
}
</style>

View File

@ -1,301 +0,0 @@
<script>
import Textbox from "../common/Textbox.svelte"
import Button from "../common/Button.svelte"
import getIcon from "../common/icon"
import FieldView from "./FieldView.svelte"
import Modal from "../common/Modal.svelte"
import { map, join, filter, some, find, keys, isDate } from "lodash/fp"
import { store } from "../builderStore"
import { common, hierarchy as h } from "../../../core/src"
import { templateApi, pipe, validate } from "../common/core"
let record
let getIndexAllowedRecords
let editingField = false
let fieldToEdit
let isNewField = false
let newField
let editField
let deleteField
let onFinishedFieldEdit
let editIndex
store.subscribe($store => {
record = $store.currentNode
const flattened = h.getFlattenedHierarchy($store.hierarchy)
getIndexAllowedRecords = index =>
pipe(index.allowedRecordNodeIds, [
filter(id => some(n => n.nodeId === id)(flattened)),
map(id => find(n => n.nodeId === id)(flattened).name),
join(", "),
])
newField = () => {
isNewField = true
fieldToEdit = templateApi($store.hierarchy).getNewField("string")
editingField = true
}
onFinishedFieldEdit = field => {
if (field) {
store.saveField(field)
}
editingField = false
}
editField = field => {
isNewField = false
fieldToEdit = field
editingField = true
}
deleteField = field => {
store.deleteField(field)
}
editIndex = index => {
store.selectExistingNode(index.nodeId)
}
})
let getTypeOptionsValueText = value => {
if (
value === Number.MAX_SAFE_INTEGER ||
value === Number.MIN_SAFE_INTEGER ||
new Date(value).getTime() === new Date(8640000000000000).getTime() ||
new Date(value).getTime() === new Date(-8640000000000000).getTime()
)
return "(any)"
if (value === null) return "(not set)"
return value
}
let getTypeOptions = typeOptions =>
pipe(typeOptions, [
keys,
map(
k =>
`<span style="color:var(--slate)">${k}: </span>${getTypeOptionsValueText(
typeOptions[k]
)}`
),
join("<br>"),
])
const nameChanged = ev => {
const pluralName = n => `${n}s`
if (record.collectionName === "") {
record.collectionName = pluralName(ev.target.value)
}
}
</script>
<div class="root">
<form class="uk-form-horizontal">
<h3 class="budibase__title--3">Settings</h3>
<Textbox label="Name:" bind:text={record.name} on:change={nameChanged} />
{#if !record.isSingle}
<Textbox label="Collection Name:" bind:text={record.collectionName} />
<Textbox
label="Estimated Record Count:"
bind:text={record.estimatedRecordCount} />
{/if}
<div class="recordkey">{record.nodeKey()}</div>
</form>
<h3 class="budibase__title--3">
Fields
<span class="add-field-button" on:click={newField}>
{@html getIcon('plus')}
</span>
</h3>
{#if record.fields.length > 0}
<table class="fields-table uk-table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Options</th>
<th />
</tr>
</thead>
<tbody>
{#each record.fields as field}
<tr>
<td>
<div class="field-label">{field.label}</div>
<div style="font-size: 0.8em; color: var(--slate)">
{field.name}
</div>
</td>
<td>{field.type}</td>
<td>
{@html getTypeOptions(field.typeOptions)}
</td>
<td>
<span class="edit-button" on:click={() => editField(field)}>
{@html getIcon('edit')}
</span>
<span class="edit-button" on:click={() => deleteField(field)}>
{@html getIcon('trash')}
</span>
</td>
</tr>
{/each}
</tbody>
</table>
{:else}(no fields added){/if}
{#if editingField}
<Modal
title="Manage Index Fields"
bind:isOpen={editingField}
onClosed={() => onFinishedFieldEdit(false)}>
<FieldView
field={fieldToEdit}
onFinished={onFinishedFieldEdit}
allFields={record.fields}
store={$store} />
</Modal>
{/if}
<h3 class="budibase__title--3">Indexes</h3>
{#each record.indexes as index}
<div class="index-container">
<div class="index-name">
{index.name}
<span style="margin-left: 7px" on:click={() => editIndex(index)}>
{@html getIcon('edit')}
</span>
</div>
<div class="index-field-row">
<span class="index-label">records indexed:</span>
<span>{getIndexAllowedRecords(index)}</span>
<span class="index-label" style="margin-left: 15px">type:</span>
<span>{index.indexType}</span>
</div>
<div class="index-field-row">
<span class="index-label">map:</span>
<code class="index-mapfilter">{index.map}</code>
</div>
{#if index.filter}
<div class="index-field-row">
<span class="index-label">filter:</span>
<code class="index-mapfilter">{index.filter}</code>
</div>
{/if}
</div>
{:else}
<div class="no-indexes">No indexes added.</div>
{/each}
</div>
<style>
.root {
height: 100%;
padding: 2rem;
}
.recordkey {
font-size: 14px;
font-weight: 600;
color: var(--primary100);
}
.fields-table {
margin: 1rem 1rem 0rem 0rem;
border-collapse: collapse;
}
.add-field-button {
cursor: pointer;
}
.edit-button {
cursor: pointer;
color: var(--secondary25);
}
.edit-button:hover {
cursor: pointer;
color: var(--secondary75);
}
th {
text-align: left;
}
td {
padding: 1rem 5rem 1rem 0rem;
margin: 0;
font-size: 14px;
font-weight: 500;
}
.field-label {
font-size: 14px;
font-weight: 500;
}
thead > tr {
border-width: 0px 0px 1px 0px;
border-style: solid;
border-color: var(--secondary75);
margin-bottom: 20px;
}
tbody > tr {
border-width: 0px 0px 1px 0px;
border-style: solid;
border-color: var(--primary10);
}
tbody > tr:hover {
background-color: var(--primary10);
}
tbody > tr:hover .edit-button {
color: var(--secondary75);
}
.index-container {
border-style: solid;
border-width: 0 0 1px 0;
border-color: var(--secondary25);
padding: 10px;
margin-bottom: 5px;
}
.index-label {
color: var(--slate);
}
.index-name {
font-weight: bold;
color: var(--primary100);
}
.index-container code {
margin: 0;
display: inline;
background-color: var(--primary10);
color: var(--secondary100);
padding: 3px;
}
.index-field-row {
margin: 1rem 0rem 0rem 0rem;
}
.no-indexes {
margin: 1rem 0rem 0rem 0rem;
font-family: var(--fontnormal);
font-size: 14px;
}
</style>

View File

@ -104,4 +104,8 @@ h5 {
font-family: var(--fontblack); font-family: var(--fontblack);
font-size: 12pt; font-size: 12pt;
color: var(--darkslate); color: var(--darkslate);
}
.hoverable:hover {
cursor: pointer;
} }

View File

@ -1,96 +1,48 @@
<script> <script>
import { store } from "../builderStore" import { getContext } from "svelte"
import { store, backendUiStore } from "../builderStore"
import HierarchyRow from "./HierarchyRow.svelte" import HierarchyRow from "./HierarchyRow.svelte"
import DropdownButton from "../common/DropdownButton.svelte" import DatabasesList from "./DatabasesList.svelte"
import UsersList from "./UsersList.svelte"
import { hierarchy as hierarchyFunctions } from "../../../core/src" import { hierarchy as hierarchyFunctions } from "../../../core/src"
import NavItem from "./NavItem.svelte" import NavItem from "./NavItem.svelte"
import getIcon from "../common/icon" import getIcon from "../common/icon"
const newRootRecord = () => {
store.newRootRecord()
}
const newRootIndex = () => {
store.newRootIndex()
}
const newChildRecord = () => {
store.newChildRecord()
}
const newChildIndex = () => {
store.newChildIndex()
}
const defaultNewChildActions = [
{
label: "New Root Record",
onclick: newRootRecord,
},
{
label: "New Root Index",
onclick: newRootIndex,
},
]
let newChildActions = defaultNewChildActions
const setActiveNav = name => () => {
store.setActiveNav(name)
}
store.subscribe(db => {
if (!db.currentNode || hierarchyFunctions.isIndex(db.currentNode)) {
newChildActions = defaultNewChildActions
} else {
newChildActions = [
{
label: "New Root Record",
onclick: newRootRecord,
},
{
label: "New Root Index",
onclick: newRootIndex,
},
{
label: `New Child Record of ${db.currentNode.name}`,
onclick: newChildRecord,
},
{
label: `New Index on ${db.currentNode.name}`,
onclick: newChildIndex,
},
]
}
})
</script> </script>
<div class="items-root"> <div class="items-root">
<div class="hierarchy"> <div class="hierarchy">
<div class="components-list-container"> <div class="components-list-container">
<div class="nav-group-header"> <div class="nav-group-header">
<div> <div class="hierarchy-title">Databases</div>
{@html getIcon('database', '18')} <i
</div> class="ri-add-line hoverable"
<div class="hierarchy-title">Database</div> on:click={() => backendUiStore.actions.modals.show('DATABASE')} />
<DropdownButton iconName="plus" actions={newChildActions} />
</div> </div>
</div> </div>
<div class="hierarchy-items-container"> <div class="hierarchy-items-container">
{#each $store.hierarchy.children as record} <DatabasesList />
<HierarchyRow node={record} type="record" />
{/each}
{#each $store.hierarchy.indexes as index}
<HierarchyRow node={index} type="index" />
{/each}
</div> </div>
</div> </div>
<hr />
{#if $backendUiStore.selectedDatabase.id}
<div class="hierarchy">
<div class="components-list-container">
<div class="nav-group-header">
<div class="hierarchy-title">Users</div>
<i
class="ri-add-line hoverable"
on:click={() => backendUiStore.actions.modals.show('USER')} />
</div>
</div>
<NavItem name="actions" label="Actions & Triggers" /> <div class="hierarchy-items-container">
<NavItem name="access levels" label="User Levels" /> <UsersList />
</div>
</div>
{/if}
<NavItem name="ACCESS_LEVELS" label="User Levels" />
</div> </div>
<style> <style>
@ -103,40 +55,15 @@
} }
.nav-group-header { .nav-group-header {
display: grid; display: flex;
grid-template-columns: [icon] auto [title] 1fr [button] auto; justify-content: space-between;
align-items: center;
padding: 2rem 1rem 1rem 1rem; padding: 2rem 1rem 1rem 1rem;
font-size: 0.9rem;
}
.nav-group-header > div:nth-child(1) {
padding: 0rem 0.7rem 0rem 0rem;
vertical-align: bottom;
grid-column-start: icon;
margin-right: 5px;
}
.nav-group-header > div:nth-child(2) {
margin-left: 5px;
vertical-align: bottom;
grid-column-start: title;
margin-top: auto;
}
.nav-group-header > div:nth-child(3) {
vertical-align: bottom;
grid-column-start: button;
cursor: pointer;
color: var(--primary75);
}
.nav-group-header > div:nth-child(3):hover {
color: var(--primary75);
} }
.hierarchy-title { .hierarchy-title {
flex: auto 1 1;
text-transform: uppercase; text-transform: uppercase;
font-size: 0.85em;
} }
.hierarchy { .hierarchy {

View File

@ -0,0 +1,78 @@
<script>
import { tick } from "svelte"
import { store, backendUiStore } from "../builderStore"
import getIcon from "../common/icon"
import { CheckIcon } from "../common/Icons"
$: instances = $store.appInstances
$: views = $store.hierarchy.indexes
async function selectDatabase(database) {
backendUiStore.actions.navigate("DATABASE")
backendUiStore.actions.records.select(null)
backendUiStore.actions.views.select(views[0])
backendUiStore.actions.database.select(database)
}
</script>
<div class="root">
<ul>
{#each $store.appInstances as database}
<li>
<span class="icon">
{#if database.id === $backendUiStore.selectedDatabase.id}
<CheckIcon />
{/if}
</span>
<button
class:active={database.id === $backendUiStore.selectedDatabase.id}
on:click={() => selectDatabase(database)}>
{database.name}
</button>
</li>
{/each}
</ul>
</div>
<style>
.root {
padding-bottom: 10px;
font-size: 0.9rem;
color: var(--secondary50);
font-weight: bold;
position: relative;
padding-left: 1.8rem;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
margin: 0.5rem 0;
}
button {
margin: 0 0 0 6px;
padding: 0;
border: none;
font-family: Roboto;
font-size: 0.8rem;
outline: none;
cursor: pointer;
background: rgba(0, 0, 0, 0);
}
.active {
font-weight: 500;
}
.icon {
display: inline-block;
width: 14px;
color: #333;
}
</style>

View File

@ -1,29 +1,41 @@
<script> <script>
import { store } from "../builderStore" import { getContext } from "svelte"
import { store, backendUiStore } from "../builderStore"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import getIcon from "../common/icon" import getIcon from "../common/icon"
export let level = 0 export let level = 0
export let node export let node
export let type export let type
let navActive = "" let navActive = ""
$: icon = type === "index" ? "list" : "file"
const ICON_MAP = {
index: "ri-eye-line",
record: "ri-list-settings-line",
}
store.subscribe(state => { store.subscribe(state => {
if (state.currentNode) { if (state.currentNode) {
navActive = navActive = node.nodeId === state.currentNode.nodeId
state.activeNav === "database" && node.nodeId === state.currentNode.nodeId
} }
}) })
function selectHierarchyItem(node) {
store.selectExistingNode(node.nodeId)
const modalType = node.type === "index" ? "VIEW" : "MODEL"
backendUiStore.actions.modals.show(modalType)
}
</script> </script>
<div> <div>
<div <div
on:click={() => store.selectExistingNode(node.nodeId)} on:click={() => selectHierarchyItem(node)}
class="budibase__nav-item" class="budibase__nav-item hierarchy-item"
class:capitalized={type === 'record'}
style="padding-left: {20 + level * 20}px" style="padding-left: {20 + level * 20}px"
class:selected={navActive}> class:selected={navActive}>
{@html getIcon(icon, 12)} <i class={ICON_MAP[type]} />
<span style="margin-left: 1rem">{node.name}</span> <span style="margin-left: 1rem">{node.name}</span>
</div> </div>
{#if node.children} {#if node.children}
@ -37,3 +49,14 @@
{/each} {/each}
{/if} {/if}
</div> </div>
<style>
.hierarchy-item {
font-size: 14px;
font-weight: 400;
}
.capitalized {
text-transform: capitalize;
}
</style>

View File

@ -1,17 +1,13 @@
<script> <script>
import { store } from "../builderStore"
import getIcon from "../common/icon" import getIcon from "../common/icon"
import { backendUiStore } from "../builderStore"
export let name = "" export let name = ""
export let label = "" export let label = ""
let navActive = "" $: navActive = $backendUiStore.leftNavItem === name
store.subscribe(db => { const setActive = () => backendUiStore.actions.navigate(name)
navActive = db.activeNav === name
})
const setActive = () => store.setActiveNav(name)
</script> </script>
<div <div

View File

@ -0,0 +1,84 @@
<script>
import { store, backendUiStore } from "../builderStore"
import HierarchyRow from "./HierarchyRow.svelte"
import DropdownButton from "../common/DropdownButton.svelte"
import { hierarchy as hierarchyFunctions } from "../../../core/src"
import NavItem from "./NavItem.svelte"
import getIcon from "../common/icon"
function newModel() {
if ($store.currentNode) {
store.newChildRecord()
} else {
store.newRootRecord()
}
backendUiStore.actions.modals.show("MODEL")
}
function newView() {
store.newRootIndex()
backendUiStore.actions.modals.show("VIEW")
}
</script>
<div class="items-root">
<div class="hierarchy">
<div class="components-list-container">
<div class="nav-group-header">
<div class="hierarchy-title">Schema</div>
<div class="uk-inline">
<i class="ri-add-line hoverable" />
<div uk-dropdown="mode: click;">
<ul class="uk-nav uk-dropdown-nav">
<li class="hoverable" on:click={newModel}>Model</li>
<li class="hoverable" on:click={newView}>View</li>
</ul>
</div>
</div>
</div>
</div>
<div class="hierarchy-items-container">
{#each $store.hierarchy.children as record}
<HierarchyRow node={record} type="record" />
{/each}
{#each $store.hierarchy.indexes as index}
<HierarchyRow node={index} type="index" />
{/each}
</div>
</div>
</div>
<style>
.items-root {
display: flex;
flex-direction: column;
max-height: 100%;
height: 100%;
background-color: var(--secondary5);
}
.nav-group-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2rem 1rem 1rem 1rem;
}
.hierarchy-title {
align-items: center;
text-transform: uppercase;
font-size: 0.85em;
}
.hierarchy {
display: flex;
flex-direction: column;
}
.hierarchy-items-container {
flex: 1 1 auto;
overflow-y: auto;
}
</style>

View File

@ -0,0 +1,86 @@
<script>
import { onMount } from "svelte"
import { store, backendUiStore } from "../builderStore"
import api from "../builderStore/api"
import getIcon from "../common/icon"
import { CheckIcon } from "../common/Icons"
const getPage = (s, name) => {
const props = s.pages[name]
return { name, props }
}
let users = []
$: currentAppInfo = {
appname: $store.appname,
instanceId: $backendUiStore.selectedDatabase.id,
}
async function fetchUsers() {
const FETCH_USERS_URL = `/_builder/instance/${currentAppInfo.appname}/${currentAppInfo.instanceId}/api/users`
const response = await api.get(FETCH_USERS_URL)
users = await response.json()
backendUiStore.update(state => {
state.users = users
return state
})
}
onMount(fetchUsers)
</script>
<div class="root">
<ul>
{#each users as user}
<li>
<i class="ri-user-4-line" />
<button class:active={user.id === $store.currentUserId}>
{user.name}
</button>
</li>
{/each}
</ul>
</div>
<style>
.root {
padding-bottom: 10px;
font-size: 0.9rem;
color: var(--secondary50);
font-weight: bold;
position: relative;
padding-left: 1.8rem;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
margin: 0.5rem 0;
}
button {
margin: 0 0 0 6px;
padding: 0;
border: none;
font-family: Roboto;
font-size: 0.8rem;
outline: none;
cursor: pointer;
background: rgba(0, 0, 0, 0);
}
.active {
font-weight: 500;
}
.icon {
display: inline-block;
width: 14px;
color: #333;
}
</style>

View File

@ -1,5 +1,5 @@
<script> <script>
import { store } from "../builderStore/store" import { store } from "../builderStore"
import UIkit from "uikit" import UIkit from "uikit"
import ActionButton from "../common/ActionButton.svelte" import ActionButton from "../common/ActionButton.svelte"
import ButtonGroup from "../common/ButtonGroup.svelte" import ButtonGroup from "../common/ButtonGroup.svelte"

View File

@ -178,8 +178,6 @@
height: 48px; height: 48px;
} }
li button { li button {
width: 48px; width: 48px;
height: 48px; height: 48px;

View File

@ -35,16 +35,13 @@
$: templatesByComponent = groupBy(t => t.component)($store.templates) $: templatesByComponent = groupBy(t => t.component)($store.templates)
$: hierarchy = $store.hierarchy $: hierarchy = $store.hierarchy
$: libraryModules = $store.libraries $: libraryModules = $store.libraries
$: standaloneTemplates = pipe( $: standaloneTemplates = pipe(templatesByComponent, [
templatesByComponent, values,
[ flatten,
values, filter(t => !$store.components.some(c => c.name === t.component)),
flatten, map(t => ({ name: splitName(t.component).componentName, template: t })),
filter(t => !$store.components.some(c => c.name === t.component)), uniqBy(t => t.name),
map(t => ({ name: splitName(t.component).componentName, template: t })), ])
uniqBy(t => t.name),
]
)
const addRootComponent = (component, allComponents) => { const addRootComponent = (component, allComponents) => {
const { libName } = splitName(component.name) const { libName } = splitName(component.name)
@ -278,7 +275,7 @@
background: #fafafa; background: #fafafa;
padding: 10px; padding: 10px;
border-radius: 2px; border-radius: 2px;
color:var(--secondary80); color: var(--secondary80);
} }
.preset-menu > span { .preset-menu > span {

View File

@ -46,7 +46,7 @@
</div> </div>
{/if} {/if}
</div> </div>
<style> <style>
@ -59,6 +59,7 @@
.switcher { .switcher {
display: flex; display: flex;
justify-content: space-between;
margin-bottom: 20px; margin-bottom: 20px;
padding: 0 20px 20px; padding: 0 20px 20px;
border-bottom: 1px solid #d8d8d8; border-bottom: 1px solid #d8d8d8;
@ -72,6 +73,8 @@
padding: 0; padding: 0;
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
text-transform: uppercase;
background: rgba(0, 0, 0, 0);
font-weight: 500; font-weight: 500;
color: var(--secondary40); color: var(--secondary40);
margin-right: 20px; margin-right: 20px;

View File

@ -1,5 +1,5 @@
<script> <script>
import { store } from "../builderStore" import { store, backendUiStore } from "../builderStore"
import { map, join } from "lodash/fp" import { map, join } from "lodash/fp"
import { pipe } from "../common/core" import { pipe } from "../common/core"
@ -63,7 +63,7 @@
] ]
} }
}], }],
appRootPath: `/_builder/instance/${$store.appname}/${$store.currentInstanceId}/`, appRootPath: `/_builder/instance/${$store.appname}/${$backendUiStore.selectedDatabase.id}/`,
} }
$: backendDefinition = { $: backendDefinition = {

View File

@ -1,6 +1,6 @@
<script> <script>
import InputGroup from "../common/Inputs/InputGroup.svelte" import InputGroup from "../common/Inputs/InputGroup.svelte"
import LayoutTemplateControls from "./LayoutTemplateControls.svelte"; import LayoutTemplateControls from "./LayoutTemplateControls.svelte"
export let onStyleChanged = () => {} export let onStyleChanged = () => {}
export let component export let component
@ -59,14 +59,13 @@
{#each Object.entries(display) as [key, [name, meta, size]] (component._id + key)} {#each Object.entries(display) as [key, [name, meta, size]] (component._id + key)}
<div class="grid"> <div class="grid">
<h5>{name}:</h5> <h5>{name}:</h5>
<LayoutTemplateControls <LayoutTemplateControls
onStyleChanged={_value => onStyleChanged('layout', key, _value)} onStyleChanged={_value => onStyleChanged('layout', key, _value)}
values={layout[key] || newValue(meta.length)} values={layout[key] || newValue(meta.length)}
propertyName={name} propertyName={name}
{meta} {meta}
{size} {size}
type="text" type="text" />
/>
</div> </div>
{/each} {/each}
</div> </div>
@ -133,11 +132,20 @@
h3 { h3 {
text-transform: uppercase; text-transform: uppercase;
font-size: 12px; font-size: 12px;
font-weight: 700;
color: #000333;
opacity: 0.6;
margin-bottom: 10px;
}
h4 {
text-transform: uppercase;
font-size: 10px;
font-weight: 600; font-weight: 600;
color: #000333; color: #000333;
opacity: 0.4; opacity: 0.4;
margin-bottom: 10px;
letter-spacing: 1px; letter-spacing: 1px;
margin-bottom: 10px;
} }
h5 { h5 {

View File

@ -115,9 +115,7 @@
bind:value={layoutComponent} bind:value={layoutComponent}
class:uk-form-danger={saveAttempted && !layoutComponent}> class:uk-form-danger={saveAttempted && !layoutComponent}>
{#each layoutComponents as comp} {#each layoutComponents as comp}
<option value={comp}> <option value={comp}>{comp.componentName} - {comp.libName}</option>
{comp.componentName} - {comp.libName}
</option>
{/each} {/each}
</select> </select>
</div> </div>
@ -127,35 +125,32 @@
</ConfirmDialog> </ConfirmDialog>
<style> <style>
.uk-margin {
display: flex;
flex-direction: column;
}
.uk-margin { .uk-form-controls {
display: flex; margin-left: 0 !important;
flex-direction: column; }
}
.uk-form-controls { .uk-form-label {
margin-left: 0 !important; padding-bottom: 10px;
} font-weight: 500;
font-size: 16px;
color: var(--secondary80);
}
.uk-form-label { .uk-input {
padding-bottom: 10px; height: 40px !important;
font-weight: 500; border-radius: 3px;
font-size: 16px; }
color: var(--secondary80);
}
.uk-input {
height: 40px !important;
border-radius: 3px;
}
.uk-select {
height: 40px !important;
font-weight: 500px;
color: var(--secondary60);
border: 1px solid var(--slate);
border-radius: 3px;
}
.uk-select {
height: 40px !important;
font-weight: 500px;
color: var(--secondary60);
border: 1px solid var(--slate);
border-radius: 3px;
}
</style> </style>

View File

@ -70,6 +70,7 @@
font-size: 0.8rem; font-size: 0.8rem;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
background: rgba(0, 0, 0, 0);
} }
.active { .active {

View File

@ -43,7 +43,7 @@
<div class="pages-list-container"> <div class="pages-list-container">
<div class="nav-header"> <div class="nav-header">
<span class="navigator-title">Navigator</span> <span class="navigator-title">Navigator</span>
<div class="border-line" /> <div class="border-line" />
<span class="components-nav-page">Pages</span> <span class="components-nav-page">Pages</span>
</div> </div>
@ -110,24 +110,23 @@
padding: 0; padding: 0;
} }
.root {
display: grid;
grid-template-columns: 275px 1fr 275px;
height: 100%;
width: 100%;
background: #fafafa;
}
@media only screen and (min-width: 1800px) {
.root { .root {
display: grid; display: grid;
grid-template-columns: 300px 1fr 300px; grid-template-columns: 275px 1fr 275px;
height: 100%; height: 100%;
width: 100%; width: 100%;
background: #fafafa; background: #fafafa;
} }
}
@media only screen and (min-width: 1800px) {
.root {
display: grid;
grid-template-columns: 300px 1fr 300px;
height: 100%;
width: 100%;
background: #fafafa;
}
}
.ui-nav { .ui-nav {
grid-column: 1; grid-column: 1;
@ -223,10 +222,10 @@
font-size: 14px; font-size: 14px;
color: var(--secondary100); color: var(--secondary100);
font-weight: 600; font-weight: 600;
text-transform: uppercase;
padding: 0 20px 20px 20px; padding: 0 20px 20px 20px;
line-height: 1rem !important; line-height: 1rem !important;
letter-spacing: 1px; letter-spacing: 1px;
} }
.border-line { .border-line {

View File

@ -8,6 +8,6 @@
"nodeId": 0 "nodeId": 0
}, },
"triggers": [], "triggers": [],
"actions": {}, "actions": [],
"props": {} "props": {}
} }

View File

@ -15,9 +15,12 @@ export const bbFactory = ({
}) => { }) => {
const relativeUrl = url => { const relativeUrl = url => {
if (!frontendDefinition.appRootPath) return url if (!frontendDefinition.appRootPath) return url
if (url.startsWith("http:") if (
|| url.startsWith("https:") url.startsWith("http:") ||
|| url.startsWith("./")) return url url.startsWith("https:") ||
url.startsWith("./")
)
return url
return frontendDefinition.appRootPath + "/" + trimSlash(url) return frontendDefinition.appRootPath + "/" + trimSlash(url)
} }

View File

@ -9,7 +9,7 @@ import {
reduce, reduce,
find, find,
} from "lodash/fp" } from "lodash/fp"
import { compileExpression, compileCode } from "../common/compileCode" import { compileCode } from "../common/compileCode"
import { $ } from "../common" import { $ } from "../common"
import { _executeAction } from "./execute" import { _executeAction } from "./execute"
import { BadRequestError, NotFoundError } from "../common/errors" import { BadRequestError, NotFoundError } from "../common/errors"
@ -49,7 +49,7 @@ const subscribeTriggers = (
const shouldRunTrigger = (trigger, eventContext) => { const shouldRunTrigger = (trigger, eventContext) => {
if (!trigger.condition) return true if (!trigger.condition) return true
const shouldRun = compileExpression(trigger.condition) const shouldRun = compileCode(trigger.condition)
return shouldRun({ context: eventContext }) return shouldRun({ context: eventContext })
} }

View File

@ -5,4 +5,4 @@ export const cloneApp = (app, mergeWith) => {
Object.assign(newApp, mergeWith) Object.assign(newApp, mergeWith)
setCleanupFunc(newApp) setCleanupFunc(newApp)
return newApp return newApp
} }

View File

@ -21,25 +21,25 @@ export const initialiseData = async (
applicationDefinition, applicationDefinition,
accessLevels accessLevels
) => { ) => {
if (!await datastore.exists(configFolder)) if (!(await datastore.exists(configFolder)))
await datastore.createFolder(configFolder) await datastore.createFolder(configFolder)
if (!await datastore.exists(appDefinitionFile)) if (!(await datastore.exists(appDefinitionFile)))
await datastore.createJson(appDefinitionFile, applicationDefinition) await datastore.createJson(appDefinitionFile, applicationDefinition)
await initialiseRootCollections(datastore, applicationDefinition.hierarchy) await initialiseRootCollections(datastore, applicationDefinition.hierarchy)
await initialiseRootIndexes(datastore, applicationDefinition.hierarchy) await initialiseRootIndexes(datastore, applicationDefinition.hierarchy)
if (!await datastore.exists(TRANSACTIONS_FOLDER)) if (!(await datastore.exists(TRANSACTIONS_FOLDER)))
await datastore.createFolder(TRANSACTIONS_FOLDER) await datastore.createFolder(TRANSACTIONS_FOLDER)
if (!await datastore.exists(AUTH_FOLDER)) if (!(await datastore.exists(AUTH_FOLDER)))
await datastore.createFolder(AUTH_FOLDER) await datastore.createFolder(AUTH_FOLDER)
if (!await datastore.exists(USERS_LIST_FILE)) if (!(await datastore.exists(USERS_LIST_FILE)))
await datastore.createJson(USERS_LIST_FILE, []) await datastore.createJson(USERS_LIST_FILE, [])
if (!await datastore.exists(ACCESS_LEVELS_FILE)) if (!(await datastore.exists(ACCESS_LEVELS_FILE)))
await datastore.createJson( await datastore.createJson(
ACCESS_LEVELS_FILE, ACCESS_LEVELS_FILE,
accessLevels ? accessLevels : { version: 0, levels: [] } accessLevels ? accessLevels : { version: 0, levels: [] }

View File

@ -44,7 +44,7 @@ export const _createUser = async (app, user, password = null) => {
const userErrors = validateUser(app)([...users, user], user) const userErrors = validateUser(app)([...users, user], user)
if (userErrors.length > 0) { if (userErrors.length > 0) {
throw new BadRequestError(`User is invalid. ${join("; ")(userErrors)}`) throw new BadRequestError(`User is invalid. ${join("; ")(userErrors.map(e => e.error))}`)
} }
const { auth, tempCode, temporaryAccessId } = await getAccess(app, password) const { auth, tempCode, temporaryAccessId } = await getAccess(app, password)

View File

@ -3,6 +3,8 @@ import { _deleteRecord } from "../recordApi/delete"
import { getAllIdsIterator } from "../indexing/allIds" import { getAllIdsIterator } from "../indexing/allIds"
import { permission } from "../authApi/permissions" import { permission } from "../authApi/permissions"
import { getCollectionDir } from "../recordApi/recordInfo" import { getCollectionDir } from "../recordApi/recordInfo"
import { ensureCollectionIsInitialised } from "./initialise"
import { getNodeForCollectionPath } from "../templateApi/hierarchy"
export const deleteCollection = (app, disableCleanup = false) => async key => export const deleteCollection = (app, disableCleanup = false) => async key =>
apiWrapper( apiWrapper(
@ -25,14 +27,19 @@ export const _deleteCollection = async (app, key, disableCleanup) => {
key = safeKey(key) key = safeKey(key)
const collectionDir = getCollectionDir(app.hierarchy, key) const collectionDir = getCollectionDir(app.hierarchy, key)
await deleteRecords(app, key) await deleteRecords(app, key)
await deleteCollectionFolder(app, collectionDir) await deleteCollectionFolder(app, key, collectionDir)
if (!disableCleanup) { if (!disableCleanup) {
await app.cleanupTransactions() await app.cleanupTransactions()
} }
} }
const deleteCollectionFolder = async (app, dir) => const deleteCollectionFolder = async (app, key, dir) => {
await app.datastore.deleteFolder(dir) await app.datastore.deleteFolder(dir)
await ensureCollectionIsInitialised(
app.datastore,
getNodeForCollectionPath(app.hierarchy)(key),
dir)
}
const deleteRecords = async (app, key) => { const deleteRecords = async (app, key) => {
const iterate = await getAllIdsIterator(app)(key) const iterate = await getAllIdsIterator(app)(key)

View File

@ -6,7 +6,7 @@ import {
} from "../templateApi/hierarchy" } from "../templateApi/hierarchy"
import { $, allTrue, joinKey } from "../common" import { $, allTrue, joinKey } from "../common"
const ensureCollectionIsInitialised = async (datastore, node, dir) => { export const ensureCollectionIsInitialised = async (datastore, node, dir) => {
if (!(await datastore.exists(dir))) { if (!(await datastore.exists(dir))) {
await datastore.createFolder(dir) await datastore.createFolder(dir)
await datastore.createFolder(joinKey(dir, node.nodeId)) await datastore.createFolder(joinKey(dir, node.nodeId))

View File

@ -1,13 +1,22 @@
import { import { compileCode as cCode } from "@nx-js/compiler-util"
compileExpression as cExp, import { includes } from "lodash/fp"
compileCode as cCode,
} from "@nx-js/compiler-util"
export const compileCode = code => { export const compileCode = code => {
let func let func
let safeCode
if (includes("return ")(code)) {
safeCode = code
} else {
let trimmed = code.trim()
trimmed = trimmed.endsWith(";")
? trimmed.substring(0, trimmed.length - 1)
: trimmed
safeCode = `return (${trimmed})`
}
try { try {
func = cCode(code) func = cCode(safeCode)
} catch (e) { } catch (e) {
e.message = `Error compiling code : ${code} : ${e.message}` e.message = `Error compiling code : ${code} : ${e.message}`
throw e throw e
@ -15,16 +24,3 @@ export const compileCode = code => {
return func return func
} }
export const compileExpression = code => {
let func
try {
func = cExp(code)
} catch (e) {
e.message = `Error compiling expression : ${code} : ${e.message}`
throw e
}
return func
}

View File

@ -1,5 +1,5 @@
import { has, isNumber, isUndefined } from "lodash/fp" import { has, isNumber, isUndefined } from "lodash/fp"
import { compileExpression, compileCode } from "@nx-js/compiler-util" import { compileCode } from "../common/compileCode"
import { safeKey, apiWrapper, events, isNonEmptyString } from "../common" import { safeKey, apiWrapper, events, isNonEmptyString } from "../common"
import { iterateIndex } from "../indexing/read" import { iterateIndex } from "../indexing/read"
import { import {
@ -147,7 +147,7 @@ const applyItemToAggregateResult = (indexNode, result, item) => {
const thisGroupResult = result[aggGroup.name] const thisGroupResult = result[aggGroup.name]
if (isNonEmptyString(aggGroup.condition)) { if (isNonEmptyString(aggGroup.condition)) {
if (!compileExpression(aggGroup.condition)({ record: item })) { if (!compileCode(aggGroup.condition)({ record: item })) {
continue continue
} }
} }

View File

@ -125,7 +125,7 @@ const buildHeirarchalIndex = async (app, indexNode) => {
) )
let allIds = await allIdsIterator() let allIds = await allIdsIterator()
while (allIds.done === false) { while (allIds.done === false) {
await createTransactionsForIds( await createTransactionsForIds(
allIds.result.collectionKey, allIds.result.collectionKey,
allIds.result.ids allIds.result.ids
@ -140,5 +140,4 @@ const buildHeirarchalIndex = async (app, indexNode) => {
const recordNodeApplies = indexNode => recordNode => const recordNodeApplies = indexNode => recordNode =>
includes(recordNode.nodeId)(indexNode.allowedRecordNodeIds) includes(recordNode.nodeId)(indexNode.allowedRecordNodeIds)
export default buildIndex export default buildIndex

View File

@ -58,10 +58,9 @@ export const folderStructureArray = recordNode => {
export const getAllIdsIterator = app => async collection_Key_or_NodeKey => { export const getAllIdsIterator = app => async collection_Key_or_NodeKey => {
collection_Key_or_NodeKey = safeKey(collection_Key_or_NodeKey) collection_Key_or_NodeKey = safeKey(collection_Key_or_NodeKey)
const recordNode = getCollectionNodeByKeyOrNodeKey( const recordNode =
app.hierarchy, getCollectionNodeByKeyOrNodeKey(app.hierarchy, collection_Key_or_NodeKey) ||
collection_Key_or_NodeKey getNodeByKeyOrNodeKey(app.hierarchy, collection_Key_or_NodeKey)
) || getNodeByKeyOrNodeKey(app.hierarchy, collection_Key_or_NodeKey)
const getAllIdsIteratorForCollectionKey = async ( const getAllIdsIteratorForCollectionKey = async (
recordNode, recordNode,

View File

@ -1,5 +1,5 @@
import { compileExpression, compileCode } from "@nx-js/compiler-util" import { compileCode } from "../common/compileCode"
import { isUndefined, keys, cloneDeep, isFunction } from "lodash/fp" import { isUndefined, keys, cloneDeep, isFunction, includes } from "lodash/fp"
import { defineError } from "../common" import { defineError } from "../common"
export const filterEval = "FILTER_EVALUATE" export const filterEval = "FILTER_EVALUATE"
@ -16,7 +16,7 @@ const getEvaluateResult = () => ({
result: null, result: null,
}) })
export const compileFilter = index => compileExpression(index.filter) export const compileFilter = index => compileCode(index.filter)
export const compileMap = index => compileCode(index.map) export const compileMap = index => compileCode(index.map)
@ -46,6 +46,9 @@ export const mapRecord = (record, index) => {
if (isFunction(mapped[key])) { if (isFunction(mapped[key])) {
delete mapped[key] delete mapped[key]
} }
if (key === "IsNew") {
delete mapped.IsNew
}
} }
mapped.key = record.key mapped.key = record.key

View File

@ -44,7 +44,7 @@ export const generateSchema = (hierarchy, indexNode) => {
keys, keys,
map(k => ({ name: k, type: schema[k].name })), map(k => ({ name: k, type: schema[k].name })),
filter(s => s.name !== "sortKey"), filter(s => s.name !== "sortKey"),
orderBy("name", ["desc"]), // reverse aplha orderBy("name", ["desc"]), // reverse alpha
concat([{ name: "sortKey", type: all.string.name }]), // sortKey on end concat([{ name: "sortKey", type: all.string.name }]), // sortKey on end
reverse, // sortKey first, then rest are alphabetical reverse, // sortKey first, then rest are alphabetical
]) ])

View File

@ -10,18 +10,18 @@ export const initialiseIndex = async (datastore, dir, index) => {
const indexDir = joinKey(dir, index.name) const indexDir = joinKey(dir, index.name)
let newDir = false let newDir = false
if (!await datastore.exists(indexDir)) { if (!(await datastore.exists(indexDir))) {
await datastore.createFolder(indexDir) await datastore.createFolder(indexDir)
newDir = true newDir = true
} }
if (isShardedIndex(index)) { if (isShardedIndex(index)) {
const shardFile = getShardMapKey(indexDir) const shardFile = getShardMapKey(indexDir)
if (newDir || !await datastore.exists(shardFile)) if (newDir || !(await datastore.exists(shardFile)))
await datastore.createFile(shardFile, "[]") await datastore.createFile(shardFile, "[]")
} else { } else {
const indexFile = getUnshardedIndexDataKey(indexDir) const indexFile = getUnshardedIndexDataKey(indexDir)
if (newDir || !await datastore.exists(indexFile)) if (newDir || !(await datastore.exists(indexFile)))
await createIndexFile(datastore, indexFile, index) await createIndexFile(datastore, indexFile, index)
} }
} }

View File

@ -3,7 +3,10 @@ import { promiseReadableStream } from "./promiseReadableStream"
import { createIndexFile } from "./sharding" import { createIndexFile } from "./sharding"
import { generateSchema } from "./indexSchemaCreator" import { generateSchema } from "./indexSchemaCreator"
import { getIndexReader, CONTINUE_READING_RECORDS } from "./serializer" import { getIndexReader, CONTINUE_READING_RECORDS } from "./serializer"
import { getAllowedRecordNodesForIndex, getRecordNodeId } from "../templateApi/hierarchy" import {
getAllowedRecordNodesForIndex,
getRecordNodeId,
} from "../templateApi/hierarchy"
import { $ } from "../common" import { $ } from "../common"
import { filter, includes, find } from "lodash/fp" import { filter, includes, find } from "lodash/fp"

View File

@ -1,4 +1,4 @@
import { compileCode } from "@nx-js/compiler-util" import { compileCode } from "../common/compileCode"
import { filter, includes, map, last } from "lodash/fp" import { filter, includes, map, last } from "lodash/fp"
import { import {
getActualKeyOfParent, getActualKeyOfParent,

View File

@ -22,6 +22,11 @@ export const getNew = app => (collectionKey, recordTypeName) => {
) )
} }
/**
* Constructs a record object that can be saved to the backend.
* @param {*} recordNode - record
* @param {*} collectionKey - nested collection key that the record will be saved to.
*/
export const _getNew = (recordNode, collectionKey) => export const _getNew = (recordNode, collectionKey) =>
constructRecord(recordNode, getNewFieldValue, collectionKey) constructRecord(recordNode, getNewFieldValue, collectionKey)

View File

@ -1,7 +1,7 @@
import { isString, flatten, map, filter } from "lodash/fp" import { isString, flatten, map, filter } from "lodash/fp"
import { initialiseChildCollections } from "../collectionApi/initialise" import { initialiseChildCollections } from "../collectionApi/initialise"
import { _loadFromInfo } from "./load" import { _loadFromInfo } from "./load"
import { $ } from "../common" import { $, joinKey } from "../common"
import { import {
getFlattenedHierarchy, getFlattenedHierarchy,
isRecord, isRecord,
@ -11,31 +11,31 @@ import {
} from "../templateApi/hierarchy" } from "../templateApi/hierarchy"
import { initialiseIndex } from "../indexing/initialiseIndex" import { initialiseIndex } from "../indexing/initialiseIndex"
import { getRecordInfo } from "./recordInfo" import { getRecordInfo } from "./recordInfo"
import { getAllIdsIterator } from "../indexing/allIds"
export const initialiseChildren = async (app, recordInfoOrKey) => { export const initialiseChildren = async (app, recordInfoOrKey) => {
const recordInfo = isString(recordInfoOrKey) const recordInfo = isString(recordInfoOrKey)
? getRecordInfo(app.hierarchy, recordInfoOrKey) ? getRecordInfo(app.hierarchy, recordInfoOrKey)
: recordInfoOrKey : recordInfoOrKey
await initialiseReverseReferenceIndexes(app, recordInfo) await initialiseReverseReferenceIndexes(app, recordInfo)
await initialiseAncestorIndexes(app, recordInfo) await initialiseAncestorIndexes(app, recordInfo)
await initialiseChildCollections(app, recordInfo) await initialiseChildCollections(app, recordInfo)
} }
export const initialiseChildrenForNode = async (app, recordNode) => { export const initialiseChildrenForNode = async (app, recordNode) => {
if (isTopLevelRecord(recordNode)) { if (isTopLevelRecord(recordNode)) {
await initialiseChildren( await initialiseChildren(app, recordNode.nodeKey())
app, recordNode.nodeKey())
return return
} }
const iterate = await getAllIdsIterator(app)(recordNode.parent().nodeKey()) const iterate = await getAllIdsIterator(app)(
recordNode.parent().collectionNodeKey()
)
let iterateResult = await iterate() let iterateResult = await iterate()
while (!iterateResult.done) { while (!iterateResult.done) {
const { result } = iterateResult const { result } = iterateResult
for (const id of result.ids) { for (const id of result.ids) {
const initialisingRecordKey = joinKey( const initialisingRecordKey = joinKey(result.collectionKey, id)
result.collectionKey, id)
await initialiseChildren(app, initialisingRecordKey) await initialiseChildren(app, initialisingRecordKey)
} }
iterateResult = await iterate() iterateResult = await iterate()
@ -77,5 +77,3 @@ const fieldsThatReferenceThisRecord = (app, recordNode) =>
flatten, flatten,
filter(fieldReversesReferenceToNode(recordNode)), filter(fieldReversesReferenceToNode(recordNode)),
]) ])

View File

@ -1,5 +1,5 @@
import { map, reduce, filter, isEmpty, flatten, each } from "lodash/fp" import { map, reduce, filter, isEmpty, flatten, each } from "lodash/fp"
import { compileExpression } from "@nx-js/compiler-util" import { compileCode } from "../common/compileCode"
import _ from "lodash" import _ from "lodash"
import { getExactNodeForKey } from "../templateApi/hierarchy" import { getExactNodeForKey } from "../templateApi/hierarchy"
import { validateFieldParse, validateTypeConstraints } from "../types" import { validateFieldParse, validateTypeConstraints } from "../types"
@ -35,7 +35,7 @@ const validateAllTypeConstraints = async (record, recordNode, context) => {
const runRecordValidationRules = (record, recordNode) => { const runRecordValidationRules = (record, recordNode) => {
const runValidationRule = rule => { const runValidationRule = rule => {
const isValid = compileExpression(rule.expressionWhenValid) const isValid = compileCode(rule.expressionWhenValid)
const expressionContext = { record, _ } const expressionContext = { record, _ }
return isValid(expressionContext) return isValid(expressionContext)
? { valid: true } ? { valid: true }

View File

@ -0,0 +1,54 @@
import {
findRoot,
getFlattenedHierarchy,
fieldReversesReferenceToIndex,
isRecord,
} from "./hierarchy"
import { $ } from "../common"
import { map, filter, reduce } from "lodash/fp"
export const canDeleteIndex = indexNode => {
const flatHierarchy = $(indexNode, [findRoot, getFlattenedHierarchy])
const reverseIndexes = $(flatHierarchy, [
filter(isRecord),
reduce((obj, r) => {
for (let field of r.fields) {
if (fieldReversesReferenceToIndex(indexNode)(field)) {
obj.push({ ...field, record: r })
}
}
return obj
}, []),
map(
f =>
`field "${f.name}" on record "${f.record.name}" uses this index as a reference`
),
])
const lookupIndexes = $(flatHierarchy, [
filter(isRecord),
reduce((obj, r) => {
for (let field of r.fields) {
if (
field.type === "reference" &&
field.typeOptions.indexNodeKey === indexNode.nodeKey()
) {
obj.push({ ...field, record: r })
}
}
return obj
}, []),
map(
f =>
`field "${f.name}" on record "${f.record.name}" uses this index as a lookup`
),
])
const errors = [...reverseIndexes, ...lookupIndexes]
return {
canDelete: errors.length === 0,
errors,
}
}

View File

@ -0,0 +1,45 @@
import {
findRoot,
getFlattenedHierarchy,
fieldReversesReferenceToIndex,
isRecord,
isAncestorIndex,
isAncestor,
} from "./hierarchy"
import { $ } from "../common"
import { map, filter, includes } from "lodash/fp"
export const canDeleteRecord = recordNode => {
const flatHierarchy = $(recordNode, [findRoot, getFlattenedHierarchy])
const ancestors = $(flatHierarchy, [filter(isAncestor(recordNode))])
const belongsToAncestor = i => ancestors.includes(i.parent())
const errorsForNode = node => {
const errorsThisNode = $(flatHierarchy, [
filter(
i =>
isAncestorIndex(i) &&
belongsToAncestor(i) &&
includes(node.nodeId)(i.allowedRecordNodeIds)
),
map(
i =>
`index "${i.name}" indexes this record. Please remove the record from the index, or delete the index`
),
])
for (let child of node.children) {
for (let err of errorsForNode(child)) {
errorsThisNode.push(err)
}
}
return errorsThisNode
}
const errors = errorsForNode(recordNode)
return { errors, canDelete: errors.length === 0 }
}

View File

@ -160,16 +160,17 @@ export const getNewRootLevel = () =>
}) })
const _getNewRecordTemplate = (parent, name, createDefaultIndex, isSingle) => { const _getNewRecordTemplate = (parent, name, createDefaultIndex, isSingle) => {
const nodeId = getNodeId(parent)
const node = constructNode(parent, { const node = constructNode(parent, {
name, name,
type: "record", type: "record",
fields: [], fields: [],
children: [], children: [],
validationRules: [], validationRules: [],
nodeId: getNodeId(parent), nodeId: nodeId,
indexes: [], indexes: [],
estimatedRecordCount: isRecord(parent) ? 500 : 1000000, estimatedRecordCount: isRecord(parent) ? 500 : 1000000,
collectionName: "", collectionName: (nodeId || "").toString(),
isSingle, isSingle,
}) })

View File

@ -4,7 +4,6 @@ import { isTopLevelIndex, getParentKey, getLastPartInKey } from "./hierarchy"
import { safeKey, joinKey } from "../common" import { safeKey, joinKey } from "../common"
export const deleteAllIndexFilesForNode = async (app, indexNode) => { export const deleteAllIndexFilesForNode = async (app, indexNode) => {
if (isTopLevelIndex(indexNode)) { if (isTopLevelIndex(indexNode)) {
await app.datastore.deleteFolder(indexNode.nodeKey()) await app.datastore.deleteFolder(indexNode.nodeKey())
return return
@ -15,13 +14,11 @@ export const deleteAllIndexFilesForNode = async (app, indexNode) => {
while (!iterateResult.done) { while (!iterateResult.done) {
const { result } = iterateResult const { result } = iterateResult
for (const id of result.ids) { for (const id of result.ids) {
const deletingIndexKey = joinKey( const deletingIndexKey = joinKey(result.collectionKey, id, indexNode.name)
result.collectionKey, id, indexNode.name)
await deleteIndexFolder(app, deletingIndexKey) await deleteIndexFolder(app, deletingIndexKey)
} }
iterateResult = await iterate() iterateResult = await iterate()
} }
} }
const deleteIndexFolder = async (app, indexKey) => { const deleteIndexFolder = async (app, indexKey) => {
@ -29,6 +26,5 @@ const deleteIndexFolder = async (app, indexKey) => {
const indexName = getLastPartInKey(indexKey) const indexName = getLastPartInKey(indexKey)
const parentRecordKey = getParentKey(indexKey) const parentRecordKey = getParentKey(indexKey)
const recordInfo = getRecordInfo(app.hierarchy, parentRecordKey) const recordInfo = getRecordInfo(app.hierarchy, parentRecordKey)
await app.datastore.deleteFolder( await app.datastore.deleteFolder(joinKey(recordInfo.dir, indexName))
joinKey(recordInfo.dir, indexName)) }
}

View File

@ -4,10 +4,8 @@ import { isTopLevelRecord, getCollectionKey } from "./hierarchy"
import { safeKey, joinKey } from "../common" import { safeKey, joinKey } from "../common"
export const deleteAllRecordsForNode = async (app, recordNode) => { export const deleteAllRecordsForNode = async (app, recordNode) => {
if (isTopLevelRecord(recordNode)) { if (isTopLevelRecord(recordNode)) {
await deleteRecordCollection( await deleteRecordCollection(app, recordNode.collectionName)
app, recordNode.collectionName)
return return
} }
@ -17,16 +15,19 @@ export const deleteAllRecordsForNode = async (app, recordNode) => {
const { result } = iterateResult const { result } = iterateResult
for (const id of result.ids) { for (const id of result.ids) {
const deletingCollectionKey = joinKey( const deletingCollectionKey = joinKey(
result.collectionKey, id, recordNode.collectionName) result.collectionKey,
id,
recordNode.collectionName
)
await deleteRecordCollection(app, deletingCollectionKey) await deleteRecordCollection(app, deletingCollectionKey)
} }
iterateResult = await iterate() iterateResult = await iterate()
} }
} }
const deleteRecordCollection = async (app, collectionKey) => { const deleteRecordCollection = async (app, collectionKey) => {
collectionKey = safeKey(collectionKey) collectionKey = safeKey(collectionKey)
await app.datastore.deleteFolder( await app.datastore.deleteFolder(
getCollectionDir(app.hierarchy, collectionKey)) getCollectionDir(app.hierarchy, collectionKey)
} )
}

View File

@ -1,4 +1,9 @@
import { getFlattenedHierarchy, isRecord, isIndex, isAncestor } from "./hierarchy" import {
getFlattenedHierarchy,
isRecord,
isIndex,
isAncestor,
} from "./hierarchy"
import { $, none } from "../common" import { $, none } from "../common"
import { map, filter, some, find, difference } from "lodash/fp" import { map, filter, some, find, difference } from "lodash/fp"
@ -19,13 +24,16 @@ export const diffHierarchy = (oldHierarchy, newHierarchy) => {
const createdRecords = findCreatedRecords(oldHierarchyFlat, newHierarchyFlat) const createdRecords = findCreatedRecords(oldHierarchyFlat, newHierarchyFlat)
const deletedRecords = findDeletedRecords(oldHierarchyFlat, newHierarchyFlat) const deletedRecords = findDeletedRecords(oldHierarchyFlat, newHierarchyFlat)
return [ return [
...createdRecords, ...createdRecords,
...deletedRecords, ...deletedRecords,
...findRenamedRecords(oldHierarchyFlat, newHierarchyFlat), ...findRenamedRecords(oldHierarchyFlat, newHierarchyFlat),
...findRecordsWithFieldsChanged(oldHierarchyFlat, newHierarchyFlat), ...findRecordsWithFieldsChanged(oldHierarchyFlat, newHierarchyFlat),
...findRecordsWithEstimatedRecordTypeChanged(oldHierarchyFlat, newHierarchyFlat), ...findRecordsWithEstimatedRecordTypeChanged(
oldHierarchyFlat,
newHierarchyFlat
),
...findCreatedIndexes(oldHierarchyFlat, newHierarchyFlat, createdRecords), ...findCreatedIndexes(oldHierarchyFlat, newHierarchyFlat, createdRecords),
...findDeletedIndexes(oldHierarchyFlat, newHierarchyFlat, deletedRecords), ...findDeletedIndexes(oldHierarchyFlat, newHierarchyFlat, deletedRecords),
...findUpdatedIndexes(oldHierarchyFlat, newHierarchyFlat), ...findUpdatedIndexes(oldHierarchyFlat, newHierarchyFlat),
@ -33,18 +41,20 @@ export const diffHierarchy = (oldHierarchy, newHierarchy) => {
} }
const changeItem = (type, oldNode, newNode) => ({ const changeItem = (type, oldNode, newNode) => ({
type, oldNode, newNode, type,
oldNode,
newNode,
}) })
const findCreatedRecords = (oldHierarchyFlat, newHierarchyFlat) => { const findCreatedRecords = (oldHierarchyFlat, newHierarchyFlat) => {
const allCreated = $(newHierarchyFlat, [ const allCreated = $(newHierarchyFlat, [
filter(isRecord), filter(isRecord),
filter(nodeDoesNotExistIn(oldHierarchyFlat)), filter(nodeDoesNotExistIn(oldHierarchyFlat)),
map(n => changeItem(HierarchyChangeTypes.recordCreated, null, n)) map(n => changeItem(HierarchyChangeTypes.recordCreated, null, n)),
]) ])
return $(allCreated, [ return $(allCreated, [
filter(r => none(r2 => isAncestor(r.newNode)(r2.newNode))(allCreated)) filter(r => none(r2 => isAncestor(r.newNode)(r2.newNode))(allCreated)),
]) ])
} }
@ -52,117 +62,142 @@ const findDeletedRecords = (oldHierarchyFlat, newHierarchyFlat) => {
const allDeleted = $(oldHierarchyFlat, [ const allDeleted = $(oldHierarchyFlat, [
filter(isRecord), filter(isRecord),
filter(nodeDoesNotExistIn(newHierarchyFlat)), filter(nodeDoesNotExistIn(newHierarchyFlat)),
map(n => changeItem(HierarchyChangeTypes.recordDeleted, n, null)) map(n => changeItem(HierarchyChangeTypes.recordDeleted, n, null)),
]) ])
return $(allDeleted, [ return $(allDeleted, [
filter(r => none(r2 => isAncestor(r.oldNode)(r2.oldNode))(allDeleted)) filter(r => none(r2 => isAncestor(r.oldNode)(r2.oldNode))(allDeleted)),
]) ])
} }
const findRenamedRecords = (oldHierarchyFlat, newHierarchyFlat) => const findRenamedRecords = (oldHierarchyFlat, newHierarchyFlat) =>
$(oldHierarchyFlat, [ $(oldHierarchyFlat, [
filter(isRecord), filter(isRecord),
filter(nodeExistsIn(newHierarchyFlat)), filter(nodeExistsIn(newHierarchyFlat)),
filter(nodeChanged(newHierarchyFlat, (_new,old) =>_new.collectionKey !== old.collectionKey )), filter(
map(n => changeItem( nodeChanged(
HierarchyChangeTypes.recordRenamed, newHierarchyFlat,
n, (_new, old) => _new.collectionKey !== old.collectionKey
findNodeIn(n, newHierarchyFlat)) )
) ),
map(n =>
changeItem(
HierarchyChangeTypes.recordRenamed,
n,
findNodeIn(n, newHierarchyFlat)
)
),
]) ])
const findRecordsWithFieldsChanged = (oldHierarchyFlat, newHierarchyFlat) => const findRecordsWithFieldsChanged = (oldHierarchyFlat, newHierarchyFlat) =>
$(oldHierarchyFlat, [ $(oldHierarchyFlat, [
filter(isRecord), filter(isRecord),
filter(nodeExistsIn(newHierarchyFlat)), filter(nodeExistsIn(newHierarchyFlat)),
filter(hasDifferentFields(newHierarchyFlat)), filter(hasDifferentFields(newHierarchyFlat)),
map(n => changeItem( map(n =>
HierarchyChangeTypes.recordFieldsChanged, changeItem(
n, HierarchyChangeTypes.recordFieldsChanged,
findNodeIn(n, newHierarchyFlat)) n,
) findNodeIn(n, newHierarchyFlat)
)
),
]) ])
const findRecordsWithEstimatedRecordTypeChanged = (oldHierarchyFlat, newHierarchyFlat) => const findRecordsWithEstimatedRecordTypeChanged = (
oldHierarchyFlat,
newHierarchyFlat
) =>
$(oldHierarchyFlat, [ $(oldHierarchyFlat, [
filter(isRecord), filter(isRecord),
filter(nodeExistsIn(newHierarchyFlat)), filter(nodeExistsIn(newHierarchyFlat)),
filter(nodeChanged(newHierarchyFlat, (_new,old) =>_new.estimatedRecordCount !== old.estimatedRecordCount)), filter(
map(n => changeItem( nodeChanged(
HierarchyChangeTypes.recordEstimatedRecordTypeChanged, newHierarchyFlat,
n, (_new, old) => _new.estimatedRecordCount !== old.estimatedRecordCount
findNodeIn(n, newHierarchyFlat)) )
) ),
map(n =>
changeItem(
HierarchyChangeTypes.recordEstimatedRecordTypeChanged,
n,
findNodeIn(n, newHierarchyFlat)
)
),
]) ])
const findCreatedIndexes = (oldHierarchyFlat, newHierarchyFlat, createdRecords) => { const findCreatedIndexes = (
oldHierarchyFlat,
newHierarchyFlat,
createdRecords
) => {
const allCreated = $(newHierarchyFlat, [ const allCreated = $(newHierarchyFlat, [
filter(isIndex), filter(isIndex),
filter(nodeDoesNotExistIn(oldHierarchyFlat)), filter(nodeDoesNotExistIn(oldHierarchyFlat)),
map(n => changeItem(HierarchyChangeTypes.indexCreated, null, n)) map(n => changeItem(HierarchyChangeTypes.indexCreated, null, n)),
]) ])
return $(allCreated, [ return $(allCreated, [
filter(r => none(r2 => isAncestor(r.newNode)(r2.newNode))(createdRecords)) filter(r => none(r2 => isAncestor(r.newNode)(r2.newNode))(createdRecords)),
]) ])
} }
const findDeletedIndexes = (oldHierarchyFlat, newHierarchyFlat, deletedRecords) => { const findDeletedIndexes = (
oldHierarchyFlat,
newHierarchyFlat,
deletedRecords
) => {
const allDeleted = $(oldHierarchyFlat, [ const allDeleted = $(oldHierarchyFlat, [
filter(isIndex), filter(isIndex),
filter(nodeDoesNotExistIn(newHierarchyFlat)), filter(nodeDoesNotExistIn(newHierarchyFlat)),
map(n => changeItem(HierarchyChangeTypes.indexDeleted, n, null)) map(n => changeItem(HierarchyChangeTypes.indexDeleted, n, null)),
]) ])
return $(allDeleted, [ return $(allDeleted, [
filter(r => none(r2 => isAncestor(r.oldNode)(r2.oldNode))(deletedRecords)) filter(r => none(r2 => isAncestor(r.oldNode)(r2.oldNode))(deletedRecords)),
]) ])
} }
const findUpdatedIndexes = (oldHierarchyFlat, newHierarchyFlat) =>
const findUpdatedIndexes = (oldHierarchyFlat, newHierarchyFlat) =>
$(oldHierarchyFlat, [ $(oldHierarchyFlat, [
filter(isIndex), filter(isIndex),
filter(nodeExistsIn(newHierarchyFlat)), filter(nodeExistsIn(newHierarchyFlat)),
filter(nodeChanged(newHierarchyFlat, indexHasChanged)), filter(nodeChanged(newHierarchyFlat, indexHasChanged)),
map(n => changeItem( map(n =>
HierarchyChangeTypes.indexChanged, changeItem(
n, HierarchyChangeTypes.indexChanged,
findNodeIn(n, newHierarchyFlat)) n,
) findNodeIn(n, newHierarchyFlat)
)
),
]) ])
const hasDifferentFields = otherFlatHierarchy => record1 => { const hasDifferentFields = otherFlatHierarchy => record1 => {
const record2 = findNodeIn(record1, otherFlatHierarchy) const record2 = findNodeIn(record1, otherFlatHierarchy)
if(record1.fields.length !== record2.fields.length) return true if (record1.fields.length !== record2.fields.length) return true
for(let f1 of record1.fields) { for (let f1 of record1.fields) {
if (none(isFieldSame(f1))(record2.fields)) return true if (none(isFieldSame(f1))(record2.fields)) return true
} }
return false return false
} }
const indexHasChanged = (_new, old) => const indexHasChanged = (_new, old) =>
_new.map !== old.map _new.map !== old.map ||
|| _new.filter !== old.filter _new.filter !== old.filter ||
|| _new.getShardName !== old.getShardName _new.getShardName !== old.getShardName ||
|| difference(_new.allowedRecordNodeIds)(old.allowedRecordNodeIds).length > 0 difference(_new.allowedRecordNodeIds)(old.allowedRecordNodeIds).length > 0
const isFieldSame = f1 => f2 => const isFieldSame = f1 => f2 => f1.name === f2.name && f1.type === f2.type
f1.name === f2.name && f1.type === f2.type
const nodeDoesNotExistIn = inThis => node => const nodeDoesNotExistIn = inThis => node =>
none(n => n.nodeId === node.nodeId)(inThis) none(n => n.nodeId === node.nodeId)(inThis)
const nodeExistsIn = inThis => node => const nodeExistsIn = inThis => node =>
some(n => n.nodeId === node.nodeId)(inThis) some(n => n.nodeId === node.nodeId)(inThis)
const nodeChanged = (inThis, isChanged) => node => const nodeChanged = (inThis, isChanged) => node =>
some(n => n.nodeId === node.nodeId && isChanged(n, node))(inThis) some(n => n.nodeId === node.nodeId && isChanged(n, node))(inThis)
const findNodeIn = (node, inThis) => const findNodeIn = (node, inThis) => find(n => n.nodeId === node.nodeId)(inThis)
find(n => n.nodeId === node.nodeId)(inThis)

View File

@ -9,6 +9,7 @@ import {
import { all, getDefaultOptions } from "../types" import { all, getDefaultOptions } from "../types"
import { applyRuleSet, makerule } from "../common/validationCommon" import { applyRuleSet, makerule } from "../common/validationCommon"
import { BadRequestError } from "../common/errors" import { BadRequestError } from "../common/errors"
import { generate } from "shortid"
export const fieldErrors = { export const fieldErrors = {
AddFieldValidationFailed: "Add field validation: ", AddFieldValidationFailed: "Add field validation: ",
@ -17,6 +18,7 @@ export const fieldErrors = {
export const allowedTypes = () => keys(all) export const allowedTypes = () => keys(all)
export const getNewField = type => ({ export const getNewField = type => ({
id: generate(),
name: "", // how field is referenced internally name: "", // how field is referenced internally
type, type,
typeOptions: getDefaultOptions(type), typeOptions: getDefaultOptions(type),

View File

@ -192,8 +192,8 @@ export const getAllowedRecordNodesForIndex = (appHierarchy, indexNode) => {
} }
export const getDependantIndexes = (hierarchy, recordNode) => { export const getDependantIndexes = (hierarchy, recordNode) => {
const allIndexes = $(hierarchy, [ getFlattenedHierarchy, filter(isIndex)]) const allIndexes = $(hierarchy, [getFlattenedHierarchy, filter(isIndex)])
const allowedAncestors = $(allIndexes, [ const allowedAncestors = $(allIndexes, [
filter(isAncestorIndex), filter(isAncestorIndex),
filter(i => recordNodeIsAllowed(i)(recordNode)), filter(i => recordNodeIsAllowed(i)(recordNode)),
@ -201,7 +201,7 @@ export const getDependantIndexes = (hierarchy, recordNode) => {
const allowedReference = $(allIndexes, [ const allowedReference = $(allIndexes, [
filter(isReferenceIndex), filter(isReferenceIndex),
filter(i => some(fieldReversesReferenceToIndex(i))(recordNode.fields)) filter(i => some(fieldReversesReferenceToIndex(i))(recordNode.fields)),
]) ])
return [...allowedAncestors, ...allowedReference] return [...allowedAncestors, ...allowedReference]
@ -222,7 +222,7 @@ export const isaggregateGroup = node =>
export const isShardedIndex = node => export const isShardedIndex = node =>
isIndex(node) && isNonEmptyString(node.getShardName) isIndex(node) && isNonEmptyString(node.getShardName)
export const isRoot = node => isSomething(node) && node.isRoot() export const isRoot = node => isSomething(node) && node.isRoot()
export const findRoot = node => isRoot(node) ? node : findRoot(node.parent()) export const findRoot = node => (isRoot(node) ? node : findRoot(node.parent()))
export const isDecendantOfARecord = hasMatchingAncestor(isRecord) export const isDecendantOfARecord = hasMatchingAncestor(isRecord)
export const isGlobalIndex = node => isIndex(node) && isRoot(node.parent()) export const isGlobalIndex = node => isIndex(node) && isRoot(node.parent())
export const isReferenceIndex = node => export const isReferenceIndex = node =>
@ -231,10 +231,8 @@ export const isAncestorIndex = node =>
isIndex(node) && node.indexType === indexTypes.ancestor isIndex(node) && node.indexType === indexTypes.ancestor
export const isTopLevelRecord = node => isRoot(node.parent()) && isRecord(node) export const isTopLevelRecord = node => isRoot(node.parent()) && isRecord(node)
export const isTopLevelIndex = node => isRoot(node.parent()) && isIndex(node) export const isTopLevelIndex = node => isRoot(node.parent()) && isIndex(node)
export const getCollectionKey = recordKey => $(recordKey, [ export const getCollectionKey = recordKey =>
splitKey, $(recordKey, [splitKey, parts => joinKey(parts.slice(0, parts.length - 1))])
parts => joinKey(parts.slice(0, parts.length - 1))
])
export const fieldReversesReferenceToNode = node => field => export const fieldReversesReferenceToNode = node => field =>
field.type === "reference" && field.type === "reference" &&
intersection(field.typeOptions.reverseIndexNodeKeys)( intersection(field.typeOptions.reverseIndexNodeKeys)(

View File

@ -58,7 +58,7 @@ const api = app => ({
validateNode, validateNode,
validateAll, validateAll,
validateTriggers, validateTriggers,
upgradeData: upgradeData(app) upgradeData: upgradeData(app),
}) })
export const getTemplateApi = app => api(app) export const getTemplateApi = app => api(app)

View File

@ -5,7 +5,6 @@ import { joinKey } from "../common"
import { initialiseIndex } from "../indexing/initialiseIndex" import { initialiseIndex } from "../indexing/initialiseIndex"
export const initialiseNewIndex = async (app, indexNode) => { export const initialiseNewIndex = async (app, indexNode) => {
if (isTopLevelIndex(indexNode)) { if (isTopLevelIndex(indexNode)) {
await initialiseIndex(app.datastore, "/", indexNode) await initialiseIndex(app.datastore, "/", indexNode)
return return
@ -18,10 +17,11 @@ export const initialiseNewIndex = async (app, indexNode) => {
for (const id of result.ids) { for (const id of result.ids) {
const recordKey = joinKey(result.collectionKey, id) const recordKey = joinKey(result.collectionKey, id)
await initialiseIndex( await initialiseIndex(
app.datastore, app.datastore,
getRecordInfo(app.hierarchy, recordKey).dir, getRecordInfo(app.hierarchy, recordKey).dir,
indexNode) indexNode
)
} }
iterateResult = await iterate() iterateResult = await iterate()
} }
} }

View File

@ -1,19 +1,19 @@
import { diffHierarchy, HierarchyChangeTypes } from "./diffHierarchy" import { diffHierarchy, HierarchyChangeTypes } from "./diffHierarchy"
import { $, switchCase } from "../common" import { $, switchCase } from "../common"
import { import {
differenceBy, differenceBy,
isEqual, isEqual,
some, some,
map, map,
filter, filter,
uniqBy, uniqBy,
flatten flatten,
} from "lodash/fp" } from "lodash/fp"
import { import {
findRoot, findRoot,
getDependantIndexes, getDependantIndexes,
isTopLevelRecord, isTopLevelRecord,
isAncestorIndex isAncestorIndex,
} from "./hierarchy" } from "./hierarchy"
import { generateSchema } from "../indexing/indexSchemaCreator" import { generateSchema } from "../indexing/indexSchemaCreator"
import { _buildIndex } from "../indexApi/buildIndex" import { _buildIndex } from "../indexApi/buildIndex"
@ -24,130 +24,142 @@ import { cloneApp } from "../appInitialise/cloneApp"
import { initialiseData } from "../appInitialise/initialiseData" import { initialiseData } from "../appInitialise/initialiseData"
import { initialiseChildrenForNode } from "../recordApi/initialiseChildren" import { initialiseChildrenForNode } from "../recordApi/initialiseChildren"
import { initialiseNewIndex } from "./initialiseNewIndex" import { initialiseNewIndex } from "./initialiseNewIndex"
import { saveApplicationHierarchy } from "../templateApi/saveApplicationHierarchy" import { _saveApplicationHierarchy } from "../templateApi/saveApplicationHierarchy"
import { getApplicationDefinition } from "../templateApi/getApplicationDefinition"
export const upgradeData = app => async newHierarchy => { export const upgradeData = app => async newHierarchy => {
const currentAppDef = await getApplicationDefinition(app.datastore)()
app.hierarchy = currentAppDef.hierarchy
newHierarchy = constructHierarchy(newHierarchy)
const diff = diffHierarchy(app.hierarchy, newHierarchy) const diff = diffHierarchy(app.hierarchy, newHierarchy)
const changeActions = gatherChangeActions(diff) const changeActions = gatherChangeActions(diff)
if (changeActions.length === 0) return if (changeActions.length === 0) return
newHierarchy = constructHierarchy(newHierarchy) const newApp =
const newApp = newHierarchy && cloneApp(app, { newHierarchy &&
hierarchy: newHierarchy cloneApp(app, {
}) hierarchy: newHierarchy,
})
await doUpgrade(app, newApp, changeActions) await doUpgrade(app, newApp, changeActions)
await saveApplicationHierarchy(newApp)(newHierarchy) await _saveApplicationHierarchy(newApp.datastore, newHierarchy)
} }
const gatherChangeActions = (diff) => const gatherChangeActions = diff =>
$(diff, [ $(diff, [map(actionForChange), flatten, uniqBy(a => a.compareKey)])
map(actionForChange),
flatten,
uniqBy(a => a.compareKey)
])
const doUpgrade = async (oldApp, newApp, changeActions) => { const doUpgrade = async (oldApp, newApp, changeActions) => {
for(let action of changeActions) { for (let action of changeActions) {
await action.run(oldApp, newApp, action.diff) await action.run(oldApp, newApp, action.diff)
} }
} }
const actionForChange = diff => const actionForChange = diff =>
switchCase( switchCase(
[isChangeType(HierarchyChangeTypes.recordCreated), recordCreatedAction], [isChangeType(HierarchyChangeTypes.recordCreated), recordCreatedAction],
[isChangeType(HierarchyChangeTypes.recordDeleted), deleteRecordsAction], [isChangeType(HierarchyChangeTypes.recordDeleted), deleteRecordsAction],
[ [
isChangeType(HierarchyChangeTypes.recordFieldsChanged), isChangeType(HierarchyChangeTypes.recordFieldsChanged),
rebuildAffectedIndexesAction rebuildAffectedIndexesAction,
], ],
[isChangeType(HierarchyChangeTypes.recordRenamed), renameRecordAction], [isChangeType(HierarchyChangeTypes.recordRenamed), renameRecordAction],
[ [
isChangeType(HierarchyChangeTypes.recordEstimatedRecordTypeChanged), isChangeType(HierarchyChangeTypes.recordEstimatedRecordTypeChanged),
reshardRecordsAction reshardRecordsAction,
], ],
[isChangeType(HierarchyChangeTypes.indexCreated), newIndexAction], [isChangeType(HierarchyChangeTypes.indexCreated), newIndexAction],
[isChangeType(HierarchyChangeTypes.indexDeleted), deleteIndexAction], [isChangeType(HierarchyChangeTypes.indexDeleted), deleteIndexAction],
[isChangeType(HierarchyChangeTypes.indexChanged), rebuildIndexAction], [isChangeType(HierarchyChangeTypes.indexChanged), rebuildIndexAction]
)(diff) )(diff)
const isChangeType = changeType => change => change.type === changeType
const isChangeType = changeType => change =>
change.type === changeType
const action = (diff, compareKey, run) => ({ const action = (diff, compareKey, run) => ({
diff, diff,
compareKey, compareKey,
run, run,
}) })
const reshardRecordsAction = diff => [
const reshardRecordsAction = diff => action(diff, `reshardRecords-${diff.oldNode.nodeKey()}`, runReshardRecords),
[action(diff, `reshardRecords-${diff.oldNode.nodeKey()}`, runReshardRecords)] ]
const rebuildIndexAction = diff => const rebuildIndexAction = diff => [
[action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex)] action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex),
]
const newIndexAction = diff => { const newIndexAction = diff => {
if (isAncestorIndex(diff.newNode)) { if (isAncestorIndex(diff.newNode)) {
return [action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex)] return [
action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex),
]
} else { } else {
return [action(diff, `newIndex-${diff.newNode.nodeKey()}`, runNewIndex)] return [action(diff, `newIndex-${diff.newNode.nodeKey()}`, runNewIndex)]
} }
} }
const deleteIndexAction = diff => const deleteIndexAction = diff => [
[action(diff, `deleteIndex-${diff.oldNode.nodeKey()}`, runDeleteIndex)] action(diff, `deleteIndex-${diff.oldNode.nodeKey()}`, runDeleteIndex),
]
const deleteRecordsAction = diff => const deleteRecordsAction = diff => [
[action(diff, `deleteRecords-${diff.oldNode.nodeKey()}`, runDeleteRecords)] action(diff, `deleteRecords-${diff.oldNode.nodeKey()}`, runDeleteRecords),
]
const renameRecordAction = diff => const renameRecordAction = diff => [
[action(diff, `renameRecords-${diff.oldNode.nodeKey()}`, runRenameRecord)] action(diff, `renameRecords-${diff.oldNode.nodeKey()}`, runRenameRecord),
]
const recordCreatedAction = diff => { const recordCreatedAction = diff => {
if (isTopLevelRecord(diff.newNode)) { if (isTopLevelRecord(diff.newNode)) {
return [action(diff, `initialiseRoot`, runInitialiseRoot)] return [action(diff, `initialiseRoot`, runInitialiseRoot)]
} }
return [action(diff, `initialiseChildRecord-${diff.newNode.nodeKey()}`, runInitialiseChildRecord)] return [
action(
diff,
`initialiseChildRecord-${diff.newNode.nodeKey()}`,
runInitialiseChildRecord
),
]
} }
const rebuildAffectedIndexesAction = diff =>{ const rebuildAffectedIndexesAction = diff => {
const newHierarchy = findRoot(diff.newNode) const newHierarchy = findRoot(diff.newNode)
const oldHierarchy = findRoot(diff.oldNode) const oldHierarchy = findRoot(diff.oldNode)
const indexes = getDependantIndexes(newHierarchy, diff.newNode) const indexes = getDependantIndexes(newHierarchy, diff.newNode)
const changedFields = (() => { const changedFields = (() => {
const addedFields = differenceBy(f => f.name) const addedFields = differenceBy(f => f.name)(diff.oldNode.fields)(
(diff.oldNode.fields) diff.newNode.fields
(diff.newNode.fields) )
const removedFields = differenceBy(f => f.name)(diff.newNode.fields)(
diff.oldNode.fields
)
const removedFields = differenceBy(f => f.name)
(diff.newNode.fields)
(diff.oldNode.fields)
return map(f => f.name)([...addedFields, ...removedFields]) return map(f => f.name)([...addedFields, ...removedFields])
})() })()
const isIndexAffected = i => { const isIndexAffected = i => {
if (!isEqual( if (
generateSchema(oldHierarchy, i), !isEqual(generateSchema(oldHierarchy, i), generateSchema(newHierarchy, i))
generateSchema(newHierarchy, i))) return true )
return true
if (some(f => indexes.filter.indexOf(`record.${f}`) > -1)(changedFields)) if (some(f => indexes.filter.indexOf(`record.${f}`) > -1)(changedFields))
return true return true
if (some(f => indexes.getShardName.indexOf(`record.${f}`) > -1)(changedFields)) if (
some(f => indexes.getShardName.indexOf(`record.${f}`) > -1)(changedFields)
)
return true return true
return false return false
@ -155,10 +167,12 @@ const rebuildAffectedIndexesAction = diff =>{
return $(indexes, [ return $(indexes, [
filter(isIndexAffected), filter(isIndexAffected),
map(i => action({ newNode:i }, `rebuildIndex-${i.nodeKey()}`, runRebuildIndex)) map(i =>
action({ newNode: i }, `rebuildIndex-${i.nodeKey()}`, runRebuildIndex)
),
]) ])
} }
const runReshardRecords = async change => { const runReshardRecords = async change => {
throw new Error("Resharding of records is not supported yet") throw new Error("Resharding of records is not supported yet")
} }
@ -167,7 +181,7 @@ const runRebuildIndex = async (_, newApp, diff) => {
await _buildIndex(newApp, diff.newNode.nodeKey()) await _buildIndex(newApp, diff.newNode.nodeKey())
} }
const runDeleteIndex = async (oldApp, _, diff) => { const runDeleteIndex = async (oldApp, _, diff) => {
await deleteAllIndexFilesForNode(oldApp, diff.oldNode) await deleteAllIndexFilesForNode(oldApp, diff.oldNode)
} }
@ -190,5 +204,5 @@ const runInitialiseRoot = async (_, newApp) => {
} }
const runInitialiseChildRecord = async (_, newApp, diff) => { const runInitialiseChildRecord = async (_, newApp, diff) => {
await initialiseChildrenForNode(newApp.datastore, diff.newNode) await initialiseChildrenForNode(newApp, diff.newNode)
} }

View File

@ -11,7 +11,7 @@ import {
isEmpty, isEmpty,
has, has,
} from "lodash/fp" } from "lodash/fp"
import { compileExpression, compileCode } from "@nx-js/compiler-util" import { compileCode } from "../common/compileCode"
import { import {
$, $,
isSomething, isSomething,
@ -73,7 +73,7 @@ const aggregateGroupRules = [
"condition does not compile", "condition does not compile",
a => a =>
isEmpty(a.condition) || isEmpty(a.condition) ||
executesWithoutException(() => compileExpression(a.condition)) executesWithoutException(() => compileCode(a.condition))
), ),
] ]
@ -196,7 +196,7 @@ const triggerRules = actions => [
t => { t => {
if (!t.condition) return true if (!t.condition) return true
try { try {
compileExpression(t.condition) compileCode(t.condition)
return true return true
} catch (_) { } catch (_) {
return false return false

View File

@ -1,5 +1,5 @@
import { flatten, map, isEmpty } from "lodash/fp" import { flatten, map, isEmpty } from "lodash/fp"
import { compileCode } from "@nx-js/compiler-util" import { compileCode } from "../common/compileCode"
import { isNonEmptyString, executesWithoutException, $ } from "../common" import { isNonEmptyString, executesWithoutException, $ } from "../common"
import { applyRuleSet, makerule } from "../common/validationCommon" import { applyRuleSet, makerule } from "../common/validationCommon"

View File

@ -50,7 +50,6 @@ export const cleanup = async app => {
} finally { } finally {
await releaseLock(app, lock) await releaseLock(app, lock)
} }
} }
const getTransactionLock = async app => const getTransactionLock = async app =>

View File

@ -40,7 +40,7 @@ import {
fieldReversesReferenceToIndex, fieldReversesReferenceToIndex,
isReferenceIndex, isReferenceIndex,
getExactNodeForKey, getExactNodeForKey,
getParentKey getParentKey,
} from "../templateApi/hierarchy" } from "../templateApi/hierarchy"
import { getRecordInfo } from "../recordApi/recordInfo" import { getRecordInfo } from "../recordApi/recordInfo"
import { getIndexDir } from "../indexApi/getIndexDir" import { getIndexDir } from "../indexApi/getIndexDir"
@ -52,7 +52,7 @@ export const executeTransactions = app => async transactions => {
for (const shard of keys(recordsByShard)) { for (const shard of keys(recordsByShard)) {
if (recordsByShard[shard].isRebuild) if (recordsByShard[shard].isRebuild)
await initialiseIndex( await initialiseIndex(
app.datastore, app.datastore,
getParentKey(recordsByShard[shard].indexDir), getParentKey(recordsByShard[shard].indexDir),
recordsByShard[shard].indexNode recordsByShard[shard].indexNode
) )
@ -87,8 +87,9 @@ const mappedRecordsByIndexShard = (hierarchy, transactions) => {
transByShard[t.indexShardKey] = { transByShard[t.indexShardKey] = {
writes: [], writes: [],
removes: [], removes: [],
isRebuild: some(i => i.indexShardKey === t.indexShardKey)(indexBuild.toWrite) isRebuild:
|| some(i => i.indexShardKey === t.indexShardKey)(indexBuild.toRemove), some(i => i.indexShardKey === t.indexShardKey)(indexBuild.toWrite) ||
some(i => i.indexShardKey === t.indexShardKey)(indexBuild.toRemove),
indexDir: t.indexDir, indexDir: t.indexDir,
indexNodeKey: t.indexNode.nodeKey(), indexNodeKey: t.indexNode.nodeKey(),
indexNode: t.indexNode, indexNode: t.indexNode,
@ -219,7 +220,7 @@ const getUpdateTransactionsByShard = (hierarchy, transactions) => {
const getBuildIndexTransactionsByShard = (hierarchy, transactions) => { const getBuildIndexTransactionsByShard = (hierarchy, transactions) => {
const buildTransactions = $(transactions, [filter(isBuildIndex)]) const buildTransactions = $(transactions, [filter(isBuildIndex)])
if (!isNonEmptyArray(buildTransactions)) return { toWrite:[], toRemove:[] } if (!isNonEmptyArray(buildTransactions)) return { toWrite: [], toRemove: [] }
const indexNode = transactions.indexNode const indexNode = transactions.indexNode
const getIndexDirs = t => { const getIndexDirs = t => {
@ -259,7 +260,7 @@ const getBuildIndexTransactionsByShard = (hierarchy, transactions) => {
return $(buildTransactions, [ return $(buildTransactions, [
map(t => { map(t => {
const mappedRecord = evaluate(t.record)(indexNode) const mappedRecord = evaluate(t.record)(indexNode)
mappedRecord.result = mappedRecord.result || t.record mappedRecord.result = mappedRecord.result || t.record
const indexDirs = getIndexDirs(t) const indexDirs = getIndexDirs(t)
return $(indexDirs, [ return $(indexDirs, [
@ -274,16 +275,16 @@ const getBuildIndexTransactionsByShard = (hierarchy, transactions) => {
), ),
})), })),
]) ])
}), }),
flatten, flatten,
reduce((obj, res) => { reduce(
if (res.mappedRecord.passedFilter) (obj, res) => {
obj.toWrite.push(res) if (res.mappedRecord.passedFilter) obj.toWrite.push(res)
else else obj.toRemove.push(res)
obj.toRemove.push(res) return obj
return obj },
}, { toWrite: [], toRemove: [] }) { toWrite: [], toRemove: [] }
),
]) ])
} }

View File

@ -31,15 +31,16 @@ export const retrieve = async app => {
app, app,
joinKey(TRANSACTIONS_FOLDER, buildIndexFolder) joinKey(TRANSACTIONS_FOLDER, buildIndexFolder)
) )
if(transactions.length === 0) { if (transactions.length === 0) {
await app.datastore.deleteFolder( await app.datastore.deleteFolder(
joinKey(TRANSACTIONS_FOLDER, buildIndexFolder)) joinKey(TRANSACTIONS_FOLDER, buildIndexFolder)
)
} else { } else {
return transactions return transactions
} }
currentFolderIndex += 1 currentFolderIndex += 1
} }
return [] return []
} }
@ -65,7 +66,7 @@ const retrieveBuildIndexTransactions = async (app, buildIndexFolder) => {
const files = await app.datastore.getFolderContents(childFolderKey) const files = await app.datastore.getFolderContents(childFolderKey)
if (files.length > 0) { if (files.length > 0) {
return { childFolderKey, files } return { childFolderKey, files }
} }
await app.datastore.deleteFolder(childFolderKey) await app.datastore.deleteFolder(childFolderKey)

View File

@ -11,4 +11,4 @@ export const setCleanupFunc = (app, cleanupTransactions) => {
newCleanup.isDefault = true newCleanup.isDefault = true
app.cleanupTransactions = newCleanup app.cleanupTransactions = newCleanup
} }
} }

View File

@ -14,7 +14,10 @@ export const UPDATE_RECORD_TRANSACTION = "update"
export const DELETE_RECORD_TRANSACTION = "delete" export const DELETE_RECORD_TRANSACTION = "delete"
export const BUILD_INDEX_TRANSACTION = "build" export const BUILD_INDEX_TRANSACTION = "build"
export const isUpdate_Or_Rebuild = isOfType(UPDATE_RECORD_TRANSACTION, BUILD_INDEX_TRANSACTION) export const isUpdate_Or_Rebuild = isOfType(
UPDATE_RECORD_TRANSACTION,
BUILD_INDEX_TRANSACTION
)
export const isUpdate = isOfType(UPDATE_RECORD_TRANSACTION) export const isUpdate = isOfType(UPDATE_RECORD_TRANSACTION)
export const isDelete = isOfType(DELETE_RECORD_TRANSACTION) export const isDelete = isOfType(DELETE_RECORD_TRANSACTION)
export const isCreate = isOfType(CREATE_RECORD_TRANSACTION) export const isCreate = isOfType(CREATE_RECORD_TRANSACTION)

View File

@ -6,7 +6,7 @@ import {
parsedSuccess, parsedSuccess,
getDefaultExport, getDefaultExport,
} from "./typeHelpers" } from "./typeHelpers"
import { switchCase, defaultCase, toDateOrNull } from "../common" import { switchCase, defaultCase, toDateOrNull, isNonEmptyArray } from "../common"
const dateFunctions = typeFunctions({ const dateFunctions = typeFunctions({
default: constant(null), default: constant(null),
@ -21,23 +21,32 @@ const parseStringToDate = s =>
[defaultCase, parsedFailed] [defaultCase, parsedFailed]
)(new Date(s)) )(new Date(s))
const isNullOrEmpty = d =>
isNull(d)
|| (d || "").toString() === ""
const isDateOrEmpty = d =>
isDate(d)
|| isNullOrEmpty(d)
const dateTryParse = switchCase( const dateTryParse = switchCase(
[isDate, parsedSuccess], [isDateOrEmpty, parsedSuccess],
[isString, parseStringToDate], [isString, parseStringToDate],
[isNull, parsedSuccess],
[defaultCase, parsedFailed] [defaultCase, parsedFailed]
) )
const options = { const options = {
maxValue: { maxValue: {
defaultValue: new Date(32503680000000), defaultValue: null,
isValid: isDate, //defaultValue: new Date(32503680000000),
isValid: isDateOrEmpty,
requirementDescription: "must be a valid date", requirementDescription: "must be a valid date",
parse: toDateOrNull, parse: toDateOrNull,
}, },
minValue: { minValue: {
defaultValue: new Date(-8520336000000), defaultValue: null,
isValid: isDate, //defaultValue: new Date(-8520336000000),
isValid: isDateOrEmpty,
requirementDescription: "must be a valid date", requirementDescription: "must be a valid date",
parse: toDateOrNull, parse: toDateOrNull,
}, },
@ -46,7 +55,7 @@ const options = {
const typeConstraints = [ const typeConstraints = [
makerule( makerule(
async (val, opts) => async (val, opts) =>
val === null || opts.minValue === null || val >= opts.minValue, val === null || isNullOrEmpty(opts.minValue) || val >= opts.minValue,
(val, opts) => (val, opts) =>
`value (${val.toString()}) must be greater than or equal to ${ `value (${val.toString()}) must be greater than or equal to ${
opts.minValue opts.minValue
@ -54,7 +63,7 @@ const typeConstraints = [
), ),
makerule( makerule(
async (val, opts) => async (val, opts) =>
val === null || opts.maxValue === null || val <= opts.maxValue, val === null || isNullOrEmpty(opts.maxValue) || val <= opts.maxValue,
(val, opts) => (val, opts) =>
`value (${val.toString()}) must be less than or equal to ${ `value (${val.toString()}) must be less than or equal to ${
opts.minValue opts.minValue

View File

@ -37,7 +37,7 @@ const options = {
isValid: v => isValid: v =>
v === null || (isArrayOfString(v) && v.length > 0 && v.length < 10000), v === null || (isArrayOfString(v) && v.length > 0 && v.length < 10000),
requirementDescription: requirementDescription:
"'values' must be null (no values) or an arry of at least one string", "'values' must be null (no values) or an array of at least one string",
parse: s => s, parse: s => s,
}, },
allowDeclaredValuesOnly: { allowDeclaredValuesOnly: {

View File

@ -8,7 +8,7 @@ import { permission } from "../src/authApi/permissions"
describe("collectionApi > delete", () => { describe("collectionApi > delete", () => {
it("should remove every key in collection's path", async () => { it("should remove every key in collection's path", async () => {
const { recordApi, collectionApi } = await setupApphierarchy( const { recordApi, collectionApi, appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields basicAppHierarchyCreator_WithFields
) )
const record1 = recordApi.getNew("/customers", "customer") const record1 = recordApi.getNew("/customers", "customer")
@ -31,7 +31,10 @@ describe("collectionApi > delete", () => {
filter(k => splitKey(k)[0] === "customers"), filter(k => splitKey(k)[0] === "customers"),
]) ])
expect(remainingKeys).toEqual([]) expect(remainingKeys).toEqual([
"/customers",
`/customers/${appHierarchy.customerRecord.nodeId}`,
])
}) })
it("should not delete anything that is not in its path", async () => { it("should not delete anything that is not in its path", async () => {
@ -51,7 +54,8 @@ describe("collectionApi > delete", () => {
filter(k => splitKey(k)[0] === "customers"), filter(k => splitKey(k)[0] === "customers"),
]) ])
const expectedRemainingKeys = allKeys.length - customerKeys.length const expectedRemainingKeys = allKeys.length - customerKeys.length + 2
// +2 because is should keep the collection folders: /customers & /customers/1
await collectionApi.delete("/customers") await collectionApi.delete("/customers")

View File

@ -39,7 +39,7 @@ export const testTemplatesPath = testAreaName =>
path.join(testFileArea(testAreaName), templateDefinitions) path.join(testFileArea(testAreaName), templateDefinitions)
export const getMemoryStore = () => setupDatastore(memory({})) export const getMemoryStore = () => setupDatastore(memory({}))
export const getMemoryTemplateApi = (store) => { export const getMemoryTemplateApi = store => {
const app = { const app = {
datastore: store || getMemoryStore(), datastore: store || getMemoryStore(),
publish: () => {}, publish: () => {},

View File

@ -0,0 +1,86 @@
import {
setupApphierarchy,
basicAppHierarchyCreator_WithFields,
stubEventHandler,
basicAppHierarchyCreator_WithFields_AndIndexes,
} from "./specHelpers"
import { canDeleteIndex } from "../src/templateApi/canDeleteIndex"
import { canDeleteRecord } from "../src/templateApi/canDeleteRecord"
describe("canDeleteIndex", () => {
it("should return no errors if deltion is valid", async () => {
const { appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields
)
const partnerIndex = appHierarchy.root.indexes.find(i => i.name === "partner_index")
const result = canDeleteIndex(partnerIndex)
expect(result.canDelete).toBe(true)
expect(result.errors).toEqual([])
})
it("should return errors if index is a lookup for a reference field", async () => {
const { appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields
)
const customerIndex = appHierarchy.root.indexes.find(i => i.name === "customer_index")
const result = canDeleteIndex(customerIndex)
expect(result.canDelete).toBe(false)
expect(result.errors.length).toBe(1)
})
it("should return errors if index is a manyToOne index for a reference field", async () => {
const { appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields
)
const referredToCustomersIndex = appHierarchy.customerRecord.indexes.find(i => i.name === "referredToCustomers")
const result = canDeleteIndex(referredToCustomersIndex)
expect(result.canDelete).toBe(false)
expect(result.errors.length).toBe(1)
})
})
describe("canDeleteRecord", () => {
it("should return no errors when deletion is valid", async () => {
const { appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields
)
appHierarchy.root.indexes = appHierarchy.root.indexes.filter(i => !i.allowedRecordNodeIds.includes(appHierarchy.customerRecord.nodeId))
const result = canDeleteRecord(appHierarchy.customerRecord)
expect(result.canDelete).toBe(true)
expect(result.errors).toEqual([])
})
it("should return errors when record is referenced by hierarchal index", async () => {
const { appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields
)
const result = canDeleteRecord(appHierarchy.customerRecord)
expect(result.canDelete).toBe(false)
expect(result.errors.some(e => e.includes("customer_index"))).toBe(true)
})
it("should return errors when record has a child which cannot be deleted", async () => {
const { appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields_AndIndexes
)
const result = canDeleteRecord(appHierarchy.customerRecord)
expect(result.canDelete).toBe(false)
expect(result.errors.some(e => e.includes("Outstanding Invoices"))).toBe(true)
})
})

View File

@ -25,7 +25,7 @@ describe("hierarchy node creation", () => {
expect(record.validationRules).toEqual([]) expect(record.validationRules).toEqual([])
expect(record.indexes).toEqual([]) expect(record.indexes).toEqual([])
expect(record.parent()).toBe(root) expect(record.parent()).toBe(root)
expect(record.collectionName).toBe("") expect(record.collectionName).toBe(record.nodeId.toString())
expect(record.estimatedRecordCount).toBe(1000000) expect(record.estimatedRecordCount).toBe(1000000)
expect(record.isSingle).toBe(false) expect(record.isSingle).toBe(false)

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