Merge
This commit is contained in:
commit
bc60d2dfc8
|
@ -103,6 +103,11 @@ const lodash_fp_exports = [
|
|||
"toNumber",
|
||||
"takeRight",
|
||||
"toPairs",
|
||||
"remove",
|
||||
"findIndex",
|
||||
"compose",
|
||||
"get",
|
||||
"tap",
|
||||
]
|
||||
|
||||
const lodash_exports = [
|
||||
|
@ -159,13 +164,15 @@ export default {
|
|||
}),
|
||||
|
||||
replace({
|
||||
"process.env.NODE_ENV": JSON.stringify(production ? "production" : "development")
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
production ? "production" : "development"
|
||||
),
|
||||
}),
|
||||
|
||||
svelte({
|
||||
// enable run-time checks when not in production
|
||||
dev: !production,
|
||||
include: "src/**/*.svelte",
|
||||
include: ["src/**/*.svelte", "node_modules/**/*.svelte"],
|
||||
// we'll extract any component CSS out into
|
||||
// a separate file — better for performance
|
||||
css: css => {
|
||||
|
|
|
@ -29,16 +29,16 @@
|
|||
{/await}
|
||||
|
||||
<!--
|
||||
<div class="settings">
|
||||
<IconButton icon="settings"
|
||||
on:click={store.showSettings}/>
|
||||
</div>
|
||||
<div class="settings">
|
||||
<IconButton icon="settings"
|
||||
on:click={store.showSettings}/>
|
||||
</div>
|
||||
|
||||
|
||||
{#if $store.useAnalytics}
|
||||
<iframe src="https://marblekirby.github.io/bb-analytics.html" width="0" height="0" style="visibility:hidden;display:none"/>
|
||||
{/if}
|
||||
-->
|
||||
{#if $store.useAnalytics}
|
||||
<iframe src="https://marblekirby.github.io/bb-analytics.html" width="0" height="0" style="visibility:hidden;display:none"/>
|
||||
{/if}
|
||||
-->
|
||||
</main>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
<script>
|
||||
import BackendNav from "./nav/BackendNav.svelte"
|
||||
import SchemaManagementDrawer from "./nav/SchemaManagementDrawer.svelte"
|
||||
import Database from "./database/DatabaseRoot.svelte"
|
||||
import UserInterface from "./userInterface/UserInterfaceRoot.svelte"
|
||||
import ActionsAndTriggers from "./actionsAndTriggers/ActionsAndTriggersRoot.svelte"
|
||||
import AccessLevels from "./accessLevels/AccessLevelsRoot.svelte"
|
||||
import ComingSoon from "./common/ComingSoon.svelte"
|
||||
|
||||
import { store } from "./builderStore"
|
||||
|
||||
export let navWidth = "50px"
|
||||
import { store, backendUiStore } from "./builderStore"
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
|
@ -16,29 +15,29 @@
|
|||
<BackendNav />
|
||||
</div>
|
||||
<div class="content">
|
||||
{#if $store.activeNav === 'database'}
|
||||
{#if $backendUiStore.leftNavItem === 'DATABASE'}
|
||||
<Database />
|
||||
{:else if $store.activeNav === 'actions'}
|
||||
{:else if $backendUiStore.leftNavItem === 'ACTIONS'}
|
||||
<ActionsAndTriggers />
|
||||
{:else if $store.activeNav === 'access levels'}
|
||||
{:else if $backendUiStore.leftNavItem === 'ACCESS_LEVELS'}
|
||||
<AccessLevels />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="nav">
|
||||
<SchemaManagementDrawer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
background-color: var(--white);
|
||||
margin: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin: 80px 60px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
|
|
|
@ -4,52 +4,58 @@
|
|||
import UserInterfaceRoot from "./userInterface/UserInterfaceRoot.svelte"
|
||||
import BackendRoot from "./BackendRoot.svelte"
|
||||
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>
|
||||
|
||||
<div class="root">
|
||||
|
||||
<div class="top-nav">
|
||||
<div class="topleftnav">
|
||||
<button class="home-logo">
|
||||
<img src="/_builder/assets/budibase-emblem-white.svg" />
|
||||
<button class="home-logo">
|
||||
<img src="/_builder/assets/budibase-emblem-white.svg" />
|
||||
</button>
|
||||
<!-- <IconButton icon="home"
|
||||
color="var(--slate)"
|
||||
hoverColor="var(--secondary75)"/> -->
|
||||
<span
|
||||
class:active={$store.isBackend}
|
||||
class:active={selectedTab === TABS.BACKEND}
|
||||
class="topnavitem"
|
||||
on:click={store.showBackend}>
|
||||
on:click={() => (selectedTab = TABS.BACKEND)}>
|
||||
Backend
|
||||
</span>
|
||||
<span
|
||||
class:active={!$store.isBackend}
|
||||
class:active={selectedTab === TABS.FRONTEND}
|
||||
class="topnavitem"
|
||||
on:click={store.showFrontend}>
|
||||
on:click={() => (selectedTab = TABS.FRONTEND)}>
|
||||
Frontend
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="toprightnav">
|
||||
<span
|
||||
class:active={!$store.isBackend}
|
||||
class:active={selectedTab === TABS.FRONTEND}
|
||||
class="topnavitemright"
|
||||
on:click={store.showFrontend}>
|
||||
<SettingsIcon />
|
||||
on:click={() => selectedTab === TABS.FRONTEND}>
|
||||
<SettingsIcon />
|
||||
</span>
|
||||
<span
|
||||
class:active={!$store.isBackend}
|
||||
class:active={selectedTab === TABS.FRONTEND}
|
||||
class="topnavitemright"
|
||||
on:click={store.showFrontend}>
|
||||
<PreviewIcon />
|
||||
on:click={() => selectedTab === TABS.FRONTEND}>
|
||||
<PreviewIcon />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
{#if $store.isBackend}
|
||||
{#if selectedTab === TABS.BACKEND}
|
||||
<div in:fade out:fade>
|
||||
<BackendRoot />
|
||||
</div>
|
||||
|
@ -133,7 +139,7 @@
|
|||
font-size: 1rem;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex:1;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import ButtonGroup from "../common/ButtonGroup.svelte"
|
||||
import Button from "../common/Button.svelte"
|
||||
import ActionButton from "../common/ActionButton.svelte"
|
||||
import { store } from "../builderStore"
|
||||
import { store, backendUiStore } from "../builderStore"
|
||||
import { generateFullPermissions, getNewAccessLevel } from "../common/core"
|
||||
import getIcon from "../common/icon"
|
||||
import AccessLevelView from "./AccessLevelView.svelte"
|
||||
|
@ -10,7 +10,12 @@
|
|||
|
||||
let editingLevel = null
|
||||
let editingLevelIsNew = false
|
||||
$: isEditing = editingLevel !== null
|
||||
$: {
|
||||
if (editingLevel !== null) {
|
||||
backendUiStore.actions.modals.show("ACCESS_LEVELS")
|
||||
}
|
||||
}
|
||||
$: modalOpen = $backendUiStore.visibleModal === "ACCESS_LEVELS"
|
||||
|
||||
let allPermissions = []
|
||||
store.subscribe(db => {
|
||||
|
@ -40,6 +45,7 @@
|
|||
store.saveLevel(level, editingLevelIsNew, editingLevel)
|
||||
}
|
||||
editingLevel = null
|
||||
backendUiStore.actions.modals.hide()
|
||||
}
|
||||
|
||||
const getPermissionsString = perms => {
|
||||
|
@ -83,19 +89,17 @@
|
|||
{:else}(no actions added){/if}
|
||||
|
||||
<Modal
|
||||
onClosed={() => (isEditing = false)}
|
||||
bind:isOpen={isEditing}
|
||||
title={isEditing ? 'Edit Access Level' : 'Create Access Level'}>
|
||||
{#if isEditing}
|
||||
<AccessLevelView
|
||||
level={editingLevel}
|
||||
{allPermissions}
|
||||
onFinished={onEditingFinished}
|
||||
isNew={editingLevelIsNew}
|
||||
allLevels={$store.accessLevels.levels}
|
||||
hierarchy={$store.hierarchy}
|
||||
actions={$store.actions} />
|
||||
{/if}
|
||||
onClosed={backendUiStore.actions.modals.hide}
|
||||
bind:isOpen={modalOpen}
|
||||
title={modalOpen ? 'Edit Access Level' : 'Create Access Level'}>
|
||||
<AccessLevelView
|
||||
level={editingLevel}
|
||||
{allPermissions}
|
||||
onFinished={onEditingFinished}
|
||||
isNew={editingLevelIsNew}
|
||||
allLevels={$store.accessLevels.levels}
|
||||
hierarchy={$store.hierarchy}
|
||||
actions={$store.actions} />
|
||||
</Modal>
|
||||
|
||||
</div>
|
||||
|
@ -106,13 +110,4 @@
|
|||
position: relative;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.actions-header {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.node-view {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -91,4 +91,38 @@
|
|||
.uk-text-right {
|
||||
display: flex;
|
||||
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;
|
||||
}
|
|
@ -39,7 +39,7 @@ const css_map = {
|
|||
},
|
||||
direction: {
|
||||
name: "flex-direction",
|
||||
generate: self
|
||||
generate: self,
|
||||
},
|
||||
gridarea: {
|
||||
name: "grid-area",
|
||||
|
@ -113,7 +113,7 @@ const object_to_css_string = [
|
|||
export const generate_css = ({ layout, position }) => {
|
||||
let _layout = pipe(layout, object_to_css_string)
|
||||
if (_layout.length) {
|
||||
_layout += `\ndisplay: ${_layout.includes("flex") ? "flex" : "grid"};`;
|
||||
_layout += `\ndisplay: ${_layout.includes("flex") ? "flex" : "grid"};`
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import getStore from "./store"
|
||||
import LogRocket from "logrocket";
|
||||
import { getStore } from "./store"
|
||||
import { getBackendUiStore } from "./store/backend"
|
||||
import LogRocket from "logrocket"
|
||||
|
||||
export const store = getStore()
|
||||
export const backendUiStore = getBackendUiStore()
|
||||
|
||||
export const initialise = async () => {
|
||||
try {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
LogRocket.init("knlald/budibase");
|
||||
LogRocket.init("knlald/budibase")
|
||||
}
|
||||
setupRouter(store)
|
||||
await store.initialise()
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -1,39 +1,23 @@
|
|||
import { hierarchy as hierarchyFunctions } from "../../../core/src"
|
||||
import {
|
||||
filter,
|
||||
cloneDeep,
|
||||
sortBy,
|
||||
map,
|
||||
last,
|
||||
concat,
|
||||
find,
|
||||
isEmpty,
|
||||
values,
|
||||
} from "lodash/fp"
|
||||
import {
|
||||
pipe,
|
||||
getNode,
|
||||
validate,
|
||||
constructHierarchy,
|
||||
templateApi,
|
||||
} from "../common/core"
|
||||
import { filter, cloneDeep, last, concat, isEmpty, values } from "lodash/fp"
|
||||
import { pipe, getNode, constructHierarchy } from "../../common/core"
|
||||
import * as backendStoreActions from "./backend"
|
||||
import { writable } from "svelte/store"
|
||||
import { defaultPagesObject } from "../userInterface/pagesParsing/defaultPagesObject"
|
||||
import api from "./api"
|
||||
import { getExactComponent } from "../userInterface/pagesParsing/searchComponents"
|
||||
import { rename } from "../userInterface/pagesParsing/renameScreen"
|
||||
import { defaultPagesObject } from "../../userInterface/pagesParsing/defaultPagesObject"
|
||||
import api from "../api"
|
||||
import { getExactComponent } from "../../userInterface/pagesParsing/searchComponents"
|
||||
import { rename } from "../../userInterface/pagesParsing/renameScreen"
|
||||
import {
|
||||
getNewScreen,
|
||||
createProps,
|
||||
makePropsSafe,
|
||||
getBuiltin,
|
||||
} from "../userInterface/pagesParsing/createProps"
|
||||
import { expandComponentDefinition } from "../userInterface/pagesParsing/types"
|
||||
import { loadLibs, libUrlsForPreview } from "./loadComponentLibraries"
|
||||
import { buildCodeForScreens } from "./buildCodeForScreens"
|
||||
import { generate_screen_css } from "./generate_css"
|
||||
import { insertCodeMetadata } from "./insertCodeMetadata"
|
||||
import { uuid } from "./uuid"
|
||||
} from "../../userInterface/pagesParsing/createProps"
|
||||
import { expandComponentDefinition } from "../../userInterface/pagesParsing/types"
|
||||
import { loadLibs, libUrlsForPreview } from "../loadComponentLibraries"
|
||||
import { buildCodeForScreens } from "../buildCodeForScreens"
|
||||
import { generate_screen_css } from "../generate_css"
|
||||
import { insertCodeMetadata } from "../insertCodeMetadata"
|
||||
import { uuid } from "../uuid"
|
||||
|
||||
let appname = ""
|
||||
|
||||
|
@ -55,8 +39,6 @@ export const getStore = () => {
|
|||
currentComponentProps: null,
|
||||
currentNodeIsNew: false,
|
||||
errors: [],
|
||||
activeNav: "database",
|
||||
isBackend: true,
|
||||
hasAppPackage: false,
|
||||
accessLevels: { version: 0, levels: [] },
|
||||
currentNode: null,
|
||||
|
@ -68,23 +50,25 @@ export const getStore = () => {
|
|||
const store = writable(initial)
|
||||
|
||||
store.initialise = initialise(store, initial)
|
||||
store.newChildRecord = newRecord(store, false)
|
||||
store.newRootRecord = newRecord(store, true)
|
||||
store.selectExistingNode = selectExistingNode(store)
|
||||
store.newChildIndex = newIndex(store, false)
|
||||
store.newRootIndex = newIndex(store, true)
|
||||
store.saveCurrentNode = saveCurrentNode(store)
|
||||
|
||||
store.newChildRecord = backendStoreActions.newRecord(store, false)
|
||||
store.newRootRecord = backendStoreActions.newRecord(store, true)
|
||||
store.selectExistingNode = backendStoreActions.selectExistingNode(store)
|
||||
store.newChildIndex = backendStoreActions.newIndex(store, false)
|
||||
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.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.addComponentLibrary = addComponentLibrary(store)
|
||||
store.renameScreen = renameScreen(store)
|
||||
|
@ -96,8 +80,6 @@ export const getStore = () => {
|
|||
store.addStylesheet = addStylesheet(store)
|
||||
store.removeStylesheet = removeStylesheet(store)
|
||||
store.savePage = savePage(store)
|
||||
store.showFrontend = showFrontend(store)
|
||||
store.showBackend = showBackend(store)
|
||||
store.showSettings = showSettings(store)
|
||||
store.useAnalytics = useAnalytics(store)
|
||||
store.createGeneratedComponents = createGeneratedComponents(store)
|
||||
|
@ -159,10 +141,6 @@ const initialise = (store, initial) => async () => {
|
|||
}
|
||||
initial.appname = appname
|
||||
initial.pages = pkg.pages
|
||||
initial.currentInstanceId =
|
||||
pkg.application.instances && pkg.application.instances.length > 0
|
||||
? pkg.application.instances[0].id
|
||||
: ""
|
||||
initial.hasAppPackage = true
|
||||
initial.hierarchy = pkg.appDefinition.hierarchy
|
||||
initial.accessLevels = pkg.accessLevels
|
||||
|
@ -174,12 +152,15 @@ const initialise = (store, initial) => async () => {
|
|||
initial.builtins = [getBuiltin("##builtin/screenslot")]
|
||||
initial.actions = values(pkg.appDefinition.actions)
|
||||
initial.triggers = pkg.appDefinition.triggers
|
||||
initial.appInstances = pkg.application.instances
|
||||
initial.appId = pkg.application.id
|
||||
|
||||
if (!!initial.hierarchy && !isEmpty(initial.hierarchy)) {
|
||||
initial.hierarchy = constructHierarchy(initial.hierarchy)
|
||||
const shadowHierarchy = createShadowHierarchy(initial.hierarchy)
|
||||
if (initial.currentNode !== null)
|
||||
if (initial.currentNode !== null) {
|
||||
initial.currentNode = getNode(shadowHierarchy, initial.currentNode.nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
store.set(initial)
|
||||
|
@ -187,121 +168,16 @@ const initialise = (store, initial) => async () => {
|
|||
}
|
||||
|
||||
const showSettings = store => () => {
|
||||
store.update(s => {
|
||||
s.showSettings = !s.showSettings
|
||||
return s
|
||||
store.update(state => {
|
||||
state.showSettings = !state.showSettings
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const useAnalytics = store => () => {
|
||||
store.update(s => {
|
||||
s.useAnalytics = !s.useAnalytics
|
||||
return s
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
store.update(state => {
|
||||
state.useAnalytics = !state.useAnalytics
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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 =>
|
||||
constructHierarchy(JSON.parse(JSON.stringify(hierarchy)))
|
||||
|
||||
|
@ -474,55 +213,27 @@ const _saveScreen = async (store, s, screen) => {
|
|||
screen
|
||||
)
|
||||
.then(() => {
|
||||
if (currentPageScreens.includes(screen)) return
|
||||
|
||||
if(currentPageScreens.includes(screen)) return
|
||||
|
||||
const screens = [
|
||||
...currentPageScreens,
|
||||
screen,
|
||||
]
|
||||
const screens = [...currentPageScreens, screen]
|
||||
|
||||
store.update(innerState => {
|
||||
innerState.pages[s.currentPageName]._screens = screens
|
||||
innerState.screens = screens
|
||||
innerState.currentPreviewItem = screen
|
||||
const safeProps = makePropsSafe(
|
||||
getComponentDefinition(innerState.components, screen.props._component),
|
||||
getComponentDefinition(
|
||||
innerState.components,
|
||||
screen.props._component
|
||||
),
|
||||
screen.props
|
||||
)
|
||||
innerState.currentComponentInfo = safeProps
|
||||
screen.props = safeProps
|
||||
|
||||
|
||||
_savePage(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
|
||||
|
@ -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 => {
|
||||
store.update(s => {
|
||||
const current_screens = s.pages[pageName]._screens
|
||||
|
@ -772,10 +472,7 @@ const setCurrentPage = store => pageName => {
|
|||
})
|
||||
}
|
||||
|
||||
const getContainerComponent = components =>
|
||||
getComponentDefinition(components, "@budibase/standard-components/container")
|
||||
|
||||
const getComponentDefinition = (components, name) =>
|
||||
const getComponentDefinition = (components, name) =>
|
||||
components.find(c => c.name === name)
|
||||
|
||||
/**
|
||||
|
@ -834,8 +531,11 @@ const addTemplatedComponent = store => props => {
|
|||
state.currentComponentInfo._children = state.currentComponentInfo._children.concat(
|
||||
props
|
||||
)
|
||||
state.currentPreviewItem._css = generate_screen_css([state.currentPreviewItem.props])
|
||||
state.currentPreviewItem._css = generate_screen_css([
|
||||
state.currentPreviewItem.props,
|
||||
])
|
||||
|
||||
setCurrentPageFunctions(state)
|
||||
_saveCurrentPreviewItem(state)
|
||||
|
||||
return state
|
|
@ -2,36 +2,20 @@
|
|||
import { JavaScriptIcon } from "../common/Icons"
|
||||
// todo: use https://ace.c9.io
|
||||
export let text = ""
|
||||
export let label = ""
|
||||
export let javascript = false
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
{#if javascript}
|
||||
<JavaScriptIcon />
|
||||
{/if}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
<textarea class="uk-textarea" bind:value={text} />
|
||||
|
||||
<style>
|
||||
textarea {
|
||||
padding: 3px;
|
||||
padding: 10px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
background: var(--lightslate);
|
||||
background: var(--secondary80);
|
||||
color: var(--white);
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
width: 95%;
|
||||
height: 100px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -53,14 +53,12 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.uk-modal-footer {
|
||||
background: var(--lightslate);
|
||||
}
|
||||
|
||||
.uk-modal-footer {
|
||||
background: var(--lightslate);
|
||||
}
|
||||
|
||||
.uk-modal-dialog {
|
||||
width: 400px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.uk-modal-dialog {
|
||||
width: 400px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,25 +5,9 @@
|
|||
</script>
|
||||
|
||||
{#if hasErrors}
|
||||
<div class="error-container">
|
||||
<div uk-alert class="uk-alert-danger">
|
||||
{#each errors as error}
|
||||
<div class="error-row">
|
||||
{error.field ? `${error.field}: ` : ''}{error.error}
|
||||
</div>
|
||||
<div>{error.field ? `${error.field}: ` : ''}{error.error}</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/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>
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
width="24"
|
||||
height="24">
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 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>
|
||||
<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"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 349 B After Width: | Height: | Size: 362 B |
|
@ -3,6 +3,9 @@
|
|||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24">
|
||||
<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"/>
|
||||
</svg>
|
||||
<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" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 244 B After Width: | Height: | Size: 263 B |
|
@ -18,4 +18,3 @@ export { default as AddIcon } from "./Add.svelte"
|
|||
export { default as JavaScriptIcon } from "./JavaScript.svelte"
|
||||
export { default as PreviewIcon } from "./Preview.svelte"
|
||||
export { default as SettingsIcon } from "./Settings.svelte"
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import ActionButton from "../common/ActionButton.svelte"
|
||||
|
||||
export let isOpen = false
|
||||
export let onClosed = () => {}
|
||||
export let onClosed
|
||||
export let id = ""
|
||||
export let title
|
||||
|
||||
|
@ -27,19 +27,24 @@
|
|||
</script>
|
||||
|
||||
<div bind:this={ukModal} uk-modal {id}>
|
||||
<div class="uk-modal-dialog" uk-overflow-auto>
|
||||
{#if title}
|
||||
<div class="uk-modal-header">
|
||||
<h4 class="budibase__title--4">{title}</h4>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="uk-modal-body">
|
||||
{#if onClosed}
|
||||
<button class="uk-modal-close-default" type="button" uk-close />
|
||||
{#if isOpen}
|
||||
<div class="uk-modal-dialog" uk-overflow-auto>
|
||||
{#if title}
|
||||
<div class="uk-modal-header">
|
||||
<h4 class="budibase__title--4">{title}</h4>
|
||||
</div>
|
||||
{/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>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -49,5 +54,6 @@
|
|||
height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
<script>
|
||||
import getIcon from "./icon"
|
||||
|
||||
export let icon
|
||||
export let value
|
||||
</script>
|
||||
|
||||
<div class="select-container">
|
||||
<select on:change bind:value>
|
||||
{#if icon}
|
||||
<i class={icon} />
|
||||
{/if}
|
||||
<select class:adjusted={icon} on:change bind:value>
|
||||
<slot />
|
||||
</select>
|
||||
<span class="arrow">
|
||||
|
@ -14,12 +19,22 @@
|
|||
|
||||
<style>
|
||||
.select-container {
|
||||
padding-bottom: 10px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--secondary50);
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
max-width: 300px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.adjusted {
|
||||
padding-left: 2.5em;
|
||||
}
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
select {
|
||||
|
@ -42,7 +57,6 @@
|
|||
.arrow {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: 30px;
|
||||
|
|
|
@ -25,6 +25,6 @@
|
|||
<style>
|
||||
textarea {
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
height: 100px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -9,8 +9,11 @@ import { find, filter, keyBy, flatten, map } from "lodash/fp"
|
|||
import { generateSchema } from "../../../core/src/indexing/indexSchemaCreator"
|
||||
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 { joinKey } from "../../../core/src/common"
|
||||
export { getExactNodeForKey } from "../../../core/src/templateApi/hierarchy"
|
||||
export const pipe = common.$
|
||||
|
||||
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 =>
|
||||
!type ? {} : allTypes[type].getDefaultOptions()
|
||||
|
||||
|
@ -118,6 +124,7 @@ export const getNewInstance = (appId, name) => {
|
|||
version: { key: "" },
|
||||
isNew: true,
|
||||
type: "instance",
|
||||
datastoreconfig: "",
|
||||
id,
|
||||
name,
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -1,70 +1,83 @@
|
|||
<script>
|
||||
import HierarchyRow from "./HierarchyRow.svelte"
|
||||
import RecordView from "./RecordView.svelte"
|
||||
import ModelView from "./ModelView.svelte"
|
||||
import IndexView from "./IndexView.svelte"
|
||||
import ActionsHeader from "./ActionsHeader.svelte"
|
||||
import { store } from "../builderStore"
|
||||
import ModelDataTable from "./ModelDataTable"
|
||||
import { store, backendUiStore } from "../builderStore"
|
||||
import getIcon from "../common/icon"
|
||||
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 = [
|
||||
{
|
||||
label: "New Root Index",
|
||||
onclick: store.newRootIndex,
|
||||
},
|
||||
]
|
||||
async function selectRecord(record) {
|
||||
selectedRecord = await api.loadRecord(record.key, {
|
||||
appname: $store.appname,
|
||||
instanceId: $backendUiStore.selectedDatabase.id,
|
||||
})
|
||||
}
|
||||
|
||||
const defaultNewRecordActions = [
|
||||
{
|
||||
label: "New Root Record",
|
||||
onclick: store.newRootRecord,
|
||||
},
|
||||
]
|
||||
function onClosed() {
|
||||
backendUiStore.actions.modals.hide()
|
||||
}
|
||||
|
||||
let newIndexActions = defaultNewIndexActions
|
||||
let newRecordActions = defaultNewRecordActions
|
||||
|
||||
store.subscribe(db => {
|
||||
if (!db.currentNode || hierarchyFunctions.isIndex(db.currentNode)) {
|
||||
newRecordActions = defaultNewRecordActions
|
||||
newIndexActions = defaultNewIndexActions
|
||||
} 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,
|
||||
},
|
||||
]
|
||||
}
|
||||
})
|
||||
$: recordOpen = $backendUiStore.visibleModal === "RECORD"
|
||||
$: modelOpen = $backendUiStore.visibleModal === "MODEL"
|
||||
$: viewOpen = $backendUiStore.visibleModal === "VIEW"
|
||||
$: databaseOpen = $backendUiStore.visibleModal === "DATABASE"
|
||||
$: deleteRecordOpen = $backendUiStore.visibleModal === "DELETE_RECORD"
|
||||
$: userOpen = $backendUiStore.visibleModal === "USER"
|
||||
$: breadcrumbs = $backendUiStore.breadcrumbs.join(" / ")
|
||||
</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="actions-header">
|
||||
{#if $store.currentNode}
|
||||
<ActionsHeader left={hierarchyWidth} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="node-view">
|
||||
{#if !$store.currentNode}
|
||||
<h1 style="margin-left: 100px">:)</h1>
|
||||
{:else if $store.currentNode.type === 'record'}
|
||||
<RecordView />
|
||||
{:else}
|
||||
<IndexView />
|
||||
{/if}
|
||||
<div class="database-actions">
|
||||
<div class="budibase__label--big">{breadcrumbs}</div>
|
||||
{#if $backendUiStore.selectedDatabase.id}
|
||||
<ActionButton
|
||||
primary
|
||||
on:click={() => {
|
||||
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>
|
||||
|
||||
|
@ -74,12 +87,13 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.actions-header {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.node-view {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.database-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
import DatePicker from "../common/DatePicker.svelte"
|
||||
import {
|
||||
cloneDeep,
|
||||
assign,
|
||||
keys,
|
||||
isNumber,
|
||||
includes,
|
||||
|
@ -60,7 +59,7 @@
|
|||
errors = validate.field(allFields)(clonedField)
|
||||
if (errors.length > 0) return
|
||||
field.typeOptions = cloneDeep(clonedField.typeOptions)
|
||||
onFinished(assign(field)(clonedField))
|
||||
onFinished({ ...field, ...clonedField })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -68,20 +67,14 @@
|
|||
|
||||
<ErrorsBox {errors} />
|
||||
|
||||
<form class="uk-form-horizontal">
|
||||
|
||||
<form class="uk-form-stacked">
|
||||
<Textbox label="Name" bind:text={clonedField.name} />
|
||||
<Dropdown
|
||||
label="Type"
|
||||
bind:selected={clonedField.type}
|
||||
options={keys(allTypes)}
|
||||
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} />
|
||||
|
||||
{#if clonedField.type === 'string'}
|
||||
|
@ -89,7 +82,7 @@
|
|||
label="Max Length"
|
||||
bind:value={clonedField.typeOptions.maxLength} />
|
||||
<ValuesList
|
||||
label="Values (options)"
|
||||
label="Categories"
|
||||
bind:values={clonedField.typeOptions.values} />
|
||||
<Checkbox
|
||||
label="Declared Values Only"
|
||||
|
@ -144,12 +137,19 @@
|
|||
{/if}
|
||||
</form>
|
||||
|
||||
<div class="uk-modal-footer uk-text-right">
|
||||
<ButtonGroup>
|
||||
<ActionButton primary on:click={save}>Save</ActionButton>
|
||||
<ActionButton alert on:click={() => onFinished(false)}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<footer>
|
||||
<ActionButton primary on:click={save}>Save</ActionButton>
|
||||
<ActionButton alert on:click={() => onFinished(false)}>Cancel</ActionButton>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
position: absolute;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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>
|
|
@ -4,31 +4,42 @@
|
|||
import Button from "../common/Button.svelte"
|
||||
import Dropdown from "../common/Dropdown.svelte"
|
||||
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 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 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 => {
|
||||
index = $store.currentNode
|
||||
indexableRecords = pipe($store.hierarchy, [
|
||||
hierarchyFunctions.getFlattenedHierarchy,
|
||||
filter(hierarchyFunctions.isDecendant(index.parent())),
|
||||
filter(hierarchyFunctions.isRecord),
|
||||
map(n => ({
|
||||
node: n,
|
||||
isallowed: some(id => n.nodeId === id)(index.allowedRecordNodeIds),
|
||||
})),
|
||||
])
|
||||
indexableRecords = indexableRecordsFromIndex($store.hierarchy)
|
||||
})
|
||||
|
||||
const toggleAllowedRecord = record => {
|
||||
if (record.isallowed) {
|
||||
index.allowedRecordNodeIds = filter(id => id !== record.node.nodeId)(
|
||||
index.allowedRecordNodeIds
|
||||
index.allowedRecordNodeIds = index.allowedRecordNodeIds.filter(
|
||||
id => id !== record.node.nodeId
|
||||
)
|
||||
} else {
|
||||
index.allowedRecordNodeIds.push(record.node.nodeId)
|
||||
|
@ -36,28 +47,65 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<form class="uk-form-horizontal root">
|
||||
<Textbox bind:text={index.name} label="Name" />
|
||||
<heading>
|
||||
<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="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}
|
||||
<input
|
||||
class="uk-checkbox"
|
||||
type="checkbox"
|
||||
checked={rec.isallowed}
|
||||
on:change={() => toggleAllowedRecord(rec)} />
|
||||
<span>{rec.node.name}</span>
|
||||
<span class="checkbox-model-label">{rec.node.name}</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Dropdown
|
||||
label="Index Type"
|
||||
bind:selected={index.indexType}
|
||||
options={['ancestor', 'reference']} />
|
||||
<h4 class="budibase__label--big">Snippets</h4>
|
||||
{#each Object.values(SNIPPET_EDITORS) as snippetType}
|
||||
<span
|
||||
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" />
|
||||
<CodeArea bind:text={index.filter} javascript label="Filter" />
|
||||
<CodeArea javascript bind:text={index.getShardName} label="Shard Name" />
|
||||
<ActionButton color="secondary" on:click={store.saveCurrentNode}>
|
||||
Save
|
||||
</ActionButton>
|
||||
|
||||
{#if !$store.currentNodeIsNew}
|
||||
<ActionButton alert on:click={store.deleteCurrentNode}>Delete</ActionButton>
|
||||
{/if}
|
||||
|
||||
</form>
|
||||
|
||||
|
@ -75,8 +123,25 @@
|
|||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.index-label {
|
||||
color: #333;
|
||||
font-size: 0.875rem;
|
||||
.snippet-selector__heading {
|
||||
margin-right: 20px;
|
||||
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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./ModelDataTable.svelte"
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,8 @@
|
|||
<script>
|
||||
import IndexView from "../../IndexView.svelte"
|
||||
import * as api from "../api"
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<IndexView />
|
||||
</section>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
|
@ -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>
|
|
@ -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>
|
|
@ -104,4 +104,8 @@ h5 {
|
|||
font-family: var(--fontblack);
|
||||
font-size: 12pt;
|
||||
color: var(--darkslate);
|
||||
}
|
||||
|
||||
.hoverable:hover {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -1,96 +1,48 @@
|
|||
<script>
|
||||
import { store } from "../builderStore"
|
||||
import { getContext } from "svelte"
|
||||
import { store, backendUiStore } from "../builderStore"
|
||||
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 NavItem from "./NavItem.svelte"
|
||||
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>
|
||||
|
||||
<div class="items-root">
|
||||
<div class="hierarchy">
|
||||
<div class="components-list-container">
|
||||
<div class="nav-group-header">
|
||||
<div>
|
||||
{@html getIcon('database', '18')}
|
||||
</div>
|
||||
<div class="hierarchy-title">Database</div>
|
||||
<DropdownButton iconName="plus" actions={newChildActions} />
|
||||
<div class="hierarchy-title">Databases</div>
|
||||
<i
|
||||
class="ri-add-line hoverable"
|
||||
on:click={() => backendUiStore.actions.modals.show('DATABASE')} />
|
||||
</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}
|
||||
<DatabasesList />
|
||||
</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" />
|
||||
<NavItem name="access levels" label="User Levels" />
|
||||
<div class="hierarchy-items-container">
|
||||
<UsersList />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
<NavItem name="ACCESS_LEVELS" label="User Levels" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -103,40 +55,15 @@
|
|||
}
|
||||
|
||||
.nav-group-header {
|
||||
display: grid;
|
||||
grid-template-columns: [icon] auto [title] 1fr [button] auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
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 {
|
||||
flex: auto 1 1;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.hierarchy {
|
||||
|
|
|
@ -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>
|
|
@ -1,29 +1,41 @@
|
|||
<script>
|
||||
import { store } from "../builderStore"
|
||||
import { getContext } from "svelte"
|
||||
import { store, backendUiStore } from "../builderStore"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import getIcon from "../common/icon"
|
||||
|
||||
export let level = 0
|
||||
export let node
|
||||
export let type
|
||||
|
||||
let navActive = ""
|
||||
$: icon = type === "index" ? "list" : "file"
|
||||
|
||||
const ICON_MAP = {
|
||||
index: "ri-eye-line",
|
||||
record: "ri-list-settings-line",
|
||||
}
|
||||
|
||||
store.subscribe(state => {
|
||||
if (state.currentNode) {
|
||||
navActive =
|
||||
state.activeNav === "database" && node.nodeId === state.currentNode.nodeId
|
||||
navActive = 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>
|
||||
|
||||
<div>
|
||||
<div
|
||||
on:click={() => store.selectExistingNode(node.nodeId)}
|
||||
class="budibase__nav-item"
|
||||
on:click={() => selectHierarchyItem(node)}
|
||||
class="budibase__nav-item hierarchy-item"
|
||||
class:capitalized={type === 'record'}
|
||||
style="padding-left: {20 + level * 20}px"
|
||||
class:selected={navActive}>
|
||||
{@html getIcon(icon, 12)}
|
||||
<i class={ICON_MAP[type]} />
|
||||
<span style="margin-left: 1rem">{node.name}</span>
|
||||
</div>
|
||||
{#if node.children}
|
||||
|
@ -37,3 +49,14 @@
|
|||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hierarchy-item {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.capitalized {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
<script>
|
||||
import { store } from "../builderStore"
|
||||
import getIcon from "../common/icon"
|
||||
import { backendUiStore } from "../builderStore"
|
||||
|
||||
export let name = ""
|
||||
export let label = ""
|
||||
|
||||
let navActive = ""
|
||||
$: navActive = $backendUiStore.leftNavItem === name
|
||||
|
||||
store.subscribe(db => {
|
||||
navActive = db.activeNav === name
|
||||
})
|
||||
|
||||
const setActive = () => store.setActiveNav(name)
|
||||
const setActive = () => backendUiStore.actions.navigate(name)
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { store } from "../builderStore/store"
|
||||
import { store } from "../builderStore"
|
||||
import UIkit from "uikit"
|
||||
import ActionButton from "../common/ActionButton.svelte"
|
||||
import ButtonGroup from "../common/ButtonGroup.svelte"
|
||||
|
|
|
@ -178,8 +178,6 @@
|
|||
height: 48px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
li button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
|
|
@ -35,16 +35,13 @@
|
|||
$: templatesByComponent = groupBy(t => t.component)($store.templates)
|
||||
$: hierarchy = $store.hierarchy
|
||||
$: libraryModules = $store.libraries
|
||||
$: standaloneTemplates = pipe(
|
||||
templatesByComponent,
|
||||
[
|
||||
values,
|
||||
flatten,
|
||||
filter(t => !$store.components.some(c => c.name === t.component)),
|
||||
map(t => ({ name: splitName(t.component).componentName, template: t })),
|
||||
uniqBy(t => t.name),
|
||||
]
|
||||
)
|
||||
$: standaloneTemplates = pipe(templatesByComponent, [
|
||||
values,
|
||||
flatten,
|
||||
filter(t => !$store.components.some(c => c.name === t.component)),
|
||||
map(t => ({ name: splitName(t.component).componentName, template: t })),
|
||||
uniqBy(t => t.name),
|
||||
])
|
||||
|
||||
const addRootComponent = (component, allComponents) => {
|
||||
const { libName } = splitName(component.name)
|
||||
|
@ -278,7 +275,7 @@
|
|||
background: #fafafa;
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
color:var(--secondary80);
|
||||
color: var(--secondary80);
|
||||
}
|
||||
|
||||
.preset-menu > span {
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -59,6 +59,7 @@
|
|||
|
||||
.switcher {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 20px 20px;
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
|
@ -72,6 +73,8 @@
|
|||
padding: 0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
font-weight: 500;
|
||||
color: var(--secondary40);
|
||||
margin-right: 20px;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { store } from "../builderStore"
|
||||
import { store, backendUiStore } from "../builderStore"
|
||||
import { map, join } from "lodash/fp"
|
||||
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 = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import InputGroup from "../common/Inputs/InputGroup.svelte"
|
||||
import LayoutTemplateControls from "./LayoutTemplateControls.svelte";
|
||||
import LayoutTemplateControls from "./LayoutTemplateControls.svelte"
|
||||
|
||||
export let onStyleChanged = () => {}
|
||||
export let component
|
||||
|
@ -59,14 +59,13 @@
|
|||
{#each Object.entries(display) as [key, [name, meta, size]] (component._id + key)}
|
||||
<div class="grid">
|
||||
<h5>{name}:</h5>
|
||||
<LayoutTemplateControls
|
||||
onStyleChanged={_value => onStyleChanged('layout', key, _value)}
|
||||
values={layout[key] || newValue(meta.length)}
|
||||
propertyName={name}
|
||||
{meta}
|
||||
{size}
|
||||
type="text"
|
||||
/>
|
||||
<LayoutTemplateControls
|
||||
onStyleChanged={_value => onStyleChanged('layout', key, _value)}
|
||||
values={layout[key] || newValue(meta.length)}
|
||||
propertyName={name}
|
||||
{meta}
|
||||
{size}
|
||||
type="text" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -133,11 +132,20 @@
|
|||
h3 {
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #000333;
|
||||
opacity: 0.6;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #000333;
|
||||
opacity: 0.4;
|
||||
margin-bottom: 10px;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
|
|
|
@ -115,9 +115,7 @@
|
|||
bind:value={layoutComponent}
|
||||
class:uk-form-danger={saveAttempted && !layoutComponent}>
|
||||
{#each layoutComponents as comp}
|
||||
<option value={comp}>
|
||||
{comp.componentName} - {comp.libName}
|
||||
</option>
|
||||
<option value={comp}>{comp.componentName} - {comp.libName}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
@ -127,35 +125,32 @@
|
|||
</ConfirmDialog>
|
||||
|
||||
<style>
|
||||
.uk-margin {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.uk-margin {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.uk-form-controls {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.uk-form-controls {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
.uk-form-label {
|
||||
padding-bottom: 10px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
color: var(--secondary80);
|
||||
}
|
||||
|
||||
.uk-form-label {
|
||||
padding-bottom: 10px;
|
||||
font-weight: 500;
|
||||
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-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;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
font-size: 0.8rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.active {
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<div class="pages-list-container">
|
||||
<div class="nav-header">
|
||||
<span class="navigator-title">Navigator</span>
|
||||
<div class="border-line" />
|
||||
<div class="border-line" />
|
||||
|
||||
<span class="components-nav-page">Pages</span>
|
||||
</div>
|
||||
|
@ -110,24 +110,23 @@
|
|||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr 300px;
|
||||
grid-template-columns: 275px 1fr 275px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
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 {
|
||||
grid-column: 1;
|
||||
|
@ -223,10 +222,10 @@
|
|||
font-size: 14px;
|
||||
color: var(--secondary100);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
padding: 0 20px 20px 20px;
|
||||
line-height: 1rem !important;
|
||||
letter-spacing: 1px;
|
||||
|
||||
}
|
||||
|
||||
.border-line {
|
||||
|
|
|
@ -8,6 +8,6 @@
|
|||
"nodeId": 0
|
||||
},
|
||||
"triggers": [],
|
||||
"actions": {},
|
||||
"actions": [],
|
||||
"props": {}
|
||||
}
|
||||
|
|
|
@ -15,9 +15,12 @@ export const bbFactory = ({
|
|||
}) => {
|
||||
const relativeUrl = url => {
|
||||
if (!frontendDefinition.appRootPath) return url
|
||||
if (url.startsWith("http:")
|
||||
|| url.startsWith("https:")
|
||||
|| url.startsWith("./")) return url
|
||||
if (
|
||||
url.startsWith("http:") ||
|
||||
url.startsWith("https:") ||
|
||||
url.startsWith("./")
|
||||
)
|
||||
return url
|
||||
|
||||
return frontendDefinition.appRootPath + "/" + trimSlash(url)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
reduce,
|
||||
find,
|
||||
} from "lodash/fp"
|
||||
import { compileExpression, compileCode } from "../common/compileCode"
|
||||
import { compileCode } from "../common/compileCode"
|
||||
import { $ } from "../common"
|
||||
import { _executeAction } from "./execute"
|
||||
import { BadRequestError, NotFoundError } from "../common/errors"
|
||||
|
@ -49,7 +49,7 @@ const subscribeTriggers = (
|
|||
|
||||
const shouldRunTrigger = (trigger, eventContext) => {
|
||||
if (!trigger.condition) return true
|
||||
const shouldRun = compileExpression(trigger.condition)
|
||||
const shouldRun = compileCode(trigger.condition)
|
||||
return shouldRun({ context: eventContext })
|
||||
}
|
||||
|
||||
|
|
|
@ -5,4 +5,4 @@ export const cloneApp = (app, mergeWith) => {
|
|||
Object.assign(newApp, mergeWith)
|
||||
setCleanupFunc(newApp)
|
||||
return newApp
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,25 +21,25 @@ export const initialiseData = async (
|
|||
applicationDefinition,
|
||||
accessLevels
|
||||
) => {
|
||||
if (!await datastore.exists(configFolder))
|
||||
if (!(await datastore.exists(configFolder)))
|
||||
await datastore.createFolder(configFolder)
|
||||
|
||||
if (!await datastore.exists(appDefinitionFile))
|
||||
if (!(await datastore.exists(appDefinitionFile)))
|
||||
await datastore.createJson(appDefinitionFile, applicationDefinition)
|
||||
|
||||
await initialiseRootCollections(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)
|
||||
|
||||
if (!await datastore.exists(AUTH_FOLDER))
|
||||
if (!(await datastore.exists(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, [])
|
||||
|
||||
if (!await datastore.exists(ACCESS_LEVELS_FILE))
|
||||
if (!(await datastore.exists(ACCESS_LEVELS_FILE)))
|
||||
await datastore.createJson(
|
||||
ACCESS_LEVELS_FILE,
|
||||
accessLevels ? accessLevels : { version: 0, levels: [] }
|
||||
|
|
|
@ -44,7 +44,7 @@ export const _createUser = async (app, user, password = null) => {
|
|||
|
||||
const userErrors = validateUser(app)([...users, user], user)
|
||||
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)
|
||||
|
|
|
@ -3,6 +3,8 @@ import { _deleteRecord } from "../recordApi/delete"
|
|||
import { getAllIdsIterator } from "../indexing/allIds"
|
||||
import { permission } from "../authApi/permissions"
|
||||
import { getCollectionDir } from "../recordApi/recordInfo"
|
||||
import { ensureCollectionIsInitialised } from "./initialise"
|
||||
import { getNodeForCollectionPath } from "../templateApi/hierarchy"
|
||||
|
||||
export const deleteCollection = (app, disableCleanup = false) => async key =>
|
||||
apiWrapper(
|
||||
|
@ -25,14 +27,19 @@ export const _deleteCollection = async (app, key, disableCleanup) => {
|
|||
key = safeKey(key)
|
||||
const collectionDir = getCollectionDir(app.hierarchy, key)
|
||||
await deleteRecords(app, key)
|
||||
await deleteCollectionFolder(app, collectionDir)
|
||||
await deleteCollectionFolder(app, key, collectionDir)
|
||||
if (!disableCleanup) {
|
||||
await app.cleanupTransactions()
|
||||
}
|
||||
}
|
||||
|
||||
const deleteCollectionFolder = async (app, dir) =>
|
||||
const deleteCollectionFolder = async (app, key, dir) => {
|
||||
await app.datastore.deleteFolder(dir)
|
||||
await ensureCollectionIsInitialised(
|
||||
app.datastore,
|
||||
getNodeForCollectionPath(app.hierarchy)(key),
|
||||
dir)
|
||||
}
|
||||
|
||||
const deleteRecords = async (app, key) => {
|
||||
const iterate = await getAllIdsIterator(app)(key)
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from "../templateApi/hierarchy"
|
||||
import { $, allTrue, joinKey } from "../common"
|
||||
|
||||
const ensureCollectionIsInitialised = async (datastore, node, dir) => {
|
||||
export const ensureCollectionIsInitialised = async (datastore, node, dir) => {
|
||||
if (!(await datastore.exists(dir))) {
|
||||
await datastore.createFolder(dir)
|
||||
await datastore.createFolder(joinKey(dir, node.nodeId))
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
import {
|
||||
compileExpression as cExp,
|
||||
compileCode as cCode,
|
||||
} from "@nx-js/compiler-util"
|
||||
import { compileCode as cCode } from "@nx-js/compiler-util"
|
||||
import { includes } from "lodash/fp"
|
||||
|
||||
export const compileCode = code => {
|
||||
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 {
|
||||
func = cCode(code)
|
||||
func = cCode(safeCode)
|
||||
} catch (e) {
|
||||
e.message = `Error compiling code : ${code} : ${e.message}`
|
||||
throw e
|
||||
|
@ -15,16 +24,3 @@ export const compileCode = code => {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { iterateIndex } from "../indexing/read"
|
||||
import {
|
||||
|
@ -147,7 +147,7 @@ const applyItemToAggregateResult = (indexNode, result, item) => {
|
|||
const thisGroupResult = result[aggGroup.name]
|
||||
|
||||
if (isNonEmptyString(aggGroup.condition)) {
|
||||
if (!compileExpression(aggGroup.condition)({ record: item })) {
|
||||
if (!compileCode(aggGroup.condition)({ record: item })) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@ const buildHeirarchalIndex = async (app, indexNode) => {
|
|||
)
|
||||
|
||||
let allIds = await allIdsIterator()
|
||||
while (allIds.done === false) {
|
||||
while (allIds.done === false) {
|
||||
await createTransactionsForIds(
|
||||
allIds.result.collectionKey,
|
||||
allIds.result.ids
|
||||
|
@ -140,5 +140,4 @@ const buildHeirarchalIndex = async (app, indexNode) => {
|
|||
const recordNodeApplies = indexNode => recordNode =>
|
||||
includes(recordNode.nodeId)(indexNode.allowedRecordNodeIds)
|
||||
|
||||
|
||||
export default buildIndex
|
||||
|
|
|
@ -58,10 +58,9 @@ export const folderStructureArray = recordNode => {
|
|||
|
||||
export const getAllIdsIterator = app => async collection_Key_or_NodeKey => {
|
||||
collection_Key_or_NodeKey = safeKey(collection_Key_or_NodeKey)
|
||||
const recordNode = getCollectionNodeByKeyOrNodeKey(
|
||||
app.hierarchy,
|
||||
collection_Key_or_NodeKey
|
||||
) || getNodeByKeyOrNodeKey(app.hierarchy, collection_Key_or_NodeKey)
|
||||
const recordNode =
|
||||
getCollectionNodeByKeyOrNodeKey(app.hierarchy, collection_Key_or_NodeKey) ||
|
||||
getNodeByKeyOrNodeKey(app.hierarchy, collection_Key_or_NodeKey)
|
||||
|
||||
const getAllIdsIteratorForCollectionKey = async (
|
||||
recordNode,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { compileExpression, compileCode } from "@nx-js/compiler-util"
|
||||
import { isUndefined, keys, cloneDeep, isFunction } from "lodash/fp"
|
||||
import { compileCode } from "../common/compileCode"
|
||||
import { isUndefined, keys, cloneDeep, isFunction, includes } from "lodash/fp"
|
||||
import { defineError } from "../common"
|
||||
|
||||
export const filterEval = "FILTER_EVALUATE"
|
||||
|
@ -16,7 +16,7 @@ const getEvaluateResult = () => ({
|
|||
result: null,
|
||||
})
|
||||
|
||||
export const compileFilter = index => compileExpression(index.filter)
|
||||
export const compileFilter = index => compileCode(index.filter)
|
||||
|
||||
export const compileMap = index => compileCode(index.map)
|
||||
|
||||
|
@ -46,6 +46,9 @@ export const mapRecord = (record, index) => {
|
|||
if (isFunction(mapped[key])) {
|
||||
delete mapped[key]
|
||||
}
|
||||
if (key === "IsNew") {
|
||||
delete mapped.IsNew
|
||||
}
|
||||
}
|
||||
|
||||
mapped.key = record.key
|
||||
|
|
|
@ -44,7 +44,7 @@ export const generateSchema = (hierarchy, indexNode) => {
|
|||
keys,
|
||||
map(k => ({ name: k, type: schema[k].name })),
|
||||
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
|
||||
reverse, // sortKey first, then rest are alphabetical
|
||||
])
|
||||
|
|
|
@ -10,18 +10,18 @@ export const initialiseIndex = async (datastore, dir, index) => {
|
|||
const indexDir = joinKey(dir, index.name)
|
||||
|
||||
let newDir = false
|
||||
if (!await datastore.exists(indexDir)) {
|
||||
if (!(await datastore.exists(indexDir))) {
|
||||
await datastore.createFolder(indexDir)
|
||||
newDir = true
|
||||
}
|
||||
|
||||
if (isShardedIndex(index)) {
|
||||
const shardFile = getShardMapKey(indexDir)
|
||||
if (newDir || !await datastore.exists(shardFile))
|
||||
if (newDir || !(await datastore.exists(shardFile)))
|
||||
await datastore.createFile(shardFile, "[]")
|
||||
} else {
|
||||
const indexFile = getUnshardedIndexDataKey(indexDir)
|
||||
if (newDir || !await datastore.exists(indexFile))
|
||||
if (newDir || !(await datastore.exists(indexFile)))
|
||||
await createIndexFile(datastore, indexFile, index)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,10 @@ import { promiseReadableStream } from "./promiseReadableStream"
|
|||
import { createIndexFile } from "./sharding"
|
||||
import { generateSchema } from "./indexSchemaCreator"
|
||||
import { getIndexReader, CONTINUE_READING_RECORDS } from "./serializer"
|
||||
import { getAllowedRecordNodesForIndex, getRecordNodeId } from "../templateApi/hierarchy"
|
||||
import {
|
||||
getAllowedRecordNodesForIndex,
|
||||
getRecordNodeId,
|
||||
} from "../templateApi/hierarchy"
|
||||
import { $ } from "../common"
|
||||
import { filter, includes, find } from "lodash/fp"
|
||||
|
||||
|
|
|
@ -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 {
|
||||
getActualKeyOfParent,
|
||||
|
|
|
@ -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) =>
|
||||
constructRecord(recordNode, getNewFieldValue, collectionKey)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { isString, flatten, map, filter } from "lodash/fp"
|
||||
import { initialiseChildCollections } from "../collectionApi/initialise"
|
||||
import { _loadFromInfo } from "./load"
|
||||
import { $ } from "../common"
|
||||
import { $, joinKey } from "../common"
|
||||
import {
|
||||
getFlattenedHierarchy,
|
||||
isRecord,
|
||||
|
@ -11,31 +11,31 @@ import {
|
|||
} from "../templateApi/hierarchy"
|
||||
import { initialiseIndex } from "../indexing/initialiseIndex"
|
||||
import { getRecordInfo } from "./recordInfo"
|
||||
import { getAllIdsIterator } from "../indexing/allIds"
|
||||
|
||||
export const initialiseChildren = async (app, recordInfoOrKey) => {
|
||||
const recordInfo = isString(recordInfoOrKey)
|
||||
? getRecordInfo(app.hierarchy, recordInfoOrKey)
|
||||
: recordInfoOrKey
|
||||
const recordInfo = isString(recordInfoOrKey)
|
||||
? getRecordInfo(app.hierarchy, recordInfoOrKey)
|
||||
: recordInfoOrKey
|
||||
await initialiseReverseReferenceIndexes(app, recordInfo)
|
||||
await initialiseAncestorIndexes(app, recordInfo)
|
||||
await initialiseChildCollections(app, recordInfo)
|
||||
}
|
||||
|
||||
export const initialiseChildrenForNode = async (app, recordNode) => {
|
||||
|
||||
if (isTopLevelRecord(recordNode)) {
|
||||
await initialiseChildren(
|
||||
app, recordNode.nodeKey())
|
||||
await initialiseChildren(app, recordNode.nodeKey())
|
||||
return
|
||||
}
|
||||
|
||||
const iterate = await getAllIdsIterator(app)(recordNode.parent().nodeKey())
|
||||
const iterate = await getAllIdsIterator(app)(
|
||||
recordNode.parent().collectionNodeKey()
|
||||
)
|
||||
let iterateResult = await iterate()
|
||||
while (!iterateResult.done) {
|
||||
const { result } = iterateResult
|
||||
for (const id of result.ids) {
|
||||
const initialisingRecordKey = joinKey(
|
||||
result.collectionKey, id)
|
||||
const initialisingRecordKey = joinKey(result.collectionKey, id)
|
||||
await initialiseChildren(app, initialisingRecordKey)
|
||||
}
|
||||
iterateResult = await iterate()
|
||||
|
@ -77,5 +77,3 @@ const fieldsThatReferenceThisRecord = (app, recordNode) =>
|
|||
flatten,
|
||||
filter(fieldReversesReferenceToNode(recordNode)),
|
||||
])
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { getExactNodeForKey } from "../templateApi/hierarchy"
|
||||
import { validateFieldParse, validateTypeConstraints } from "../types"
|
||||
|
@ -35,7 +35,7 @@ const validateAllTypeConstraints = async (record, recordNode, context) => {
|
|||
|
||||
const runRecordValidationRules = (record, recordNode) => {
|
||||
const runValidationRule = rule => {
|
||||
const isValid = compileExpression(rule.expressionWhenValid)
|
||||
const isValid = compileCode(rule.expressionWhenValid)
|
||||
const expressionContext = { record, _ }
|
||||
return isValid(expressionContext)
|
||||
? { valid: true }
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -160,16 +160,17 @@ export const getNewRootLevel = () =>
|
|||
})
|
||||
|
||||
const _getNewRecordTemplate = (parent, name, createDefaultIndex, isSingle) => {
|
||||
const nodeId = getNodeId(parent)
|
||||
const node = constructNode(parent, {
|
||||
name,
|
||||
type: "record",
|
||||
fields: [],
|
||||
children: [],
|
||||
validationRules: [],
|
||||
nodeId: getNodeId(parent),
|
||||
nodeId: nodeId,
|
||||
indexes: [],
|
||||
estimatedRecordCount: isRecord(parent) ? 500 : 1000000,
|
||||
collectionName: "",
|
||||
collectionName: (nodeId || "").toString(),
|
||||
isSingle,
|
||||
})
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import { isTopLevelIndex, getParentKey, getLastPartInKey } from "./hierarchy"
|
|||
import { safeKey, joinKey } from "../common"
|
||||
|
||||
export const deleteAllIndexFilesForNode = async (app, indexNode) => {
|
||||
|
||||
if (isTopLevelIndex(indexNode)) {
|
||||
await app.datastore.deleteFolder(indexNode.nodeKey())
|
||||
return
|
||||
|
@ -15,13 +14,11 @@ export const deleteAllIndexFilesForNode = async (app, indexNode) => {
|
|||
while (!iterateResult.done) {
|
||||
const { result } = iterateResult
|
||||
for (const id of result.ids) {
|
||||
const deletingIndexKey = joinKey(
|
||||
result.collectionKey, id, indexNode.name)
|
||||
const deletingIndexKey = joinKey(result.collectionKey, id, indexNode.name)
|
||||
await deleteIndexFolder(app, deletingIndexKey)
|
||||
}
|
||||
iterateResult = await iterate()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const deleteIndexFolder = async (app, indexKey) => {
|
||||
|
@ -29,6 +26,5 @@ const deleteIndexFolder = async (app, indexKey) => {
|
|||
const indexName = getLastPartInKey(indexKey)
|
||||
const parentRecordKey = getParentKey(indexKey)
|
||||
const recordInfo = getRecordInfo(app.hierarchy, parentRecordKey)
|
||||
await app.datastore.deleteFolder(
|
||||
joinKey(recordInfo.dir, indexName))
|
||||
}
|
||||
await app.datastore.deleteFolder(joinKey(recordInfo.dir, indexName))
|
||||
}
|
||||
|
|
|
@ -4,10 +4,8 @@ import { isTopLevelRecord, getCollectionKey } from "./hierarchy"
|
|||
import { safeKey, joinKey } from "../common"
|
||||
|
||||
export const deleteAllRecordsForNode = async (app, recordNode) => {
|
||||
|
||||
if (isTopLevelRecord(recordNode)) {
|
||||
await deleteRecordCollection(
|
||||
app, recordNode.collectionName)
|
||||
await deleteRecordCollection(app, recordNode.collectionName)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -17,16 +15,19 @@ export const deleteAllRecordsForNode = async (app, recordNode) => {
|
|||
const { result } = iterateResult
|
||||
for (const id of result.ids) {
|
||||
const deletingCollectionKey = joinKey(
|
||||
result.collectionKey, id, recordNode.collectionName)
|
||||
result.collectionKey,
|
||||
id,
|
||||
recordNode.collectionName
|
||||
)
|
||||
await deleteRecordCollection(app, deletingCollectionKey)
|
||||
}
|
||||
iterateResult = await iterate()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const deleteRecordCollection = async (app, collectionKey) => {
|
||||
collectionKey = safeKey(collectionKey)
|
||||
await app.datastore.deleteFolder(
|
||||
getCollectionDir(app.hierarchy, collectionKey))
|
||||
}
|
||||
getCollectionDir(app.hierarchy, collectionKey)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { getFlattenedHierarchy, isRecord, isIndex, isAncestor } from "./hierarchy"
|
||||
import {
|
||||
getFlattenedHierarchy,
|
||||
isRecord,
|
||||
isIndex,
|
||||
isAncestor,
|
||||
} from "./hierarchy"
|
||||
import { $, none } from "../common"
|
||||
import { map, filter, some, find, difference } from "lodash/fp"
|
||||
|
||||
|
@ -19,13 +24,16 @@ export const diffHierarchy = (oldHierarchy, newHierarchy) => {
|
|||
|
||||
const createdRecords = findCreatedRecords(oldHierarchyFlat, newHierarchyFlat)
|
||||
const deletedRecords = findDeletedRecords(oldHierarchyFlat, newHierarchyFlat)
|
||||
|
||||
|
||||
return [
|
||||
...createdRecords,
|
||||
...deletedRecords,
|
||||
...findRenamedRecords(oldHierarchyFlat, newHierarchyFlat),
|
||||
...findRecordsWithFieldsChanged(oldHierarchyFlat, newHierarchyFlat),
|
||||
...findRecordsWithEstimatedRecordTypeChanged(oldHierarchyFlat, newHierarchyFlat),
|
||||
...findRecordsWithEstimatedRecordTypeChanged(
|
||||
oldHierarchyFlat,
|
||||
newHierarchyFlat
|
||||
),
|
||||
...findCreatedIndexes(oldHierarchyFlat, newHierarchyFlat, createdRecords),
|
||||
...findDeletedIndexes(oldHierarchyFlat, newHierarchyFlat, deletedRecords),
|
||||
...findUpdatedIndexes(oldHierarchyFlat, newHierarchyFlat),
|
||||
|
@ -33,18 +41,20 @@ export const diffHierarchy = (oldHierarchy, newHierarchy) => {
|
|||
}
|
||||
|
||||
const changeItem = (type, oldNode, newNode) => ({
|
||||
type, oldNode, newNode,
|
||||
type,
|
||||
oldNode,
|
||||
newNode,
|
||||
})
|
||||
|
||||
const findCreatedRecords = (oldHierarchyFlat, newHierarchyFlat) => {
|
||||
const allCreated = $(newHierarchyFlat, [
|
||||
filter(isRecord),
|
||||
filter(nodeDoesNotExistIn(oldHierarchyFlat)),
|
||||
map(n => changeItem(HierarchyChangeTypes.recordCreated, null, n))
|
||||
map(n => changeItem(HierarchyChangeTypes.recordCreated, null, n)),
|
||||
])
|
||||
|
||||
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, [
|
||||
filter(isRecord),
|
||||
filter(nodeDoesNotExistIn(newHierarchyFlat)),
|
||||
map(n => changeItem(HierarchyChangeTypes.recordDeleted, n, null))
|
||||
map(n => changeItem(HierarchyChangeTypes.recordDeleted, n, null)),
|
||||
])
|
||||
|
||||
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, [
|
||||
filter(isRecord),
|
||||
filter(nodeExistsIn(newHierarchyFlat)),
|
||||
filter(nodeChanged(newHierarchyFlat, (_new,old) =>_new.collectionKey !== old.collectionKey )),
|
||||
map(n => changeItem(
|
||||
HierarchyChangeTypes.recordRenamed,
|
||||
n,
|
||||
findNodeIn(n, newHierarchyFlat))
|
||||
)
|
||||
filter(
|
||||
nodeChanged(
|
||||
newHierarchyFlat,
|
||||
(_new, old) => _new.collectionKey !== old.collectionKey
|
||||
)
|
||||
),
|
||||
map(n =>
|
||||
changeItem(
|
||||
HierarchyChangeTypes.recordRenamed,
|
||||
n,
|
||||
findNodeIn(n, newHierarchyFlat)
|
||||
)
|
||||
),
|
||||
])
|
||||
|
||||
const findRecordsWithFieldsChanged = (oldHierarchyFlat, newHierarchyFlat) =>
|
||||
const findRecordsWithFieldsChanged = (oldHierarchyFlat, newHierarchyFlat) =>
|
||||
$(oldHierarchyFlat, [
|
||||
filter(isRecord),
|
||||
filter(nodeExistsIn(newHierarchyFlat)),
|
||||
filter(hasDifferentFields(newHierarchyFlat)),
|
||||
map(n => changeItem(
|
||||
HierarchyChangeTypes.recordFieldsChanged,
|
||||
n,
|
||||
findNodeIn(n, newHierarchyFlat))
|
||||
)
|
||||
map(n =>
|
||||
changeItem(
|
||||
HierarchyChangeTypes.recordFieldsChanged,
|
||||
n,
|
||||
findNodeIn(n, newHierarchyFlat)
|
||||
)
|
||||
),
|
||||
])
|
||||
|
||||
const findRecordsWithEstimatedRecordTypeChanged = (oldHierarchyFlat, newHierarchyFlat) =>
|
||||
const findRecordsWithEstimatedRecordTypeChanged = (
|
||||
oldHierarchyFlat,
|
||||
newHierarchyFlat
|
||||
) =>
|
||||
$(oldHierarchyFlat, [
|
||||
filter(isRecord),
|
||||
filter(nodeExistsIn(newHierarchyFlat)),
|
||||
filter(nodeChanged(newHierarchyFlat, (_new,old) =>_new.estimatedRecordCount !== old.estimatedRecordCount)),
|
||||
map(n => changeItem(
|
||||
HierarchyChangeTypes.recordEstimatedRecordTypeChanged,
|
||||
n,
|
||||
findNodeIn(n, newHierarchyFlat))
|
||||
)
|
||||
filter(
|
||||
nodeChanged(
|
||||
newHierarchyFlat,
|
||||
(_new, old) => _new.estimatedRecordCount !== old.estimatedRecordCount
|
||||
)
|
||||
),
|
||||
map(n =>
|
||||
changeItem(
|
||||
HierarchyChangeTypes.recordEstimatedRecordTypeChanged,
|
||||
n,
|
||||
findNodeIn(n, newHierarchyFlat)
|
||||
)
|
||||
),
|
||||
])
|
||||
|
||||
const findCreatedIndexes = (oldHierarchyFlat, newHierarchyFlat, createdRecords) => {
|
||||
const findCreatedIndexes = (
|
||||
oldHierarchyFlat,
|
||||
newHierarchyFlat,
|
||||
createdRecords
|
||||
) => {
|
||||
const allCreated = $(newHierarchyFlat, [
|
||||
filter(isIndex),
|
||||
filter(nodeDoesNotExistIn(oldHierarchyFlat)),
|
||||
map(n => changeItem(HierarchyChangeTypes.indexCreated, null, n))
|
||||
map(n => changeItem(HierarchyChangeTypes.indexCreated, null, n)),
|
||||
])
|
||||
|
||||
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, [
|
||||
filter(isIndex),
|
||||
filter(nodeDoesNotExistIn(newHierarchyFlat)),
|
||||
map(n => changeItem(HierarchyChangeTypes.indexDeleted, n, null))
|
||||
map(n => changeItem(HierarchyChangeTypes.indexDeleted, n, null)),
|
||||
])
|
||||
|
||||
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, [
|
||||
filter(isIndex),
|
||||
filter(nodeExistsIn(newHierarchyFlat)),
|
||||
filter(nodeChanged(newHierarchyFlat, indexHasChanged)),
|
||||
map(n => changeItem(
|
||||
HierarchyChangeTypes.indexChanged,
|
||||
n,
|
||||
findNodeIn(n, newHierarchyFlat))
|
||||
)
|
||||
map(n =>
|
||||
changeItem(
|
||||
HierarchyChangeTypes.indexChanged,
|
||||
n,
|
||||
findNodeIn(n, newHierarchyFlat)
|
||||
)
|
||||
),
|
||||
])
|
||||
|
||||
const hasDifferentFields = otherFlatHierarchy => record1 => {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const indexHasChanged = (_new, old) =>
|
||||
_new.map !== old.map
|
||||
|| _new.filter !== old.filter
|
||||
|| _new.getShardName !== old.getShardName
|
||||
|| difference(_new.allowedRecordNodeIds)(old.allowedRecordNodeIds).length > 0
|
||||
const indexHasChanged = (_new, old) =>
|
||||
_new.map !== old.map ||
|
||||
_new.filter !== old.filter ||
|
||||
_new.getShardName !== old.getShardName ||
|
||||
difference(_new.allowedRecordNodeIds)(old.allowedRecordNodeIds).length > 0
|
||||
|
||||
const isFieldSame = f1 => f2 =>
|
||||
f1.name === f2.name && f1.type === f2.type
|
||||
const isFieldSame = f1 => f2 => f1.name === f2.name && f1.type === f2.type
|
||||
|
||||
const nodeDoesNotExistIn = inThis => node =>
|
||||
none(n => n.nodeId === node.nodeId)(inThis)
|
||||
const nodeDoesNotExistIn = inThis => node =>
|
||||
none(n => n.nodeId === node.nodeId)(inThis)
|
||||
|
||||
const nodeExistsIn = inThis => node =>
|
||||
const nodeExistsIn = inThis => node =>
|
||||
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)
|
||||
|
||||
const findNodeIn = (node, inThis) =>
|
||||
find(n => n.nodeId === node.nodeId)(inThis)
|
||||
const findNodeIn = (node, inThis) => find(n => n.nodeId === node.nodeId)(inThis)
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { all, getDefaultOptions } from "../types"
|
||||
import { applyRuleSet, makerule } from "../common/validationCommon"
|
||||
import { BadRequestError } from "../common/errors"
|
||||
import { generate } from "shortid"
|
||||
|
||||
export const fieldErrors = {
|
||||
AddFieldValidationFailed: "Add field validation: ",
|
||||
|
@ -17,6 +18,7 @@ export const fieldErrors = {
|
|||
export const allowedTypes = () => keys(all)
|
||||
|
||||
export const getNewField = type => ({
|
||||
id: generate(),
|
||||
name: "", // how field is referenced internally
|
||||
type,
|
||||
typeOptions: getDefaultOptions(type),
|
||||
|
|
|
@ -192,8 +192,8 @@ export const getAllowedRecordNodesForIndex = (appHierarchy, indexNode) => {
|
|||
}
|
||||
|
||||
export const getDependantIndexes = (hierarchy, recordNode) => {
|
||||
const allIndexes = $(hierarchy, [ getFlattenedHierarchy, filter(isIndex)])
|
||||
|
||||
const allIndexes = $(hierarchy, [getFlattenedHierarchy, filter(isIndex)])
|
||||
|
||||
const allowedAncestors = $(allIndexes, [
|
||||
filter(isAncestorIndex),
|
||||
filter(i => recordNodeIsAllowed(i)(recordNode)),
|
||||
|
@ -201,7 +201,7 @@ export const getDependantIndexes = (hierarchy, recordNode) => {
|
|||
|
||||
const allowedReference = $(allIndexes, [
|
||||
filter(isReferenceIndex),
|
||||
filter(i => some(fieldReversesReferenceToIndex(i))(recordNode.fields))
|
||||
filter(i => some(fieldReversesReferenceToIndex(i))(recordNode.fields)),
|
||||
])
|
||||
|
||||
return [...allowedAncestors, ...allowedReference]
|
||||
|
@ -222,7 +222,7 @@ export const isaggregateGroup = node =>
|
|||
export const isShardedIndex = node =>
|
||||
isIndex(node) && isNonEmptyString(node.getShardName)
|
||||
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 isGlobalIndex = node => isIndex(node) && isRoot(node.parent())
|
||||
export const isReferenceIndex = node =>
|
||||
|
@ -231,10 +231,8 @@ export const isAncestorIndex = node =>
|
|||
isIndex(node) && node.indexType === indexTypes.ancestor
|
||||
export const isTopLevelRecord = node => isRoot(node.parent()) && isRecord(node)
|
||||
export const isTopLevelIndex = node => isRoot(node.parent()) && isIndex(node)
|
||||
export const getCollectionKey = recordKey => $(recordKey, [
|
||||
splitKey,
|
||||
parts => joinKey(parts.slice(0, parts.length - 1))
|
||||
])
|
||||
export const getCollectionKey = recordKey =>
|
||||
$(recordKey, [splitKey, parts => joinKey(parts.slice(0, parts.length - 1))])
|
||||
export const fieldReversesReferenceToNode = node => field =>
|
||||
field.type === "reference" &&
|
||||
intersection(field.typeOptions.reverseIndexNodeKeys)(
|
||||
|
|
|
@ -58,7 +58,7 @@ const api = app => ({
|
|||
validateNode,
|
||||
validateAll,
|
||||
validateTriggers,
|
||||
upgradeData: upgradeData(app)
|
||||
upgradeData: upgradeData(app),
|
||||
})
|
||||
|
||||
export const getTemplateApi = app => api(app)
|
||||
|
|
|
@ -5,7 +5,6 @@ import { joinKey } from "../common"
|
|||
import { initialiseIndex } from "../indexing/initialiseIndex"
|
||||
|
||||
export const initialiseNewIndex = async (app, indexNode) => {
|
||||
|
||||
if (isTopLevelIndex(indexNode)) {
|
||||
await initialiseIndex(app.datastore, "/", indexNode)
|
||||
return
|
||||
|
@ -18,10 +17,11 @@ export const initialiseNewIndex = async (app, indexNode) => {
|
|||
for (const id of result.ids) {
|
||||
const recordKey = joinKey(result.collectionKey, id)
|
||||
await initialiseIndex(
|
||||
app.datastore,
|
||||
app.datastore,
|
||||
getRecordInfo(app.hierarchy, recordKey).dir,
|
||||
indexNode)
|
||||
indexNode
|
||||
)
|
||||
}
|
||||
iterateResult = await iterate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { diffHierarchy, HierarchyChangeTypes } from "./diffHierarchy"
|
||||
import { $, switchCase } from "../common"
|
||||
import {
|
||||
differenceBy,
|
||||
isEqual,
|
||||
some,
|
||||
map,
|
||||
import {
|
||||
differenceBy,
|
||||
isEqual,
|
||||
some,
|
||||
map,
|
||||
filter,
|
||||
uniqBy,
|
||||
flatten
|
||||
flatten,
|
||||
} from "lodash/fp"
|
||||
import {
|
||||
findRoot,
|
||||
getDependantIndexes,
|
||||
isTopLevelRecord,
|
||||
isAncestorIndex
|
||||
import {
|
||||
findRoot,
|
||||
getDependantIndexes,
|
||||
isTopLevelRecord,
|
||||
isAncestorIndex,
|
||||
} from "./hierarchy"
|
||||
import { generateSchema } from "../indexing/indexSchemaCreator"
|
||||
import { _buildIndex } from "../indexApi/buildIndex"
|
||||
|
@ -24,130 +24,142 @@ import { cloneApp } from "../appInitialise/cloneApp"
|
|||
import { initialiseData } from "../appInitialise/initialiseData"
|
||||
import { initialiseChildrenForNode } from "../recordApi/initialiseChildren"
|
||||
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 => {
|
||||
const currentAppDef = await getApplicationDefinition(app.datastore)()
|
||||
app.hierarchy = currentAppDef.hierarchy
|
||||
newHierarchy = constructHierarchy(newHierarchy)
|
||||
const diff = diffHierarchy(app.hierarchy, newHierarchy)
|
||||
const changeActions = gatherChangeActions(diff)
|
||||
|
||||
if (changeActions.length === 0) return
|
||||
|
||||
newHierarchy = constructHierarchy(newHierarchy)
|
||||
const newApp = newHierarchy && cloneApp(app, {
|
||||
hierarchy: newHierarchy
|
||||
})
|
||||
const newApp =
|
||||
newHierarchy &&
|
||||
cloneApp(app, {
|
||||
hierarchy: newHierarchy,
|
||||
})
|
||||
await doUpgrade(app, newApp, changeActions)
|
||||
await saveApplicationHierarchy(newApp)(newHierarchy)
|
||||
await _saveApplicationHierarchy(newApp.datastore, newHierarchy)
|
||||
}
|
||||
|
||||
const gatherChangeActions = (diff) =>
|
||||
$(diff, [
|
||||
map(actionForChange),
|
||||
flatten,
|
||||
uniqBy(a => a.compareKey)
|
||||
])
|
||||
const gatherChangeActions = diff =>
|
||||
$(diff, [map(actionForChange), flatten, uniqBy(a => a.compareKey)])
|
||||
|
||||
const doUpgrade = async (oldApp, newApp, changeActions) => {
|
||||
for(let action of changeActions) {
|
||||
for (let action of changeActions) {
|
||||
await action.run(oldApp, newApp, action.diff)
|
||||
}
|
||||
}
|
||||
|
||||
const actionForChange = diff =>
|
||||
const actionForChange = diff =>
|
||||
switchCase(
|
||||
|
||||
[isChangeType(HierarchyChangeTypes.recordCreated), recordCreatedAction],
|
||||
|
||||
[isChangeType(HierarchyChangeTypes.recordDeleted), deleteRecordsAction],
|
||||
|
||||
[
|
||||
isChangeType(HierarchyChangeTypes.recordFieldsChanged),
|
||||
rebuildAffectedIndexesAction
|
||||
isChangeType(HierarchyChangeTypes.recordFieldsChanged),
|
||||
rebuildAffectedIndexesAction,
|
||||
],
|
||||
|
||||
[isChangeType(HierarchyChangeTypes.recordRenamed), renameRecordAction],
|
||||
|
||||
[
|
||||
isChangeType(HierarchyChangeTypes.recordEstimatedRecordTypeChanged),
|
||||
reshardRecordsAction
|
||||
isChangeType(HierarchyChangeTypes.recordEstimatedRecordTypeChanged),
|
||||
reshardRecordsAction,
|
||||
],
|
||||
|
||||
[isChangeType(HierarchyChangeTypes.indexCreated), newIndexAction],
|
||||
|
||||
[isChangeType(HierarchyChangeTypes.indexDeleted), deleteIndexAction],
|
||||
|
||||
[isChangeType(HierarchyChangeTypes.indexChanged), rebuildIndexAction],
|
||||
|
||||
[isChangeType(HierarchyChangeTypes.indexChanged), rebuildIndexAction]
|
||||
)(diff)
|
||||
|
||||
|
||||
const isChangeType = changeType => change =>
|
||||
change.type === changeType
|
||||
const isChangeType = changeType => change => change.type === changeType
|
||||
|
||||
const action = (diff, compareKey, run) => ({
|
||||
diff,
|
||||
compareKey,
|
||||
compareKey,
|
||||
run,
|
||||
})
|
||||
|
||||
|
||||
const reshardRecordsAction = diff =>
|
||||
[action(diff, `reshardRecords-${diff.oldNode.nodeKey()}`, runReshardRecords)]
|
||||
const reshardRecordsAction = diff => [
|
||||
action(diff, `reshardRecords-${diff.oldNode.nodeKey()}`, runReshardRecords),
|
||||
]
|
||||
|
||||
const rebuildIndexAction = diff =>
|
||||
[action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex)]
|
||||
const rebuildIndexAction = diff => [
|
||||
action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex),
|
||||
]
|
||||
|
||||
const newIndexAction = diff => {
|
||||
if (isAncestorIndex(diff.newNode)) {
|
||||
return [action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex)]
|
||||
return [
|
||||
action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex),
|
||||
]
|
||||
} else {
|
||||
return [action(diff, `newIndex-${diff.newNode.nodeKey()}`, runNewIndex)]
|
||||
}
|
||||
}
|
||||
|
||||
const deleteIndexAction = diff =>
|
||||
[action(diff, `deleteIndex-${diff.oldNode.nodeKey()}`, runDeleteIndex)]
|
||||
const deleteIndexAction = diff => [
|
||||
action(diff, `deleteIndex-${diff.oldNode.nodeKey()}`, runDeleteIndex),
|
||||
]
|
||||
|
||||
const deleteRecordsAction = diff =>
|
||||
[action(diff, `deleteRecords-${diff.oldNode.nodeKey()}`, runDeleteRecords)]
|
||||
const deleteRecordsAction = diff => [
|
||||
action(diff, `deleteRecords-${diff.oldNode.nodeKey()}`, runDeleteRecords),
|
||||
]
|
||||
|
||||
const renameRecordAction = diff =>
|
||||
[action(diff, `renameRecords-${diff.oldNode.nodeKey()}`, runRenameRecord)]
|
||||
const renameRecordAction = diff => [
|
||||
action(diff, `renameRecords-${diff.oldNode.nodeKey()}`, runRenameRecord),
|
||||
]
|
||||
|
||||
const recordCreatedAction = diff => {
|
||||
if (isTopLevelRecord(diff.newNode)) {
|
||||
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 oldHierarchy = findRoot(diff.oldNode)
|
||||
const indexes = getDependantIndexes(newHierarchy, diff.newNode)
|
||||
|
||||
const changedFields = (() => {
|
||||
const addedFields = differenceBy(f => f.name)
|
||||
(diff.oldNode.fields)
|
||||
(diff.newNode.fields)
|
||||
const addedFields = differenceBy(f => f.name)(diff.oldNode.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])
|
||||
})()
|
||||
|
||||
const isIndexAffected = i => {
|
||||
if (!isEqual(
|
||||
generateSchema(oldHierarchy, i),
|
||||
generateSchema(newHierarchy, i))) return true
|
||||
|
||||
if (
|
||||
!isEqual(generateSchema(oldHierarchy, i), generateSchema(newHierarchy, i))
|
||||
)
|
||||
return true
|
||||
|
||||
if (some(f => indexes.filter.indexOf(`record.${f}`) > -1)(changedFields))
|
||||
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 false
|
||||
|
@ -155,10 +167,12 @@ const rebuildAffectedIndexesAction = diff =>{
|
|||
|
||||
return $(indexes, [
|
||||
filter(isIndexAffected),
|
||||
map(i => action({ newNode:i }, `rebuildIndex-${i.nodeKey()}`, runRebuildIndex))
|
||||
map(i =>
|
||||
action({ newNode: i }, `rebuildIndex-${i.nodeKey()}`, runRebuildIndex)
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
const runReshardRecords = async change => {
|
||||
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())
|
||||
}
|
||||
|
||||
const runDeleteIndex = async (oldApp, _, diff) => {
|
||||
const runDeleteIndex = async (oldApp, _, diff) => {
|
||||
await deleteAllIndexFilesForNode(oldApp, diff.oldNode)
|
||||
}
|
||||
|
||||
|
@ -190,5 +204,5 @@ const runInitialiseRoot = async (_, newApp) => {
|
|||
}
|
||||
|
||||
const runInitialiseChildRecord = async (_, newApp, diff) => {
|
||||
await initialiseChildrenForNode(newApp.datastore, diff.newNode)
|
||||
}
|
||||
await initialiseChildrenForNode(newApp, diff.newNode)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
isEmpty,
|
||||
has,
|
||||
} from "lodash/fp"
|
||||
import { compileExpression, compileCode } from "@nx-js/compiler-util"
|
||||
import { compileCode } from "../common/compileCode"
|
||||
import {
|
||||
$,
|
||||
isSomething,
|
||||
|
@ -73,7 +73,7 @@ const aggregateGroupRules = [
|
|||
"condition does not compile",
|
||||
a =>
|
||||
isEmpty(a.condition) ||
|
||||
executesWithoutException(() => compileExpression(a.condition))
|
||||
executesWithoutException(() => compileCode(a.condition))
|
||||
),
|
||||
]
|
||||
|
||||
|
@ -196,7 +196,7 @@ const triggerRules = actions => [
|
|||
t => {
|
||||
if (!t.condition) return true
|
||||
try {
|
||||
compileExpression(t.condition)
|
||||
compileCode(t.condition)
|
||||
return true
|
||||
} catch (_) {
|
||||
return false
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { applyRuleSet, makerule } from "../common/validationCommon"
|
||||
|
||||
|
|
|
@ -50,7 +50,6 @@ export const cleanup = async app => {
|
|||
} finally {
|
||||
await releaseLock(app, lock)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const getTransactionLock = async app =>
|
||||
|
|
|
@ -40,7 +40,7 @@ import {
|
|||
fieldReversesReferenceToIndex,
|
||||
isReferenceIndex,
|
||||
getExactNodeForKey,
|
||||
getParentKey
|
||||
getParentKey,
|
||||
} from "../templateApi/hierarchy"
|
||||
import { getRecordInfo } from "../recordApi/recordInfo"
|
||||
import { getIndexDir } from "../indexApi/getIndexDir"
|
||||
|
@ -52,7 +52,7 @@ export const executeTransactions = app => async transactions => {
|
|||
for (const shard of keys(recordsByShard)) {
|
||||
if (recordsByShard[shard].isRebuild)
|
||||
await initialiseIndex(
|
||||
app.datastore,
|
||||
app.datastore,
|
||||
getParentKey(recordsByShard[shard].indexDir),
|
||||
recordsByShard[shard].indexNode
|
||||
)
|
||||
|
@ -87,8 +87,9 @@ const mappedRecordsByIndexShard = (hierarchy, transactions) => {
|
|||
transByShard[t.indexShardKey] = {
|
||||
writes: [],
|
||||
removes: [],
|
||||
isRebuild: some(i => i.indexShardKey === t.indexShardKey)(indexBuild.toWrite)
|
||||
|| some(i => i.indexShardKey === t.indexShardKey)(indexBuild.toRemove),
|
||||
isRebuild:
|
||||
some(i => i.indexShardKey === t.indexShardKey)(indexBuild.toWrite) ||
|
||||
some(i => i.indexShardKey === t.indexShardKey)(indexBuild.toRemove),
|
||||
indexDir: t.indexDir,
|
||||
indexNodeKey: t.indexNode.nodeKey(),
|
||||
indexNode: t.indexNode,
|
||||
|
@ -219,7 +220,7 @@ const getUpdateTransactionsByShard = (hierarchy, transactions) => {
|
|||
|
||||
const getBuildIndexTransactionsByShard = (hierarchy, transactions) => {
|
||||
const buildTransactions = $(transactions, [filter(isBuildIndex)])
|
||||
if (!isNonEmptyArray(buildTransactions)) return { toWrite:[], toRemove:[] }
|
||||
if (!isNonEmptyArray(buildTransactions)) return { toWrite: [], toRemove: [] }
|
||||
const indexNode = transactions.indexNode
|
||||
|
||||
const getIndexDirs = t => {
|
||||
|
@ -259,7 +260,7 @@ const getBuildIndexTransactionsByShard = (hierarchy, transactions) => {
|
|||
|
||||
return $(buildTransactions, [
|
||||
map(t => {
|
||||
const mappedRecord = evaluate(t.record)(indexNode)
|
||||
const mappedRecord = evaluate(t.record)(indexNode)
|
||||
mappedRecord.result = mappedRecord.result || t.record
|
||||
const indexDirs = getIndexDirs(t)
|
||||
return $(indexDirs, [
|
||||
|
@ -274,16 +275,16 @@ const getBuildIndexTransactionsByShard = (hierarchy, transactions) => {
|
|||
),
|
||||
})),
|
||||
])
|
||||
|
||||
}),
|
||||
flatten,
|
||||
reduce((obj, res) => {
|
||||
if (res.mappedRecord.passedFilter)
|
||||
obj.toWrite.push(res)
|
||||
else
|
||||
obj.toRemove.push(res)
|
||||
return obj
|
||||
}, { toWrite: [], toRemove: [] })
|
||||
reduce(
|
||||
(obj, res) => {
|
||||
if (res.mappedRecord.passedFilter) obj.toWrite.push(res)
|
||||
else obj.toRemove.push(res)
|
||||
return obj
|
||||
},
|
||||
{ toWrite: [], toRemove: [] }
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
@ -31,15 +31,16 @@ export const retrieve = async app => {
|
|||
app,
|
||||
joinKey(TRANSACTIONS_FOLDER, buildIndexFolder)
|
||||
)
|
||||
if(transactions.length === 0) {
|
||||
if (transactions.length === 0) {
|
||||
await app.datastore.deleteFolder(
|
||||
joinKey(TRANSACTIONS_FOLDER, buildIndexFolder))
|
||||
joinKey(TRANSACTIONS_FOLDER, buildIndexFolder)
|
||||
)
|
||||
} else {
|
||||
return transactions
|
||||
}
|
||||
currentFolderIndex += 1
|
||||
}
|
||||
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
|
@ -65,7 +66,7 @@ const retrieveBuildIndexTransactions = async (app, buildIndexFolder) => {
|
|||
const files = await app.datastore.getFolderContents(childFolderKey)
|
||||
|
||||
if (files.length > 0) {
|
||||
return { childFolderKey, files }
|
||||
return { childFolderKey, files }
|
||||
}
|
||||
|
||||
await app.datastore.deleteFolder(childFolderKey)
|
||||
|
|
|
@ -11,4 +11,4 @@ export const setCleanupFunc = (app, cleanupTransactions) => {
|
|||
newCleanup.isDefault = true
|
||||
app.cleanupTransactions = newCleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,10 @@ export const UPDATE_RECORD_TRANSACTION = "update"
|
|||
export const DELETE_RECORD_TRANSACTION = "delete"
|
||||
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 isDelete = isOfType(DELETE_RECORD_TRANSACTION)
|
||||
export const isCreate = isOfType(CREATE_RECORD_TRANSACTION)
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
parsedSuccess,
|
||||
getDefaultExport,
|
||||
} from "./typeHelpers"
|
||||
import { switchCase, defaultCase, toDateOrNull } from "../common"
|
||||
import { switchCase, defaultCase, toDateOrNull, isNonEmptyArray } from "../common"
|
||||
|
||||
const dateFunctions = typeFunctions({
|
||||
default: constant(null),
|
||||
|
@ -21,23 +21,32 @@ const parseStringToDate = s =>
|
|||
[defaultCase, parsedFailed]
|
||||
)(new Date(s))
|
||||
|
||||
const isNullOrEmpty = d =>
|
||||
isNull(d)
|
||||
|| (d || "").toString() === ""
|
||||
|
||||
const isDateOrEmpty = d =>
|
||||
isDate(d)
|
||||
|| isNullOrEmpty(d)
|
||||
|
||||
const dateTryParse = switchCase(
|
||||
[isDate, parsedSuccess],
|
||||
[isDateOrEmpty, parsedSuccess],
|
||||
[isString, parseStringToDate],
|
||||
[isNull, parsedSuccess],
|
||||
[defaultCase, parsedFailed]
|
||||
)
|
||||
|
||||
const options = {
|
||||
maxValue: {
|
||||
defaultValue: new Date(32503680000000),
|
||||
isValid: isDate,
|
||||
defaultValue: null,
|
||||
//defaultValue: new Date(32503680000000),
|
||||
isValid: isDateOrEmpty,
|
||||
requirementDescription: "must be a valid date",
|
||||
parse: toDateOrNull,
|
||||
},
|
||||
minValue: {
|
||||
defaultValue: new Date(-8520336000000),
|
||||
isValid: isDate,
|
||||
defaultValue: null,
|
||||
//defaultValue: new Date(-8520336000000),
|
||||
isValid: isDateOrEmpty,
|
||||
requirementDescription: "must be a valid date",
|
||||
parse: toDateOrNull,
|
||||
},
|
||||
|
@ -46,7 +55,7 @@ const options = {
|
|||
const typeConstraints = [
|
||||
makerule(
|
||||
async (val, opts) =>
|
||||
val === null || opts.minValue === null || val >= opts.minValue,
|
||||
val === null || isNullOrEmpty(opts.minValue) || val >= opts.minValue,
|
||||
(val, opts) =>
|
||||
`value (${val.toString()}) must be greater than or equal to ${
|
||||
opts.minValue
|
||||
|
@ -54,7 +63,7 @@ const typeConstraints = [
|
|||
),
|
||||
makerule(
|
||||
async (val, opts) =>
|
||||
val === null || opts.maxValue === null || val <= opts.maxValue,
|
||||
val === null || isNullOrEmpty(opts.maxValue) || val <= opts.maxValue,
|
||||
(val, opts) =>
|
||||
`value (${val.toString()}) must be less than or equal to ${
|
||||
opts.minValue
|
||||
|
|
|
@ -37,7 +37,7 @@ const options = {
|
|||
isValid: v =>
|
||||
v === null || (isArrayOfString(v) && v.length > 0 && v.length < 10000),
|
||||
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,
|
||||
},
|
||||
allowDeclaredValuesOnly: {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { permission } from "../src/authApi/permissions"
|
|||
|
||||
describe("collectionApi > delete", () => {
|
||||
it("should remove every key in collection's path", async () => {
|
||||
const { recordApi, collectionApi } = await setupApphierarchy(
|
||||
const { recordApi, collectionApi, appHierarchy } = await setupApphierarchy(
|
||||
basicAppHierarchyCreator_WithFields
|
||||
)
|
||||
const record1 = recordApi.getNew("/customers", "customer")
|
||||
|
@ -31,7 +31,10 @@ describe("collectionApi > delete", () => {
|
|||
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 () => {
|
||||
|
@ -51,7 +54,8 @@ describe("collectionApi > delete", () => {
|
|||
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")
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ export const testTemplatesPath = testAreaName =>
|
|||
path.join(testFileArea(testAreaName), templateDefinitions)
|
||||
|
||||
export const getMemoryStore = () => setupDatastore(memory({}))
|
||||
export const getMemoryTemplateApi = (store) => {
|
||||
export const getMemoryTemplateApi = store => {
|
||||
const app = {
|
||||
datastore: store || getMemoryStore(),
|
||||
publish: () => {},
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -25,7 +25,7 @@ describe("hierarchy node creation", () => {
|
|||
expect(record.validationRules).toEqual([])
|
||||
expect(record.indexes).toEqual([])
|
||||
expect(record.parent()).toBe(root)
|
||||
expect(record.collectionName).toBe("")
|
||||
expect(record.collectionName).toBe(record.nodeId.toString())
|
||||
expect(record.estimatedRecordCount).toBe(1000000)
|
||||
expect(record.isSingle).toBe(false)
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue