738 lines
20 KiB
JavaScript
738 lines
20 KiB
JavaScript
import {
|
|
hierarchy as hierarchyFunctions,
|
|
} from "../../../core/src";
|
|
import {
|
|
filter, cloneDeep, sortBy,
|
|
map, last, keys, concat, keyBy,
|
|
find, isEmpty, reduce, values
|
|
} from "lodash/fp";
|
|
import {
|
|
pipe, getNode, validate,
|
|
constructHierarchy, templateApi
|
|
} from "../common/core";
|
|
import { writable } from "svelte/store";
|
|
import { defaultPagesObject } from "../userInterface/pagesParsing/defaultPagesObject"
|
|
import { buildPropsHierarchy } from "../userInterface/pagesParsing/buildPropsHierarchy"
|
|
import api from "./api";
|
|
import { isRootComponent, getExactComponent } from "../userInterface/pagesParsing/searchComponents";
|
|
import { rename } from "../userInterface/pagesParsing/renameScreen";
|
|
import {
|
|
getNewComponentInfo, getScreenInfo, getComponentInfo
|
|
} from "../userInterface/pagesParsing/createProps";
|
|
import {
|
|
loadLibs, loadLibUrls, loadGeneratorLibs
|
|
} from "./loadComponentLibraries";
|
|
|
|
let appname = "";
|
|
|
|
export const getStore = () => {
|
|
|
|
const initial = {
|
|
apps: [],
|
|
appname: "",
|
|
hierarchy: {},
|
|
actions: [],
|
|
triggers: [],
|
|
pages: defaultPagesObject(),
|
|
mainUi: {},
|
|
unauthenticatedUi: {},
|
|
components: [],
|
|
currentFrontEndItem: null,
|
|
currentComponentInfo: null,
|
|
currentFrontEndType: "none",
|
|
currentPageName: "",
|
|
currentComponentProps: null,
|
|
currentNodeIsNew: false,
|
|
errors: [],
|
|
activeNav: "database",
|
|
isBackend: true,
|
|
hasAppPackage: false,
|
|
accessLevels: { version: 0, levels: [] },
|
|
currentNode: null,
|
|
libraries: null,
|
|
showSettings: false,
|
|
useAnalytics: true,
|
|
};
|
|
|
|
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.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.refreshComponents = refreshComponents(store);
|
|
store.addComponentLibrary = addComponentLibrary(store);
|
|
store.renameScreen = renameScreen(store);
|
|
store.deleteScreen = deleteScreen(store);
|
|
store.setCurrentScreen = setCurrentScreen(store);
|
|
store.setCurrentPage = setCurrentPage(store);
|
|
store.createScreen = createScreen(store);
|
|
store.removeComponentLibrary = removeComponentLibrary(store);
|
|
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);
|
|
store.addChildComponent = addChildComponent(store);
|
|
store.selectComponent = selectComponent(store);
|
|
store.updateComponentProp = updateComponentProp(store);
|
|
|
|
return store;
|
|
}
|
|
|
|
export default getStore;
|
|
|
|
const initialise = (store, initial) => async () => {
|
|
|
|
appname = window.location.hash
|
|
? last(window.location.hash.substr(1).split("/"))
|
|
: "";
|
|
|
|
if (!appname) {
|
|
initial.apps = await api.get(`/_builder/api/apps`).then(r => r.json());
|
|
initial.hasAppPackage = false;
|
|
store.set(initial);
|
|
return initial;
|
|
}
|
|
|
|
const pkg = await api.get(`/_builder/api/${appname}/appPackage`)
|
|
.then(r => r.json());
|
|
|
|
initial.libraries = await loadLibs(appname, pkg);
|
|
initial.generatorLibraries = await loadGeneratorLibs(appname, pkg);
|
|
initial.loadLibraryUrls = () => loadLibUrls(appname, pkg);
|
|
initial.appname = appname;
|
|
initial.pages = pkg.pages;
|
|
initial.hasAppPackage = true;
|
|
initial.hierarchy = pkg.appDefinition.hierarchy;
|
|
initial.accessLevels = pkg.accessLevels;
|
|
initial.screens = values(pkg.screens);
|
|
initial.generators = generatorsArray(pkg.components.generators);
|
|
initial.components = values(pkg.components.components);
|
|
initial.actions = values(pkg.appDefinition.actions);
|
|
initial.triggers = pkg.appDefinition.triggers;
|
|
|
|
if (!!initial.hierarchy && !isEmpty(initial.hierarchy)) {
|
|
initial.hierarchy = constructHierarchy(initial.hierarchy);
|
|
const shadowHierarchy = createShadowHierarchy(initial.hierarchy);
|
|
if (initial.currentNode !== null)
|
|
initial.currentNode = getNode(
|
|
shadowHierarchy, initial.currentNode.nodeId
|
|
);
|
|
}
|
|
|
|
store.set(initial);
|
|
return initial;
|
|
}
|
|
|
|
const generatorsArray = generators =>
|
|
pipe(generators, [
|
|
keys,
|
|
filter(k => k !== "_lib"),
|
|
map(k => generators[k])
|
|
]);
|
|
|
|
|
|
const showSettings = store => show => {
|
|
store.update(s => {
|
|
s.showSettings = !s.showSettings;
|
|
return s;
|
|
});
|
|
}
|
|
|
|
const useAnalytics = store => useAnalytics => {
|
|
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);
|
|
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);
|
|
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;
|
|
|
|
savePackage(store, s);
|
|
|
|
return s;
|
|
});
|
|
}
|
|
|
|
const importAppDefinition = store => appDefinition => {
|
|
store.update(s => {
|
|
s.hierarchy = appDefinition.hierarchy;
|
|
s.currentNode = appDefinition.hierarchy.children.length > 0
|
|
? appDefinition.hierarchy.children[0]
|
|
: null;
|
|
s.actions = appDefinition.actions;
|
|
s.triggers = appDefinition.triggers;
|
|
s.currentNodeIsNew = false;
|
|
return s;
|
|
});
|
|
}
|
|
|
|
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 = [];
|
|
savePackage(store, 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);
|
|
}
|
|
savePackage(store, s);
|
|
return s;
|
|
});
|
|
}
|
|
|
|
const deleteAction = store => action => {
|
|
store.update(s => {
|
|
s.actions = filter(a => a.name !== action.name)(s.actions);
|
|
savePackage(store, 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);
|
|
}
|
|
savePackage(store, 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);
|
|
|
|
savePackage(store, s);
|
|
return s;
|
|
});
|
|
}
|
|
|
|
const deleteLevel = store => level => {
|
|
store.update(s => {
|
|
s.accessLevels.levels = filter(t => t.name !== level.name)(s.accessLevels.levels);
|
|
incrementAccessLevelsVersion(s);
|
|
savePackage(store, s);
|
|
return s;
|
|
});
|
|
}
|
|
|
|
const setActiveNav = store => navName => {
|
|
store.update(s => {
|
|
s.activeNav = navName;
|
|
return s;
|
|
});
|
|
}
|
|
|
|
const createShadowHierarchy = hierarchy =>
|
|
constructHierarchy(JSON.parse(JSON.stringify(hierarchy)));
|
|
|
|
const saveScreen = store => (screen) => {
|
|
store.update(s => {
|
|
return _saveScreen(store, s, screen);
|
|
})
|
|
};
|
|
|
|
const _saveScreen = (store, s, screen) => {
|
|
const screens = pipe(s.screens, [
|
|
filter(c => c.name !== screen.name),
|
|
concat([screen])
|
|
]);
|
|
|
|
s.screens = screens;
|
|
s.currentFrontEndItem = screen;
|
|
s.currentComponentInfo = getScreenInfo(
|
|
s.components, screen);
|
|
|
|
api.post(`/_builder/api/${s.appname}/screen`, screen)
|
|
.then(() => savePackage(store, s));
|
|
|
|
return s;
|
|
}
|
|
|
|
const createScreen = store => (screenName, layoutComponentName) => {
|
|
store.update(s => {
|
|
const newComponentInfo = getNewComponentInfo(
|
|
s.components, layoutComponentName, screenName);
|
|
|
|
s.currentFrontEndItem = newComponentInfo.component;
|
|
s.currentComponentInfo = newComponentInfo;
|
|
s.currentFrontEndType = "screen";
|
|
|
|
return _saveScreen(store, s, newComponentInfo.component);
|
|
});
|
|
};
|
|
|
|
const createGeneratedComponents = store => components => {
|
|
store.update(s => {
|
|
s.components = [...s.components, ...components];
|
|
s.screens = [...s.screens, ...components];
|
|
|
|
const doCreate = async () => {
|
|
for (let c of components) {
|
|
await api.post(`/_builder/api/${s.appname}/screen`, c);
|
|
}
|
|
|
|
await savePackage(store, s);
|
|
}
|
|
|
|
doCreate();
|
|
|
|
return s;
|
|
});
|
|
};
|
|
|
|
const deleteScreen = store => name => {
|
|
store.update(s => {
|
|
|
|
const components = pipe(s.components, [
|
|
filter(c => c.name !== name)
|
|
]);
|
|
|
|
const screens = pipe(s.screens, [
|
|
filter(c => c.name !== name)
|
|
]);
|
|
|
|
s.components = components;
|
|
s.screens = screens;
|
|
if (s.currentFrontEndItem.name === name) {
|
|
s.currentFrontEndItem = null;
|
|
s.currentFrontEndType = "";
|
|
}
|
|
|
|
api.delete(`/_builder/api/${s.appname}/screen/${name}`);
|
|
|
|
return s;
|
|
})
|
|
}
|
|
|
|
const renameScreen = store => (oldname, newname) => {
|
|
store.update(s => {
|
|
|
|
const {
|
|
screens, pages, error, changedScreens
|
|
} = rename(s.pages, s.screens, oldname, newname);
|
|
|
|
if (error) {
|
|
// should really do something with this
|
|
return s;
|
|
}
|
|
|
|
s.screens = screens;
|
|
s.pages = pages;
|
|
if (s.currentFrontEndItem.name === oldname)
|
|
s.currentFrontEndItem.name = newname;
|
|
|
|
const saveAllChanged = async () => {
|
|
for (let screenName of changedScreens) {
|
|
const changedScreen
|
|
= getExactComponent(screens, screenName);
|
|
await api.post(`/_builder/api/${s.appname}/screen`, changedScreen);
|
|
}
|
|
}
|
|
|
|
api.patch(`/_builder/api/${s.appname}/screen`, {
|
|
oldname, newname
|
|
})
|
|
.then(() => saveAllChanged())
|
|
.then(() => {
|
|
savePackage(store, s);
|
|
});
|
|
|
|
return s;
|
|
})
|
|
}
|
|
|
|
const savePage = store => async page => {
|
|
store.update(s => {
|
|
if (s.currentFrontEndType !== "page" || !s.currentPageName) {
|
|
return s;
|
|
}
|
|
|
|
s.pages[s.currentPageName] = page;
|
|
savePackage(store, s);
|
|
return s;
|
|
});
|
|
}
|
|
|
|
const addComponentLibrary = store => async lib => {
|
|
|
|
const response =
|
|
await api.get(`/_builder/api/${appname}/componentlibrary?lib=${encodeURI(lib)}`, undefined, false);
|
|
|
|
const success = response.status === 200;
|
|
|
|
const error = response.status === 404
|
|
? `Could not find library ${lib}`
|
|
: success
|
|
? ""
|
|
: response.statusText;
|
|
|
|
const components = success
|
|
? await response.json()
|
|
: [];
|
|
|
|
store.update(s => {
|
|
if (success) {
|
|
|
|
const componentsArray = [];
|
|
for (let c in components) {
|
|
componentsArray.push(components[c]);
|
|
}
|
|
|
|
s.components = pipe(s.components, [
|
|
filter(c => !c.name.startsWith(`${lib}/`)),
|
|
concat(componentsArray)
|
|
]);
|
|
|
|
s.pages.componentLibraries.push(lib);
|
|
savePackage(store, s);
|
|
}
|
|
|
|
return s;
|
|
})
|
|
|
|
|
|
}
|
|
|
|
const removeComponentLibrary = store => lib => {
|
|
store.update(s => {
|
|
|
|
|
|
s.pages.componentLibraries = filter(l => l !== lib)(
|
|
s.pages.componentLibraries);
|
|
savePackage(store, s);
|
|
|
|
|
|
return s;
|
|
})
|
|
}
|
|
|
|
const addStylesheet = store => stylesheet => {
|
|
store.update(s => {
|
|
s.pages.stylesheets.push(stylesheet);
|
|
savePackage(store, s);
|
|
return s;
|
|
})
|
|
}
|
|
|
|
const removeStylesheet = store => stylesheet => {
|
|
store.update(s => {
|
|
s.pages.stylesheets = filter(s => s !== stylesheet)(s.pages.stylesheets);
|
|
savePackage(store, s);
|
|
return s;
|
|
});
|
|
}
|
|
|
|
const refreshComponents = store => async () => {
|
|
|
|
const componentsAndGenerators =
|
|
await api.get(`/_builder/api/${db.appname}/components`).then(r => r.json());
|
|
|
|
const components = pipe(componentsAndGenerators.components, [
|
|
keys,
|
|
map(k => ({ ...componentsAndGenerators[k], name: k }))
|
|
]);
|
|
|
|
store.update(s => {
|
|
s.components = pipe(s.components, [
|
|
filter(c => !isRootComponent(c)),
|
|
concat(components)
|
|
]);
|
|
s.generators = componentsAndGenerators.generators;
|
|
return s;
|
|
});
|
|
};
|
|
|
|
const savePackage = (store, s) => {
|
|
|
|
const appDefinition = {
|
|
hierarchy: s.hierarchy,
|
|
triggers: s.triggers,
|
|
actions: keyBy("name")(s.actions),
|
|
props: {
|
|
main: buildPropsHierarchy(
|
|
s.components,
|
|
s.screens,
|
|
s.pages.main.appBody),
|
|
unauthenticated: buildPropsHierarchy(
|
|
s.components,
|
|
s.screens,
|
|
s.pages.unauthenticated.appBody)
|
|
}
|
|
};
|
|
|
|
const data = {
|
|
appDefinition,
|
|
accessLevels: s.accessLevels,
|
|
pages: s.pages,
|
|
}
|
|
|
|
return api.post(`/_builder/api/${s.appname}/appPackage`, data);
|
|
}
|
|
|
|
const setCurrentScreen = store => screenName => {
|
|
store.update(s => {
|
|
const screen = getExactComponent(s.screens, screenName);
|
|
s.currentFrontEndItem = screen;
|
|
s.currentFrontEndType = "screen";
|
|
s.currentComponentInfo = getScreenInfo(s.components, screen);
|
|
return s;
|
|
})
|
|
}
|
|
|
|
const setCurrentPage = store => pageName => {
|
|
store.update(s => {
|
|
s.currentFrontEndType = "page";
|
|
s.currentPageName = pageName;
|
|
return s;
|
|
})
|
|
}
|
|
|
|
const addChildComponent = store => component => {
|
|
|
|
store.update(s => {
|
|
const newComponent = getNewComponentInfo(
|
|
s.components, component);
|
|
|
|
const children = s.currentFrontEndItem.props._children;
|
|
|
|
const component_definition = Object.assign(
|
|
cloneDeep(newComponent.fullProps), {
|
|
_component: component,
|
|
})
|
|
|
|
s.currentFrontEndItem.props._children =
|
|
children ?
|
|
children.concat(component_definition) :
|
|
[component_definition];
|
|
|
|
return s;
|
|
})
|
|
}
|
|
|
|
const selectComponent = store => component => {
|
|
store.update(s => {
|
|
s.currentComponentInfo = component;
|
|
return s;
|
|
})
|
|
|
|
}
|
|
|
|
const updateComponentProp = store => (name, value) => {
|
|
store.update(s => {
|
|
const current_component = s.currentComponentInfo;
|
|
s.currentComponentInfo[name] = value;
|
|
_saveScreen(store, s, s.currentFrontEndItem);
|
|
s.currentComponentInfo = current_component;
|
|
return s;
|
|
})
|
|
|
|
}
|