1
0
Fork 0
mirror of synced 2024-10-03 10:36:59 +13:00

Merge branch 'master' of https://github.com/Budibase/budibase into property-panel/components-from-design

This commit is contained in:
Conor_Mack 2020-06-02 10:51:57 +01:00
commit 98610c60ed
42 changed files with 1813 additions and 1311 deletions

View file

@ -51,7 +51,7 @@
"safe-buffer": "^5.1.2",
"shortid": "^2.2.8",
"string_decoder": "^1.2.0",
"svelte-simple-modal": "^0.3.0",
"svelte-simple-modal": "^0.4.2",
"uikit": "^3.1.7"
},
"devDependencies": {

View file

@ -57,23 +57,23 @@
.budibase__nav-item {
cursor: pointer;
padding: 0 7px 0 3px;
padding: 0 4px 0 2px;
height: 35px;
margin: 5px 20px 5px 0px;
margin: 5px 0px 4px 0px;
border-radius: 0 5px 5px 0;
display: flex;
align-items: center;
font-weight: 500;
font-size: 13px;
font-size: 14px;
transition: 0.2s;
}
.budibase__nav-item.selected {
color: var(--button-text);
background: #f1f4fc;
color: var(--ink);
background: var(--blue-light);
}
.budibase__nav-item:hover {
background: #fafafa;
background: var(--grey-light);
}
.budibase__input {

View file

@ -1,42 +1,41 @@
export const generate_screen_css = (component_arr) => {
let styles = '';
export const generate_screen_css = component_arr => {
let styles = ""
for (const { _styles, _id, _children, _component } of component_arr) {
let [ componentName ] = _component.match(/[a-z]*$/);
Object.keys(_styles).forEach((selector) => {
const cssString = generate_css(_styles[selector]);
let [componentName] = _component.match(/[a-z]*$/)
Object.keys(_styles).forEach(selector => {
const cssString = generate_css(_styles[selector])
if (cssString) {
styles += apply_class(_id, componentName, cssString, selector);
styles += apply_class(_id, componentName, cssString, selector)
}
});
})
if (_children && _children.length) {
styles += generate_screen_css(_children) + '\n';
styles += generate_screen_css(_children) + "\n"
}
}
return styles.trim();
};
return styles.trim()
}
export const generate_css = (style) => {
export const generate_css = style => {
let cssString = Object.entries(style).reduce((str, [key, value]) => {
//TODO Handle arrays and objects here also
if (typeof value === 'string') {
if (typeof value === "string") {
if (value) {
return (str += `${key}: ${value};\n`);
return (str += `${key}: ${value};\n`)
}
} else if (Array.isArray(value)) {
if (value.length > 0 && !value.every((v) => v === '')) {
return (str += `${key}: ${value.join(' ')};\n`);
if (value.length > 0 && !value.every(v => v === "")) {
return (str += `${key}: ${value.join(" ")};\n`)
}
}
}, '');
}, "")
return (cssString || '').trim();
};
return (cssString || "").trim()
}
export const apply_class = (id, name = 'element', styles, selector) => {
if (selector === 'normal') {
return `.${name}-${id} {\n${styles}\n}`;
export const apply_class = (id, name = "element", styles, selector) => {
if (selector === "normal") {
return `.${name}-${id} {\n${styles}\n}`
} else {
let sel = selector === 'selected' ? '::selection' : `:${selector}`;
return `.${name}-${id}${sel} {\n${styles}\n}`;
let sel = selector === "selected" ? "::selection" : `:${selector}`
return `.${name}-${id}${sel} {\n${styles}\n}`
}
}
};

View file

@ -1,107 +1,123 @@
import { cloneDeep, values } from 'lodash/fp';
import { backendUiStore } from 'builderStore';
import * as backendStoreActions from './backend';
import { writable, get } from 'svelte/store';
import api from '../api';
import { DEFAULT_PAGES_OBJECT } from '../../constants';
import { getExactComponent } from 'components/userInterface/pagesParsing/searchComponents';
import { rename } from 'components/userInterface/pagesParsing/renameScreen';
import { createProps, makePropsSafe, getBuiltin } from 'components/userInterface/pagesParsing/createProps';
import { fetchComponentLibDefinitions } from '../loadComponentLibraries';
import { buildCodeForScreens } from '../buildCodeForScreens';
import { generate_screen_css } from '../generate_css';
import { insertCodeMetadata } from '../insertCodeMetadata';
import { uuid } from '../uuid';
import { values } from "lodash/fp"
import { backendUiStore } from "builderStore"
import * as backendStoreActions from "./backend"
import { writable, get } from "svelte/store"
import api from "../api"
import { DEFAULT_PAGES_OBJECT } from "../../constants"
import { getExactComponent } from "components/userInterface/pagesParsing/searchComponents"
import { rename } from "components/userInterface/pagesParsing/renameScreen"
import {
createProps,
makePropsSafe,
getBuiltin,
} from "components/userInterface/pagesParsing/createProps"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import { buildCodeForScreens } from "../buildCodeForScreens"
import { generate_screen_css } from "../generate_css"
import { insertCodeMetadata } from "../insertCodeMetadata"
import { uuid } from "../uuid"
import {
selectComponent as _selectComponent,
getParent,
walkProps,
savePage as _savePage,
saveCurrentPreviewItem as _saveCurrentPreviewItem,
saveScreenApi as _saveScreenApi,
} from "../storeUtils"
export const getStore = () => {
const initial = {
apps: [],
appname: '',
appname: "",
pages: DEFAULT_PAGES_OBJECT,
mainUi: {},
unauthenticatedUi: {},
components: [],
currentPreviewItem: null,
currentComponentInfo: null,
currentFrontEndType: 'none',
currentPageName: '',
currentFrontEndType: "none",
currentPageName: "",
currentComponentProps: null,
errors: [],
hasAppPackage: false,
libraries: null,
appId: ''
};
appId: "",
}
const store = writable(initial);
const store = writable(initial)
store.setPackage = setPackage(store, initial);
store.setPackage = setPackage(store, initial)
store.createDatabaseForApp = backendStoreActions.createDatabaseForApp(store);
store.createDatabaseForApp = backendStoreActions.createDatabaseForApp(store)
store.saveScreen = saveScreen(store);
store.renameScreen = renameScreen(store);
store.deleteScreen = deleteScreen(store);
store.setCurrentScreen = setCurrentScreen(store);
store.setCurrentPage = setCurrentPage(store);
store.createScreen = createScreen(store);
store.addStylesheet = addStylesheet(store);
store.removeStylesheet = removeStylesheet(store);
store.savePage = savePage(store);
store.addChildComponent = addChildComponent(store);
store.selectComponent = selectComponent(store);
store.setComponentProp = setComponentProp(store);
store.setComponentStyle = setComponentStyle(store);
store.setComponentCode = setComponentCode(store);
store.setScreenType = setScreenType(store);
store.deleteComponent = deleteComponent(store);
store.moveUpComponent = moveUpComponent(store);
store.moveDownComponent = moveDownComponent(store);
store.copyComponent = copyComponent(store);
store.getPathToComponent = getPathToComponent(store);
store.addTemplatedComponent = addTemplatedComponent(store);
store.setMetadataProp = setMetadataProp(store);
return store;
};
store.saveScreen = saveScreen(store)
store.renameScreen = renameScreen(store)
store.deleteScreen = deleteScreen(store)
store.setCurrentScreen = setCurrentScreen(store)
store.setCurrentPage = setCurrentPage(store)
store.createScreen = createScreen(store)
store.addStylesheet = addStylesheet(store)
store.removeStylesheet = removeStylesheet(store)
store.savePage = savePage(store)
store.addChildComponent = addChildComponent(store)
store.selectComponent = selectComponent(store)
store.setComponentProp = setComponentProp(store)
store.setComponentStyle = setComponentStyle(store)
store.setComponentCode = setComponentCode(store)
store.setScreenType = setScreenType(store)
store.getPathToComponent = getPathToComponent(store)
store.addTemplatedComponent = addTemplatedComponent(store)
store.setMetadataProp = setMetadataProp(store)
return store
}
export default getStore;
export default getStore
const setPackage = (store, initial) => async (pkg) => {
export const getComponentDefinition = (state, name) =>
name.startsWith("##") ? getBuiltin(name) : state.components[name]
const setPackage = (store, initial) => async pkg => {
const [main_screens, unauth_screens] = await Promise.all([
api.get(`/_builder/api/${pkg.application._id}/pages/main/screens`).then((r) => r.json()),
api.get(`/_builder/api/${pkg.application._id}/pages/unauthenticated/screens`).then((r) => r.json())
]);
api
.get(`/_builder/api/${pkg.application._id}/pages/main/screens`)
.then(r => r.json()),
api
.get(`/_builder/api/${pkg.application._id}/pages/unauthenticated/screens`)
.then(r => r.json()),
])
pkg.pages = {
main: {
...pkg.pages.main,
_screens: Object.values(main_screens)
_screens: Object.values(main_screens),
},
unauthenticated: {
...pkg.pages.unauthenticated,
_screens: Object.values(unauth_screens)
_screens: Object.values(unauth_screens),
},
}
};
initial.libraries = pkg.application.componentLibraries;
initial.components = await fetchComponentLibDefinitions(pkg.application._id);
initial.appname = pkg.application.name;
initial.appId = pkg.application._id;
initial.pages = pkg.pages;
initial.hasAppPackage = true;
initial.screens = values(pkg.screens);
initial.builtins = [ getBuiltin('##builtin/screenslot') ];
initial.appInstances = pkg.application.instances;
initial.appId = pkg.application._id;
initial.libraries = pkg.application.componentLibraries
initial.components = await fetchComponentLibDefinitions(pkg.application._id)
initial.appname = pkg.application.name
initial.appId = pkg.application._id
initial.pages = pkg.pages
initial.hasAppPackage = true
initial.screens = values(pkg.screens)
initial.builtins = [getBuiltin("##builtin/screenslot")]
initial.appInstances = pkg.application.instances
initial.appId = pkg.application._id
store.set(initial);
return initial;
};
store.set(initial)
return initial
}
const saveScreen = (store) => (screen) => {
store.update((state) => {
return _saveScreen(store, state, screen);
});
};
const saveScreen = store => screen => {
store.update(state => {
return _saveScreen(store, state, screen)
})
}
const _saveScreen = async (store, s, screen) => {
const currentPageScreens = s.pages[s.currentPageName]._screens;
@ -127,430 +143,349 @@ const _saveScreen = async (store, s, screen) => {
return s;
};
const _saveScreenApi = (screen, s) => {
api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen).then(() => _savePage(s));
};
const createScreen = (store) => (screenName, route, layoutComponentName) => {
store.update((state) => {
const rootComponent = state.components[layoutComponentName];
const createScreen = store => (screenName, route, layoutComponentName) => {
store.update(state => {
const rootComponent = state.components[layoutComponentName]
const newScreen = {
name: screenName || '',
description: '',
url: '',
_css: '',
uiFunctions: '',
props: createProps(rootComponent).props
};
newScreen.route = route;
state.currentPreviewItem = newScreen;
state.currentComponentInfo = newScreen.props;
state.currentFrontEndType = 'screen';
_saveScreen(store, state, newScreen);
return state;
});
};
const setCurrentScreen = (store) => (screenName) => {
store.update((s) => {
const screen = getExactComponent(s.screens, screenName);
screen._css = generate_screen_css([ screen.props ]);
s.currentPreviewItem = screen;
s.currentFrontEndType = 'screen';
s.currentView = 'detail';
const safeProps = makePropsSafe(s.components[screen.props._component], screen.props);
screen.props = safeProps;
s.currentComponentInfo = safeProps;
setCurrentPageFunctions(s);
return s;
});
};
const deleteScreen = (store) => (name) => {
store.update((s) => {
const components = s.components.filter((c) => c.name !== name);
const screens = s.screens.filter((c) => c.name !== name);
s.components = components;
s.screens = screens;
if (s.currentPreviewItem.name === name) {
s.currentPreviewItem = null;
s.currentFrontEndType = '';
name: screenName || "",
description: "",
url: "",
_css: "",
uiFunctions: "",
props: createProps(rootComponent).props,
}
api.delete(`/_builder/api/${s.appId}/screen/${name}`);
newScreen.route = route
state.currentPreviewItem = newScreen
state.currentComponentInfo = newScreen.props
state.currentFrontEndType = "screen"
return s;
});
};
_saveScreen(store, state, newScreen)
const renameScreen = (store) => (oldname, newname) => {
store.update((s) => {
const { screens, pages, error, changedScreens } = rename(s.pages, s.screens, oldname, newname);
return state
})
}
const setCurrentScreen = store => screenName => {
store.update(s => {
const screen = getExactComponent(s.screens, screenName)
screen._css = generate_screen_css([screen.props])
s.currentPreviewItem = screen
s.currentFrontEndType = "screen"
s.currentView = "detail"
const safeProps = makePropsSafe(
s.components[screen.props._component],
screen.props
)
screen.props = safeProps
s.currentComponentInfo = safeProps
setCurrentPageFunctions(s)
return s
})
}
const deleteScreen = store => name => {
store.update(s => {
const components = s.components.filter(c => c.name !== name)
const screens = s.screens.filter(c => c.name !== name)
s.components = components
s.screens = screens
if (s.currentPreviewItem.name === name) {
s.currentPreviewItem = null
s.currentFrontEndType = ""
}
api.delete(`/_builder/api/${s.appId}/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;
return s
}
s.screens = screens;
s.pages = pages;
if (s.currentPreviewItem.name === oldname) s.currentPreviewItem.name = newname;
s.screens = screens
s.pages = pages
if (s.currentPreviewItem.name === oldname)
s.currentPreviewItem.name = newname
const saveAllChanged = async () => {
for (let screenName of changedScreens) {
const changedScreen = getExactComponent(screens, screenName);
await api.post(`/_builder/api/${s.appId}/screen`, changedScreen);
const changedScreen = getExactComponent(screens, screenName)
await api.post(`/_builder/api/${s.appId}/screen`, changedScreen)
}
}
};
api
.patch(`/_builder/api/${s.appId}/screen`, {
oldname,
newname
newname,
})
.then(() => saveAllChanged())
.then(() => {
_savePage(s);
});
_savePage(s)
})
return s;
});
};
const savePage = (store) => async (page) => {
store.update((state) => {
if (state.currentFrontEndType !== 'page' || !state.currentPageName) {
return state;
return s
})
}
state.pages[state.currentPageName] = page;
_savePage(state);
return state;
});
};
const savePage = store => async page => {
store.update(state => {
if (state.currentFrontEndType !== "page" || !state.currentPageName) {
return state
}
const addStylesheet = (store) => (stylesheet) => {
store.update((s) => {
s.pages.stylesheets.push(stylesheet);
_savePage(s);
return s;
});
};
state.pages[state.currentPageName] = page
_savePage(state)
return state
})
}
const removeStylesheet = (store) => (stylesheet) => {
store.update((state) => {
state.pages.stylesheets = state.pages.stylesheets.filter((s) => s !== stylesheet);
_savePage(state);
return state;
});
};
const addStylesheet = store => stylesheet => {
store.update(s => {
s.pages.stylesheets.push(stylesheet)
_savePage(s)
return s
})
}
const _savePage = async (s) => {
const page = s.pages[s.currentPageName];
await api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}`, {
page: { componentLibraries: s.pages.componentLibraries, ...page },
uiFunctions: s.currentPageFunctions,
screens: page._screens
});
};
const removeStylesheet = store => stylesheet => {
store.update(state => {
state.pages.stylesheets = state.pages.stylesheets.filter(
s => s !== stylesheet
)
_savePage(state)
return state
})
}
const setCurrentPage = (store) => (pageName) => {
store.update((state) => {
const current_screens = state.pages[pageName]._screens;
const setCurrentPage = store => pageName => {
store.update(state => {
const current_screens = state.pages[pageName]._screens
const currentPage = state.pages[pageName];
const currentPage = state.pages[pageName]
state.currentFrontEndType = 'page';
state.currentPageName = pageName;
state.screens = Array.isArray(current_screens) ? current_screens : Object.values(current_screens);
const safeProps = makePropsSafe(state.components[currentPage.props._component], currentPage.props);
state.currentComponentInfo = safeProps;
currentPage.props = safeProps;
state.currentPreviewItem = state.pages[pageName];
state.currentPreviewItem._css = generate_screen_css([ state.currentPreviewItem.props ]);
state.currentFrontEndType = "page"
state.currentPageName = pageName
state.screens = Array.isArray(current_screens)
? current_screens
: Object.values(current_screens)
const safeProps = makePropsSafe(
state.components[currentPage.props._component],
currentPage.props
)
state.currentComponentInfo = safeProps
currentPage.props = safeProps
state.currentPreviewItem = state.pages[pageName]
state.currentPreviewItem._css = generate_screen_css([
state.currentPreviewItem.props,
])
for (let screen of state.screens) {
screen._css = generate_screen_css([ screen.props ]);
screen._css = generate_screen_css([screen.props])
}
setCurrentPageFunctions(state);
return state;
});
};
// const getComponentDefinition = (components, name) => components.find(c => c.name === name)
setCurrentPageFunctions(state)
return state
})
}
/**
* @param {string} componentToAdd - name of the component to add to the application
* @param {string} presetName - name of the component preset if defined
*/
const addChildComponent = (store) => (componentToAdd, presetName) => {
store.update((state) => {
const addChildComponent = store => (componentToAdd, presetName) => {
store.update(state => {
function findSlot(component_array) {
for (let i = 0; i < component_array.length; i += 1) {
if (component_array[i]._component === '##builtin/screenslot') {
return true;
if (component_array[i]._component === "##builtin/screenslot") {
return true
}
if (component_array[i]._children) findSlot(component_array[i]);
if (component_array[i]._children) findSlot(component_array[i])
}
return false;
return false
}
if (componentToAdd.startsWith('##') && findSlot(state.pages[state.currentPageName].props._children)) {
return state;
if (
componentToAdd.startsWith("##") &&
findSlot(state.pages[state.currentPageName].props._children)
) {
return state
}
const component = componentToAdd.startsWith('##')
? getBuiltin(componentToAdd)
: state.components[componentToAdd];
const component = getComponentDefinition(state, componentToAdd)
const presetProps = presetName ? component.presets[presetName] : {};
const presetProps = presetName ? component.presets[presetName] : {}
const instanceId = get(backendUiStore).selectedDatabase._id;
const instanceId = get(backendUiStore).selectedDatabase._id
const newComponent = createProps(
component,
{
...presetProps,
_instanceId: instanceId
_instanceId: instanceId,
},
state
);
)
state.currentComponentInfo._children = state.currentComponentInfo._children.concat(newComponent.props);
state.currentComponentInfo._children = state.currentComponentInfo._children.concat(
newComponent.props
)
state.currentFrontEndType === 'page' ? _savePage(state) : _saveScreenApi(state.currentPreviewItem, state);
state.currentFrontEndType === "page"
? _savePage(state)
: _saveScreenApi(state.currentPreviewItem, state)
state.currentView = 'component';
state.currentComponentInfo = newComponent.props;
state.currentView = "component"
state.currentComponentInfo = newComponent.props
return state;
});
};
return state
})
}
/**
* @param {string} props - props to add, as child of current component
*/
const addTemplatedComponent = (store) => (props) => {
store.update((state) => {
walkProps(props, (p) => {
p._id = uuid();
});
state.currentComponentInfo._children = state.currentComponentInfo._children.concat(props);
state.currentPreviewItem._css = generate_screen_css([ state.currentPreviewItem.props ]);
setCurrentPageFunctions(state);
_saveCurrentPreviewItem(state);
const addTemplatedComponent = store => props => {
store.update(state => {
walkProps(props, p => {
p._id = uuid()
})
state.currentComponentInfo._children = state.currentComponentInfo._children.concat(
props
)
state.currentPreviewItem._css = generate_screen_css([
state.currentPreviewItem.props,
])
return state;
});
};
setCurrentPageFunctions(state)
_saveCurrentPreviewItem(state)
const selectComponent = (store) => (component) => {
store.update((state) => {
const componentDef = component._component.startsWith('##') ? component : state.components[component._component];
state.currentComponentInfo = makePropsSafe(componentDef, component);
state.currentView = 'component';
return state;
});
};
const setComponentProp = (store) => (name, value) => {
store.update((state) => {
const current_component = state.currentComponentInfo;
state.currentComponentInfo[name] = value;
_saveCurrentPreviewItem(state);
state.currentComponentInfo = current_component;
return state;
});
};
const setComponentStyle = (store) => (type, name, value) => {
store.update((state) => {
if (!state.currentComponentInfo._styles) {
state.currentComponentInfo._styles = {};
return state
})
}
state.currentComponentInfo._styles[type][name] = value;
state.currentPreviewItem._css = generate_screen_css([ state.currentPreviewItem.props ]);
const selectComponent = store => component => {
store.update(state => {
return _selectComponent(state, component)
})
}
const setComponentProp = store => (name, value) => {
store.update(state => {
const current_component = state.currentComponentInfo
state.currentComponentInfo[name] = value
_saveCurrentPreviewItem(state)
state.currentComponentInfo = current_component
return state
})
}
const setComponentStyle = store => (type, name, value) => {
store.update(state => {
if (!state.currentComponentInfo._styles) {
state.currentComponentInfo._styles = {}
}
state.currentComponentInfo._styles[type][name] = value
state.currentPreviewItem._css = generate_screen_css([
state.currentPreviewItem.props,
])
// save without messing with the store
_saveCurrentPreviewItem(state);
return state;
});
};
_saveCurrentPreviewItem(state)
return state
})
}
const setComponentCode = (store) => (code) => {
store.update((state) => {
state.currentComponentInfo._code = code;
const setComponentCode = store => code => {
store.update(state => {
state.currentComponentInfo._code = code
setCurrentPageFunctions(state);
setCurrentPageFunctions(state)
// save without messing with the store
_saveScreenApi(state.currentPreviewItem, state);
_saveScreenApi(state.currentPreviewItem, state)
return state;
});
};
return state
})
}
const setCurrentPageFunctions = (s) => {
s.currentPageFunctions = buildPageCode(s.screens, s.pages[s.currentPageName]);
insertCodeMetadata(s.currentPreviewItem.props);
};
const setCurrentPageFunctions = s => {
s.currentPageFunctions = buildPageCode(s.screens, s.pages[s.currentPageName])
insertCodeMetadata(s.currentPreviewItem.props)
}
const buildPageCode = (screens, page) => buildCodeForScreens([ page, ...screens ]);
const buildPageCode = (screens, page) => buildCodeForScreens([page, ...screens])
const setScreenType = (store) => (type) => {
store.update((state) => {
state.currentFrontEndType = type;
const setScreenType = store => type => {
store.update(state => {
state.currentFrontEndType = type
const pageOrScreen =
type === 'page' ? state.pages[state.currentPageName] : state.pages[state.currentPageName]._screens[0];
type === "page"
? state.pages[state.currentPageName]
: state.pages[state.currentPageName]._screens[0]
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null;
state.currentPreviewItem = pageOrScreen;
return state;
});
};
const deleteComponent = (store) => (componentName) => {
store.update((state) => {
const parent = getParent(state.currentPreviewItem.props, componentName);
if (parent) {
parent._children = parent._children.filter((component) => component !== componentName);
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null
state.currentPreviewItem = pageOrScreen
return state
})
}
_saveCurrentPreviewItem(state);
return state;
});
};
const moveUpComponent = (store) => (component) => {
store.update((s) => {
const parent = getParent(s.currentPreviewItem.props, component);
if (parent) {
const currentIndex = parent._children.indexOf(component);
if (currentIndex === 0) return s;
const newChildren = parent._children.filter((c) => c !== component);
newChildren.splice(currentIndex - 1, 0, component);
parent._children = newChildren;
}
s.currentComponentInfo = component;
_saveCurrentPreviewItem(s);
return s;
});
};
const moveDownComponent = (store) => (component) => {
store.update((s) => {
const parent = getParent(s.currentPreviewItem.props, component);
if (parent) {
const currentIndex = parent._children.indexOf(component);
if (currentIndex === parent._children.length - 1) return s;
const newChildren = parent._children.filter((c) => c !== component);
newChildren.splice(currentIndex + 1, 0, component);
parent._children = newChildren;
}
s.currentComponentInfo = component;
_saveCurrentPreviewItem(s);
return s;
});
};
const copyComponent = (store) => (component) => {
store.update((s) => {
const parent = getParent(s.currentPreviewItem.props, component);
const copiedComponent = cloneDeep(component);
walkProps(copiedComponent, (p) => {
p._id = uuid();
});
parent._children = [ ...parent._children, copiedComponent ];
s.curren;
_saveCurrentPreviewItem(s);
s.currentComponentInfo = copiedComponent;
return s;
});
};
const getPathToComponent = (store) => (component) => {
const getPathToComponent = store => component => {
// Gets all the components to needed to construct a path.
const tempStore = get(store);
let pathComponents = [];
let parent = component;
let root = false;
const tempStore = get(store)
let pathComponents = []
let parent = component
let root = false
while (!root) {
parent = getParent(tempStore.currentPreviewItem.props, parent);
parent = getParent(tempStore.currentPreviewItem.props, parent)
if (!parent) {
root = true;
root = true
} else {
pathComponents.push(parent);
pathComponents.push(parent)
}
}
// Remove root entry since it's the screen or page layout.
// Reverse array since we need the correct order of the IDs
const reversedComponents = pathComponents.reverse().slice(1);
const reversedComponents = pathComponents.reverse().slice(1)
// Add component
const allComponents = [ ...reversedComponents, component ];
const allComponents = [...reversedComponents, component]
// Map IDs
const IdList = allComponents.map((c) => c._id);
const IdList = allComponents.map(c => c._id)
// Construct ID Path:
const path = IdList.join('/');
const path = IdList.join("/")
return path;
};
const getParent = (rootProps, child) => {
let parent;
walkProps(rootProps, (p, breakWalk) => {
if (p._children && p._children.includes(child)) {
parent = p;
breakWalk();
return path
}
});
return parent;
};
const walkProps = (props, action, cancelToken = null) => {
cancelToken = cancelToken || { cancelled: false };
action(props, () => {
cancelToken.cancelled = true;
});
if (props._children) {
for (let child of props._children) {
if (cancelToken.cancelled) return;
walkProps(child, action, cancelToken);
const setMetadataProp = store => (name, prop) => {
store.update(s => {
s.currentPreviewItem[name] = prop
return s
})
}
}
};
const setMetadataProp = (store) => (name, prop) => {
store.update((s) => {
s.currentPreviewItem[name] = prop;
return s;
});
};
const _saveCurrentPreviewItem = (s) =>
s.currentFrontEndType === 'page' ? _savePage(s) : _saveScreenApi(s.currentPreviewItem, s);

View file

@ -0,0 +1,59 @@
import { makePropsSafe } from "components/userInterface/pagesParsing/createProps"
import api from "./api"
export const selectComponent = (state, component) => {
const componentDef = component._component.startsWith("##")
? component
: state.components[component._component]
state.currentComponentInfo = makePropsSafe(componentDef, component)
state.currentView = "component"
return state
}
export const getParent = (rootProps, child) => {
let parent
walkProps(rootProps, (p, breakWalk) => {
if (
p._children &&
(p._children.includes(child) || p._children.some(c => c._id === child))
) {
parent = p
breakWalk()
}
})
return parent
}
export const saveCurrentPreviewItem = s =>
s.currentFrontEndType === "page"
? savePage(s)
: saveScreenApi(s.currentPreviewItem, s)
export const savePage = async s => {
const page = s.pages[s.currentPageName]
await api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}`, {
page: { componentLibraries: s.pages.componentLibraries, ...page },
uiFunctions: s.currentPageFunctions,
screens: page._screens,
})
}
export const saveScreenApi = (screen, s) => {
api
.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen)
.then(() => savePage(s))
}
export const walkProps = (props, action, cancelToken = null) => {
cancelToken = cancelToken || { cancelled: false }
action(props, () => {
cancelToken.cancelled = true
})
if (props._children) {
for (let child of props._children) {
if (cancelToken.cancelled) return
walkProps(child, action, cancelToken)
}
}
}

View file

@ -5,7 +5,7 @@
UIKit.notification({
message: `
<div class="message-container">
<i class="ri-information-fill information-icon"></i>
<div class="information-icon">🤯</div>
<span class="notification-message">
${message}
</span>
@ -21,6 +21,7 @@
<style>
:global(.information-icon) {
font-size: 24px;
margin-right: 8px;
}
:global(.uk-nofi) {
@ -31,10 +32,9 @@
}
:global(.message-container) {
display: grid;
grid-template-columns: 40px 1fr auto;
grid-gap: 5px;
display: flex;
align-items: center;
justify-content: center;
}
:global(.uk-notification) {
@ -44,7 +44,6 @@
margin-right: auto !important;
margin-left: auto !important;
border-radius: 10px;
box-shadow: 0px 3px 6px #00000029;
}
:global(.uk-notification-message) {
@ -56,21 +55,23 @@
}
:global(.uk-notification-message-danger) {
background: #f2545b !important;
background: var(--ink-light) !important;
color: #fff !important;
font-family: Roboto;
font-size: 14px !important;
font-size: 16px !important;
}
:global(.refresh-page-button) {
font-size: 13px;
font-weight: 600;
border-radius: 5px;
font-size: 14px;
border-radius: 3px;
border: none;
padding: 5px;
width: 91px;
height: 28px;
color: #f2545b;
padding: 8px 16px;
color: var(--ink);
background: #ffffff;
margin-left: 20px;
}
:global(.refresh-page-button):hover {
background: var(--grey-light);
}
</style>

View file

@ -0,0 +1,12 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6
12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21
12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5
1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z" />
</svg>

After

Width:  |  Height:  |  Size: 419 B

View file

@ -31,3 +31,4 @@ export { default as EmailIcon } from "./Email.svelte"
export { default as TwitterIcon } from "./Twitter.svelte"
export { default as InfoIcon } from "./Info.svelte"
export { default as CloseIcon } from "./Close.svelte"
export { default as MoreIcon } from "./More.svelte"

View file

@ -15,7 +15,7 @@
<style>
input {
width: 32px;
/* width: 32px; */
height: 32px;
font-size: 12px;
font-weight: 700;
@ -23,6 +23,9 @@
color: var(--ink);
opacity: 0.7;
padding: 0px 4px;
line-height: 1.3;
/* padding: 12px; */
width: 164px;
box-sizing: border-box;
border: 1px solid var(--grey);
border-radius: 2px;

View file

@ -6,6 +6,7 @@
export let label = ""
export let value = ["0", "0", "0", "0"]
export let suffix = ""
export let onChange = () => {}
function handleChange(val, idx) {
@ -42,4 +43,5 @@
.inputs-group {
flex: 1;
}
</style>

View file

@ -0,0 +1,77 @@
<script>
export let tabs = []
export const selectTab = tabName => {
selected = tabName
selectedIndex = tabs.indexOf(selected)
}
let selected = tabs.length > 0 && tabs[0]
let selectedIndex = 0
const isSelected = tab => selected === tab
</script>
<div class="root">
<div class="switcher">
{#each tabs as tab}
<button class:selected={selected === tab} on:click={() => selectTab(tab)}>
{tab}
</button>
{/each}
</div>
<div class="panel">
{#if selectedIndex === 0}
<slot name="0" />
{:else if selectedIndex === 1}
<slot name="1" />
{:else if selectedIndex === 2}
<slot name="2" />
{:else if selectedIndex === 3}
<slot name="3" />
{/if}
</div>
</div>
<style>
.root {
height: 100%;
display: flex;
flex-direction: column;
padding: 20px 20px;
border-left: solid 1px var(--grey);
box-sizing: border-box;
}
.switcher {
display: flex;
margin: 0px 20px 20px 0px;
box-sizing: border-box;
}
.switcher > button {
display: inline-block;
border: none;
margin: 0;
padding: 0;
cursor: pointer;
font-size: 18px;
font-weight: 700;
color: var(--ink-lighter);
margin-right: 20px;
background: none;
}
.switcher > .selected {
color: var(--ink);
}
.panel {
min-height: 0;
overflow-y: auto;
}
</style>

View file

@ -13,17 +13,41 @@
const FIELD_TYPES = ["string", "number", "boolean"]
export let field = { type: "string" }
export let field = {
type: "string",
constraints: { type: "string", presence: false },
}
export let schema
export let goBack
let errors = []
let draftField = cloneDeep(field)
let type = field.type
let constraints = field.constraints
let required =
field.constraints.presence && !field.constraints.presence.allowEmpty
const save = () => {
constraints.presence = required ? { allowEmpty: false } : false
draftField.constraints = constraints
draftField.type = type
schema[field.name] = draftField
goBack()
}
$: constraints =
type === "string"
? { type: "string", length: {}, presence: false }
: type === "number"
? { type: "number", presence: false, numericality: {} }
: type === "boolean"
? { type: "boolean", presence: false }
: type === "datetime"
? { type: "date", datetime: {}, presence: false }
: type.startsWith("array")
? { type: "array", presence: false }
: { type: "string", presence: false }
</script>
<div class="root">
@ -32,32 +56,26 @@
<form on:submit|preventDefault class="uk-form-stacked">
<Textbox label="Name" bind:text={field.name} />
<Dropdown
label="Type"
bind:selected={draftField.type}
options={FIELD_TYPES} />
<Dropdown label="Type" bind:selected={type} options={FIELD_TYPES} />
{#if field.type === 'string'}
<NumberBox label="Max Length" bind:value={draftField.maxLength} />
<ValuesList label="Categories" bind:values={draftField.values} />
{:else if field.type === 'boolean'}
<!-- TODO: revisit and fix with JSON schema -->
<Checkbox label="Allow Null" bind:checked={draftField.allowNulls} />
{:else if field.format === 'datetime'}
<!-- TODO: revisit and fix with JSON schema -->
<DatePicker label="Min Value" bind:value={draftField.minValue} />
<DatePicker label="Max Value" bind:value={draftField.maxValue} />
{:else if field.type === 'number'}
<NumberBox label="Min Value" bind:value={draftField.minimum} />
<NumberBox label="Max Value" bind:value={draftField.maximum} />
{:else if draftField.type.startsWith('array')}
<Checkbox label="Required" bind:checked={required} />
{#if type === 'string'}
<NumberBox label="Max Length" bind:value={constraints.length.maximum} />
<ValuesList label="Categories" bind:values={constraints.inclusion} />
{:else if type === 'datetime'}
<!-- TODO: revisit and fix with JSON schema -->
<DatePicker
label="Min Value"
bind:value={constraints.datetime.earliest} />
<DatePicker label="Max Value" bind:value={constraints.datetime.latest} />
{:else if type === 'number'}
<NumberBox
label="Min Length"
bind:value={draftField.typeOptions.minLength} />
label="Min Value"
bind:value={constraints.numericality.greaterThanOrEqualTo} />
<NumberBox
label="Max Length"
bind:value={draftField.typeOptions.maxLength} />
label="Max Value"
bind:value={constraints.numericality.lessThanOrEqualTo} />
{/if}
</form>
</div>

View file

@ -8,10 +8,6 @@
import * as api from "../api"
import ErrorsBox from "components/common/ErrorsBox.svelte"
const CLASS_NAME_MAP = {
boolean: "uk-checkbox",
}
export let record = {}
export let onClosed
@ -28,14 +24,25 @@
onClosed()
}
const isSelect = meta =>
meta.type === "string" &&
meta.constraints &&
meta.constraints.inclusion &&
meta.constraints.inclusion.length > 0
function determineInputType(meta) {
if (meta.type === "datetime") return "date"
if (meta.type === "number") return "number"
if (meta.type === "boolean") return "checkbox"
if (isSelect(meta)) return "select"
return "text"
}
function determineOptions(meta) {
return isSelect(meta) ? meta.constraints.inclusion : []
}
async function saveRecord() {
const recordResponse = await api.saveRecord(
{
@ -46,7 +53,9 @@
$backendUiStore.selectedModel._id
)
if (recordResponse.errors) {
errors = recordResponse.errors
errors = Object.keys(recordResponse.errors)
.map(k => ({ dataPath: k, message: recordResponse.errors[k] }))
.flat()
return
}
@ -65,8 +74,8 @@
{#each modelSchema as [key, meta]}
<div class="uk-margin">
<RecordFieldControl
className={CLASS_NAME_MAP[meta.type]}
type={determineInputType(meta)}
options={determineOptions(meta)}
label={key}
bind:value={record[key]} />
</div>

View file

@ -3,10 +3,16 @@
export let value = ""
export let label
export let errors = []
export let className = "uk-input"
export let options = []
let checked = type === "checkbox" ? value : false
const determineClassName = type => {
if (type === "checkbox") return "uk-checkbox"
if (type === "select") return "uk-select"
return "uk-input"
}
const handleInput = event => {
if (event.target.type === "checkbox") {
value = event.target.checked
@ -23,11 +29,23 @@
</script>
<label>{label}</label>
{#if type === 'select'}
<select
class={determineClassName(type)}
bind:value
class:uk-form-danger={errors.length > 0}>
{#each options as opt}
<option value={opt}>{opt}</option>
{/each}
</select>
{:else}
<input
class={className}
class={determineClassName(type)}
class:uk-form-danger={errors.length > 0}
{checked}
{type}
{value}
on:input={handleInput}
on:change={handleInput} />
{/if}

View file

@ -45,7 +45,6 @@
<DatabasesList />
</div>
</div>
<hr />
{#if $backendUiStore.selectedDatabase._id}
<div class="hierarchy">
<div class="components-list-container">

View file

@ -0,0 +1,276 @@
<script>
import { MoreIcon } from "components/common/Icons"
import { store } from "builderStore"
import { getComponentDefinition } from "builderStore/store"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last, cloneDeep } from "lodash/fp"
import UIkit from "uikit"
import {
selectComponent,
getParent,
walkProps,
saveCurrentPreviewItem,
} from "builderStore/storeUtils"
import { uuid } from "builderStore/uuid"
export let component
let confirmDeleteDialog
let dropdownEl
$: dropdown = UIkit.dropdown(dropdownEl, {
mode: "click",
offset: 0,
pos: "bottom-right",
"delay-hide": 0,
animation: false,
})
$: dropdown && UIkit.util.on(dropdown, "shown", () => (hidden = false))
$: noChildrenAllowed =
!component ||
getComponentDefinition($store, component._component).children === false
$: noPaste =
!$store.componentToPaste || $store.componentToPaste._id === component._id
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
const hideDropdown = () => {
dropdown.hide()
}
const moveUpComponent = () => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === 0) return s
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex - 1, 0, component)
parent._children = newChildren
}
s.currentComponentInfo = component
saveCurrentPreviewItem(s)
return s
})
}
const moveDownComponent = () => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === parent._children.length - 1) return s
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex + 1, 0, component)
parent._children = newChildren
}
s.currentComponentInfo = component
saveCurrentPreviewItem(s)
return s
})
}
const copyComponent = () => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
const copiedComponent = cloneDeep(component)
walkProps(copiedComponent, p => {
p._id = uuid()
})
parent._children = [...parent._children, copiedComponent]
saveCurrentPreviewItem(s)
s.currentComponentInfo = copiedComponent
return s
})
}
const deleteComponent = () => {
store.update(state => {
const parent = getParent(state.currentPreviewItem.props, component)
if (parent) {
parent._children = parent._children.filter(c => c !== component)
}
saveCurrentPreviewItem(state)
return state
})
}
const generateNewIdsForComponent = c =>
walkProps(c, p => {
p._id = uuid()
})
const storeComponentForCopy = (cut = false) => {
store.update(s => {
const copiedComponent = cloneDeep(component)
s.componentToPaste = copiedComponent
if (cut) {
const parent = getParent(s.currentPreviewItem.props, component._id)
parent._children = parent._children.filter(c => c._id !== component._id)
selectComponent(s, parent)
}
return s
})
}
const pasteComponent = mode => {
store.update(s => {
if (!s.componentToPaste) return s
const componentToPaste = cloneDeep(s.componentToPaste)
generateNewIdsForComponent(componentToPaste)
delete componentToPaste._cutId
if (mode === "inside") {
component._children.push(componentToPaste)
return s
}
const parent = getParent(s.currentPreviewItem.props, component)
const targetIndex = parent._children.indexOf(component)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
saveCurrentPreviewItem(s)
selectComponent(s, componentToPaste)
return s
})
}
</script>
<div class="root" on:click|stopPropagation={() => {}}>
<button>
<MoreIcon />
</button>
<ul class="menu" bind:this={dropdownEl} on:click={hideDropdown}>
<li class="item" on:click={() => confirmDeleteDialog.show()}>
<i class="icon ri-delete-bin-2-line" />
Delete
</li>
<li class="item" on:click={moveUpComponent}>
<i class="icon ri-arrow-up-line" />
Move up
</li>
<li class="item" on:click={moveDownComponent}>
<i class="icon ri-arrow-down-line" />
Move down
</li>
<li class="item" on:click={copyComponent}>
<i class="icon ri-repeat-one-line" />
Duplicate
</li>
<li class="item" on:click={() => storeComponentForCopy(true)}>
<i class="icon ri-scissors-cut-line" />
Cut
</li>
<li class="item" on:click={() => storeComponentForCopy(false)}>
<i class="icon ri-file-copy-line" />
Copy
</li>
<hr class="hr-style" />
<li
class="item"
class:disabled={noPaste}
on:click={() => pasteComponent('above')}>
<i class="icon ri-insert-row-top" />
Paste above
</li>
<li
class="item"
class:disabled={noPaste}
on:click={() => pasteComponent('below')}>
<i class="icon ri-insert-row-bottom" />
Paste below
</li>
<li
class="item"
class:disabled={noPaste || noChildrenAllowed}
on:click={() => pasteComponent('inside')}>
<i class="icon ri-insert-column-right" />
Paste inside
</li>
</ul>
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(component)}' component?`}
okText="Delete Component"
onOk={deleteComponent} />
<style>
.root {
overflow: hidden;
z-index: 9;
}
.root button {
border-style: none;
border-radius: 2px;
padding: 5px;
background: transparent;
cursor: pointer;
color: var(--ink);
outline: none;
}
.menu {
z-index: 100000;
overflow: visible;
padding: 12px 0px;
border-radius: 5px;
}
.menu li {
border-style: none;
background-color: transparent;
list-style-type: none;
padding: 4px 16px;
margin: 0;
width: 100%;
box-sizing: border-box;
}
.item {
display: flex;
align-items: center;
font-size: 14px;
}
.icon {
margin-right: 8px;
}
.menu li:not(.disabled) {
cursor: pointer;
color: var(--ink-light);
}
.menu li:not(.disabled):hover {
color: var(--ink);
background-color: var(--grey-light);
}
.disabled {
color: var(--grey-dark);
cursor: default;
}
.hr-style {
margin: 8px 0;
color: var(--grey-dark);
}
</style>

View file

@ -104,6 +104,12 @@
height: 100%;
display: flex;
flex-direction: column;
/* Merge Check */
overflow-x: hidden;
overflow-y: hidden;
padding: 20px;
box-sizing: border-box;
/* Merge Check */
}
.title > div:nth-child(1) {
@ -118,5 +124,7 @@
.component-props-container {
margin-top: 20px;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
}
</style>

View file

@ -1,4 +1,5 @@
<script>
import { goto } from "@sveltech/routify"
import { splitName } from "./pagesParsing/splitRootComponentName.js"
import components from "./temporaryPanelStructure.js"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -32,7 +33,14 @@
const onComponentChosen = component => {
store.addChildComponent(component._component)
toggleTab()
toggleTab("Navigate")
// Get ID path
const path = store.getPathToComponent($store.currentComponentInfo)
// Go to correct URL
$goto(`./:page/:screen/${path}`)
}
</script>

View file

@ -1,7 +1,6 @@
<script>
import { params, goto } from "@sveltech/routify"
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import { last, sortBy, map, trimCharsStart, trimChars, join } from "lodash/fp"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { pipe } from "components/common/core"
@ -36,11 +35,6 @@
sortBy("title"),
])
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const changeScreen = screen => {
store.setCurrentScreen(screen.title)
$goto(`./:page/${screen.title}`)
@ -62,9 +56,7 @@
{/if}
</span>
<span class="icon">
<ShapeIcon />
</span>
<i class="ri-artboard-2-fill icon" />
<span class="title">{screen.title}</span>
</div>
@ -72,41 +64,32 @@
{#if $store.currentPreviewItem.name === screen.title && screen.component.props._children}
<ComponentsHierarchyChildren
components={screen.component.props._children}
currentComponent={$store.currentComponentInfo}
onDeleteComponent={confirmDeleteComponent}
onMoveUpComponent={store.moveUpComponent}
onMoveDownComponent={store.moveDownComponent}
onCopyComponent={store.copyComponent} />
currentComponent={$store.currentComponentInfo} />
{/if}
{/each}
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component?`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)} />
<style>
.root {
font-weight: 400;
color: #000333;
color: var(--ink);
}
.title {
margin-left: 10px;
margin-top: 2px;
font-size: 13px;
font-size: 14px;
font-weight: 400;
}
.icon {
display: inline-block;
transition: 0.2s;
font-size: 24px;
width: 20px;
margin-top: 2px;
color: #333;
color: var(--ink-light);
}
.icon:nth-of-type(2) {

View file

@ -3,6 +3,7 @@
import { store } from "builderStore"
import { last } from "lodash/fp"
import { pipe } from "components/common/core"
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import {
XCircleIcon,
ChevronUpIcon,
@ -14,23 +15,12 @@
export let currentComponent
export let onSelect = () => {}
export let level = 0
export let onDeleteComponent
export let onMoveUpComponent
export let onMoveDownComponent
export let onCopyComponent
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
const get_name = s => (!s ? "" : last(s.split("/")))
const get_capitalised_name = name => pipe(name, [get_name, capitalise])
const moveDownComponent = component => {
const c = component
return () => {
return onMoveDownComponent(c)
}
}
const selectComponent = component => {
// Set current component
store.selectComponent(component)
@ -50,31 +40,13 @@
class="budibase__nav-item item"
class:selected={currentComponent === component}
style="padding-left: {level * 20 + 53}px">
<div>{get_capitalised_name(component._component)}</div>
<div class="reorder-buttons">
{#if index > 0}
<button
class:solo={index === components.length - 1}
on:click|stopPropagation={() => onMoveUpComponent(component)}>
<ChevronUpIcon />
</button>
{/if}
{#if index < components.length - 1}
<button
class:solo={index === 0}
on:click|stopPropagation={moveDownComponent(component)}>
<ChevronDownIcon />
</button>
{/if}
<div class="nav-item">
<i class="icon ri-arrow-right-circle-fill" />
{get_capitalised_name(component._component)}
</div>
<div class="actions">
<ComponentDropdownMenu {component} />
</div>
<button
class="copy"
on:click|stopPropagation={() => onCopyComponent(component)}>
<CopyIcon />
</button>
<button on:click|stopPropagation={() => onDeleteComponent(component)}>
<XCircleIcon />
</button>
</div>
{#if component._children}
@ -82,11 +54,7 @@
components={component._children}
{currentComponent}
{onSelect}
level={level + 1}
{onDeleteComponent}
{onMoveUpComponent}
{onMoveDownComponent}
{onCopyComponent} />
level={level + 1} />
{/if}
</li>
{/each}
@ -107,50 +75,37 @@
border-radius: 3px;
height: 35px;
align-items: center;
font-weight: 400;
font-size: 13px;
}
.item button {
.actions {
display: none;
height: 20px;
width: 28px;
color: var(--slate);
height: 24px;
width: 24px;
color: var(--ink);
padding: 0px 5px;
border-style: none;
background: rgba(0, 0, 0, 0);
cursor: pointer;
}
.item button.copy {
width: 26px;
position: relative;
}
.item:hover {
background: #fafafa;
background: var(--grey-light);
cursor: pointer;
}
.item:hover button {
.item:hover .actions {
display: block;
}
.item:hover button:hover {
color: var(--button-text);
}
.reorder-buttons {
.nav-item {
display: flex;
flex-direction: column;
height: 100%;
align-items: center;
font-size: 14px;
color: var(--ink);
}
.reorder-buttons > button {
flex: 1 1 auto;
width: 30px;
height: 15px;
}
.reorder-buttons > button.solo {
padding-top: 2px;
.icon {
color: var(--ink-light);
margin-right: 8px;
}
</style>

View file

@ -0,0 +1,57 @@
<script>
import { store, backendUiStore } from "builderStore"
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
import NewScreen from "components/userInterface/NewScreen.svelte"
const newScreen = () => {
newScreenPicker.show()
}
let newScreenPicker
</script>
<PagesList />
<button class="newscreen" on:click={newScreen}>
<i class="icon ri-add-circle-fill" />
Create New Screen
</button>
<PageLayout layout={$store.pages[$store.currentPageName]} />
<div class="nav-items-container">
<ComponentsHierarchy screens={$store.screens} />
</div>
<NewScreen bind:this={newScreenPicker} />
<style>
.newscreen {
cursor: pointer;
border: 1px solid var(--grey-dark);
border-radius: 3px;
width: 100%;
padding: 8px 16px;
margin: 20px 0px 12px 0px;
display: flex;
justify-content: center;
align-items: center;
background: white;
color: var(--ink);
font-size: 14px;
font-weight: 500;
transition: all 2ms;
}
.newscreen:hover {
background: var(--grey-light);
}
.icon {
color: var(--ink);
font-size: 16px;
margin-right: 4px;
}
</style>

View file

@ -3,7 +3,7 @@
export let item
</script>
<div class="item-item" transition:fly={{ y: 100, duration: 1000 }} on:click>
<div class="item-item" in:fly={{ y: 100, duration: 1000 }} on:click>
<div class="item-icon">
<i class={item.icon} />
</div>

View file

@ -3,7 +3,6 @@
const dispatch = createEventDispatcher()
import Item from "./Item.svelte"
import { store } from "builderStore"
export let list
let category = list

View file

@ -34,19 +34,12 @@
title: lastPartOfName(layout),
}
const confirmDeleteComponent = async component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const setCurrentScreenToLayout = () => {
store.setScreenType("page")
$goto("./:page/page-layout")
}
</script>
<div class="pagelayoutSection">
<div class="components-nav-page">Page Layout</div>
<div
class="budibase__nav-item root"
class:selected={$store.currentComponentInfo._id === _layout.component.props._id}
@ -56,64 +49,36 @@
class:rotate={$store.currentPreviewItem.name !== _layout.title}>
<ArrowDownIcon />
</span>
<span class="icon">
<GridIcon />
</span>
<span class="title">Page Layout</span>
<i class="ri-layout-3-fill icon-big" />
<span class="title">Master Screen</span>
</div>
{#if $store.currentPreviewItem.name === _layout.title && _layout.component.props._children}
<ComponentsHierarchyChildren
thisComponent={_layout.component.props}
components={_layout.component.props._children}
currentComponent={$store.currentComponentInfo}
onDeleteComponent={confirmDeleteComponent}
onMoveUpComponent={store.moveUpComponent}
onMoveDownComponent={store.moveDownComponent}
onCopyComponent={store.copyComponent} />
currentComponent={$store.currentComponentInfo} />
{/if}
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component?`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)} />
<style>
.components-nav-page {
font-size: 13px;
color: #000333;
text-transform: uppercase;
margin-bottom: 10px;
padding-left: 20px;
font-weight: 600;
opacity: 0.4;
letter-spacing: 1px;
}
.pagelayoutSection {
margin: 20px 0px 20px 0px;
}
.title {
margin-left: 10px;
font-size: 13px;
font-size: 14px;
font-weight: 400;
color: var(--ink);
}
.icon {
width: 24px;
display: inline-block;
transition: 0.2s;
width: 20px;
margin-top: 2px;
color: #000333;
color: var(--ink-light);
}
.icon:nth-of-type(2) {
width: 14px;
margin: 0 0 0 5px;
.icon-big {
font-size: 24px;
color: var(--ink-light);
}
:global(svg) {

View file

@ -1,8 +1,6 @@
<script>
import { params, goto } from "@sveltech/routify"
import { store } from "builderStore"
import getIcon from "components/common/icon"
import { CheckIcon } from "components/common/Icons"
const getPage = (s, name) => {
const props = s.pages[name]
@ -20,6 +18,7 @@
},
]
if (!$store.currentPageName)
store.setCurrentPage($params.page ? $params.page : "main")
const changePage = id => {
@ -29,63 +28,37 @@
</script>
<div class="root">
<ul>
{#each pages as { title, id }}
<li>
<span class="icon">
{#if id === $params.page}
<CheckIcon />
{/if}
</span>
<button
class:active={id === $params.page}
on:click={() => changePage(id)}>
<button class:active={id === $params.page} on:click={() => changePage(id)}>
{title}
</button>
</li>
{/each}
</ul>
</div>
<style>
.root {
padding-bottom: 10px;
font-size: 0.9rem;
color: #000333;
font-weight: bold;
position: relative;
padding-left: 1.8rem;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
margin: 0.5rem 0;
display: flex;
flex-direction: row;
}
button {
margin: 0 0 0 6px;
padding: 0;
border: none;
font-family: Roboto;
font-size: 13px;
outline: none;
cursor: pointer;
background: rgba(0, 0, 0, 0);
padding: 8px 16px;
text-align: center;
background: #ffffff;
color: var(--ink-light);
border-radius: 5px;
font-family: Roboto;
font-size: 14px;
font-weight: 400;
transition: all 0.3s;
text-rendering: optimizeLegibility;
border: none !important;
transition: 0.2s;
}
.active {
font-weight: 500;
}
.icon {
display: inline-block;
width: 14px;
color: #000333;
background: var(--ink-light);
color: var(--white);
}
</style>

View file

@ -27,11 +27,6 @@
settingsView.show()
}
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script>
@ -42,7 +37,6 @@
<div class="pages-list-container">
<div class="nav-header">
<span class="navigator-title">Navigator</span>
<div class="border-line" />
<span class="components-nav-page">Pages</span>
</div>
@ -52,12 +46,8 @@
</div>
</div>
<div class="border-line" />
<PageLayout layout={$store.pages[$store.currentPageName]} />
<div class="border-line" />
<div class="components-list-container">
<div class="nav-group-header">
<span class="components-nav-header" style="margin-top: 0;">
@ -91,13 +81,6 @@
<NewScreen bind:this={newScreenPicker} />
<SettingsView bind:this={settingsView} />
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)} />
<style>
button {
cursor: pointer;
@ -112,22 +95,12 @@
padding: 0;
}
.root {
display: grid;
grid-template-columns: 275px 1fr 300px;
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;
}
background: #fbfbfb;
}
.ui-nav {
@ -135,7 +108,6 @@
background-color: var(--white);
height: calc(100vh - 49px);
padding: 0;
overflow: scroll;
display: flex;
flex-direction: column;
}
@ -229,10 +201,6 @@
letter-spacing: 1px;
}
.border-line {
border-bottom: 1px solid #d8d8d8;
}
.components-list-container {
padding: 20px 0px 0 0;
}

View file

@ -1,7 +1,7 @@
import Input from '../common/Input.svelte';
import OptionSelect from './OptionSelect.svelte';
import InputGroup from '../common/Inputs/InputGroup.svelte';
import FlatButtonGroup from './FlatButtonGroup.svelte';
import Input from "../common/Input.svelte"
import OptionSelect from "./OptionSelect.svelte"
import InputGroup from "../common/Inputs/InputGroup.svelte"
import FlatButtonGroup from "./FlatButtonGroup.svelte"
// import Colorpicker from "../common/Colorpicker.svelte"
/*
TODO: Allow for default values for all properties
@ -9,279 +9,290 @@ import FlatButtonGroup from './FlatButtonGroup.svelte';
export const layout = [
{
label: 'Display',
key: 'display',
label: "Display",
key: "display",
control: OptionSelect,
initialValue: 'Flex',
options: [ { label: 'Flex', value: 'flex' }, { label: 'Inline Flex', value: 'inline-flex' } ]
initialValue: "Flex",
options: [
{ label: "Flex", value: "flex" },
{ label: "Inline Flex", value: "inline-flex" },
],
},
{
label: 'Direction',
key: 'flex-direction',
label: "Direction",
key: "flex-direction",
control: FlatButtonGroup,
buttonProps: [
{ icon: 'ri-arrow-right-line', padding: '0px 5px', value: 'row' },
{ icon: 'ri-arrow-left-line', padding: '0px 5px', value: 'rowReverse' },
{ icon: 'ri-arrow-down-line', padding: '0px 5px', value: 'column' },
{ icon: "ri-arrow-right-line", padding: "0px 5px", value: "row" },
{ icon: "ri-arrow-left-line", padding: "0px 5px", value: "rowReverse" },
{ icon: "ri-arrow-down-line", padding: "0px 5px", value: "column" },
{
icon: 'ri-arrow-up-line',
padding: '0px 5px',
value: 'columnReverse'
}
]
icon: "ri-arrow-up-line",
padding: "0px 5px",
value: "columnReverse",
},
],
},
{
label: 'Justify',
key: 'justify-content',
label: "Justify",
key: "justify-content",
control: OptionSelect,
initialValue: 'Flex Start',
initialValue: "Flex Start",
options: [
{ label: 'Flex Start', value: 'flex-start' },
{ label: 'Flex End', value: 'flex-end' },
{ label: 'Center', value: 'center' },
{ label: 'Space Between', value: 'space-between' },
{ label: 'Space Around', value: 'space-around' },
{ label: 'Space Evenly', value: 'space-evenly' }
]
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
{ label: "Space Between", value: "space-between" },
{ label: "Space Around", value: "space-around" },
{ label: "Space Evenly", value: "space-evenly" },
],
},
{
label: 'Align',
key: 'align-items',
label: "Align",
key: "align-items",
control: OptionSelect,
initialValue: 'Flex Start',
initialValue: "Flex Start",
options: [
{ label: 'Flex Start', value: 'flex-start' },
{ label: 'Flex End', value: 'flex-end' },
{ label: 'Center', value: 'center' },
{ label: 'Baseline', value: 'baseline' },
{ label: 'Stretch', value: 'stretch' }
]
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
{ label: "Baseline", value: "baseline" },
{ label: "Stretch", value: "stretch" },
],
},
{
label: 'Wrap',
key: 'flex-wrap',
label: "Wrap",
key: "flex-wrap",
control: OptionSelect,
options: [ { label: 'wrap', value: 'wrap' }, { label: 'no wrap', value: 'noWrap' } ]
}
];
options: [
{ label: "wrap", value: "wrap" },
{ label: "no wrap", value: "noWrap" },
],
},
]
const spacingMeta = [ { placeholder: 'T' }, { placeholder: 'R' }, { placeholder: 'B' }, { placeholder: 'L' } ];
const spacingMeta = [
{ placeholder: "T" },
{ placeholder: "R" },
{ placeholder: "B" },
{ placeholder: "L" },
]
export const spacing = [
{
label: 'Margin',
key: 'margin',
label: "Margin",
key: "margin",
control: InputGroup,
meta: spacingMeta,
suffix: 'px',
defaultValue: [ '0', '0', '0', '0' ]
suffix: "px",
defaultValue: ["0", "0", "0", "0"],
},
{
label: 'Padding',
key: 'padding',
label: "Padding",
key: "padding",
control: InputGroup,
meta: spacingMeta,
suffix: 'px',
defaultValue: [ '0', '0', '0', '0' ]
}
];
suffix: "px",
defaultValue: ["0", "0", "0", "0"],
},
]
export const size = [
{
label: 'Width',
key: 'width',
label: "Width",
key: "width",
control: Input,
placeholder: 'px',
width: '48px',
textAlign: 'center'
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: 'Height',
key: 'height',
label: "Height",
key: "height",
control: Input,
placeholder: 'px',
width: '48px',
textAlign: 'center'
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: 'Min W',
key: 'min-width',
label: "Min W",
key: "min-width",
control: Input,
placeholder: 'px',
width: '48px',
textAlign: 'center'
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: 'Min H',
key: 'min-height',
label: "Min H",
key: "min-height",
control: Input,
placeholder: 'px',
width: '48px',
textAlign: 'center'
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: 'Max W',
key: 'max-width',
label: "Max W",
key: "max-width",
control: Input,
placeholder: 'px',
width: '48px',
textAlign: 'center'
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: 'Max H',
key: 'max-height',
label: "Max H",
key: "max-height",
control: Input,
placeholder: 'px',
width: '48px',
textAlign: 'center'
}
];
placeholder: "px",
width: "48px",
textAlign: "center",
},
]
export const position = [
{
label: 'Position',
key: 'position',
label: "Position",
key: "position",
control: OptionSelect,
initialValue: 'Wrap',
initialValue: "Wrap",
options: [
{ label: 'Static', value: 'static' },
{ label: 'Relative', value: 'relative' },
{ label: 'Fixed', value: 'fixed' },
{ label: 'Absolute', value: 'absolute' },
{ label: 'Sticky', value: 'sticky' }
{ label: "Static", value: "static" },
{ label: "Relative", value: "relative" },
{ label: "Fixed", value: "fixed" },
{ label: "Absolute", value: "absolute" },
{ label: "Sticky", value: "sticky" },
],
},
{
label: "Top",
key: "top",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Right",
key: "right",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Bottom",
key: "bottom",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Left",
key: "left",
control: Input,
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: "Z-index",
key: "z-index",
control: Input,
placeholder: "num",
width: "48px",
textAlign: "center",
},
]
},
{
label: 'Top',
key: 'top',
control: Input,
placeholder: 'px',
width: '48px',
textAlign: 'center'
},
{
label: 'Right',
key: 'right',
control: Input,
placeholder: 'px',
width: '48px',
textAlign: 'center'
},
{
label: 'Bottom',
key: 'bottom',
control: Input,
placeholder: 'px',
width: '48px',
textAlign: 'center'
},
{
label: 'Left',
key: 'left',
control: Input,
placeholder: 'px',
width: '48px',
textAlign: 'center'
},
{
label: 'Z-index',
key: 'z-index',
control: Input,
placeholder: 'num',
width: '48px',
textAlign: 'center'
}
];
export const typography = [
{
label: 'Font',
key: 'font-family',
label: "Font",
key: "font-family",
control: OptionSelect,
defaultValue: 'initial',
defaultValue: "initial",
options: [
'initial',
'Arial',
'Arial Black',
'Cursive',
'Courier',
'Comic Sans MS',
'Helvetica',
'Impact',
'Inter',
'Lucida Sans Unicode',
'Open Sans',
'Playfair',
'Roboto',
'Roboto Mono',
'Times New Roman',
'Verdana'
"initial",
"Arial",
"Arial Black",
"Cursive",
"Courier",
"Comic Sans MS",
"Helvetica",
"Impact",
"Inter",
"Lucida Sans Unicode",
"Open Sans",
"Playfair",
"Roboto",
"Roboto Mono",
"Times New Roman",
"Verdana",
],
styleBindingProperty: 'font-family'
styleBindingProperty: "font-family",
},
{
label: 'Weight',
key: 'font-weight',
label: "Weight",
key: "font-weight",
control: OptionSelect,
options: [ 'normal', 'bold', 'bolder', 'lighter' ]
options: ["normal", "bold", "bolder", "lighter"],
},
{
label: 'size',
key: 'font-size',
defaultValue: '',
label: "size",
key: "font-size",
defaultValue: "",
control: Input,
placeholder: 'px',
width: '48px',
textAlign: 'center'
placeholder: "px",
width: "48px",
textAlign: "center",
},
{
label: 'Line H',
key: 'line-height',
label: "Line H",
key: "line-height",
control: Input,
placeholder: 'lh',
width: '48px',
textAlign: 'center'
placeholder: "lh",
width: "48px",
textAlign: "center",
},
{
label: 'Color',
key: 'color',
label: "Color",
key: "color",
control: Input,
placeholder: "hex",
},
{
label: 'align',
key: 'text-align',
label: "align",
key: "text-align",
control: FlatButtonGroup,
buttonProps: [
{ icon: 'ri-align-left', padding: '0px 5px', value: 'left' },
{ icon: 'ri-align-center', padding: '0px 5px', value: 'center' },
{ icon: 'ri-align-right', padding: '0px 5px', value: 'right' },
{ icon: 'ri-align-justify', padding: '0px 5px', value: 'justify' }
]
{ icon: "ri-align-left", padding: "0px 5px", value: "left" },
{ icon: "ri-align-center", padding: "0px 5px", value: "center" },
{ icon: "ri-align-right", padding: "0px 5px", value: "right" },
{ icon: "ri-align-justify", padding: "0px 5px", value: "justify" },
],
},
{
label: 'transform',
key: 'text-transform',
label: "transform",
key: "text-transform",
control: FlatButtonGroup,
buttonProps: [
{ text: 'BB', padding: '0px 5px', fontWeight: 500, value: 'uppercase' },
{ text: 'Bb', padding: '0px 5px', fontWeight: 500, value: 'capitalize' },
{ text: 'bb', padding: '0px 5px', fontWeight: 500, value: 'lowercase' },
{ text: "BB", padding: "0px 5px", fontWeight: 500, value: "uppercase" },
{ text: "Bb", padding: "0px 5px", fontWeight: 500, value: "capitalize" },
{ text: "bb", padding: "0px 5px", fontWeight: 500, value: "lowercase" },
{
text: '&times;',
padding: '0px 5px',
text: "&times;",
padding: "0px 5px",
fontWeight: 500,
value: 'none'
}
]
value: "none",
},
{ label: 'style', key: 'font-style', control: Input }
];
],
},
{ label: "style", key: "font-style", control: Input },
]
export const background = [
{
label: 'Background',
key: 'background',
label: "Background",
key: "background",
control: Input,
},
{
@ -290,54 +301,65 @@ export const background = [
control: Input,
placeholder: "src",
},
];
]
export const border = [
{
label: 'Radius',
key: 'border-radius',
label: "Radius",
key: "border-radius",
control: Input,
width: '48px',
placeholder: 'px',
textAlign: 'center'
width: "48px",
placeholder: "px",
textAlign: "center",
},
{
label: 'Width',
key: 'border-width',
label: "Width",
key: "border-width",
control: Input,
width: '48px',
placeholder: 'px',
textAlign: 'center'
width: "48px",
placeholder: "px",
textAlign: "center",
}, //custom
{
label: 'Color',
key: 'border-color',
control: Input
label: "Color",
key: "border-color",
control: Input,
},
{
label: 'Style',
key: 'border-style',
label: "Style",
key: "border-style",
control: OptionSelect,
options: [ 'none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset' ]
}
];
options: [
"none",
"hidden",
"dotted",
"dashed",
"solid",
"double",
"groove",
"ridge",
"inset",
"outset",
],
},
]
export const effects = [
{
label: 'Opacity',
key: 'opacity',
label: "Opacity",
key: "opacity",
control: Input,
width: '48px',
textAlign: 'center',
placeholder: '%'
width: "48px",
textAlign: "center",
placeholder: "%",
},
{
label: 'Rotate',
key: 'transform',
label: "Rotate",
key: "transform",
control: Input,
width: '48px',
textAlign: 'center',
placeholder: 'deg'
width: "48px",
textAlign: "center",
placeholder: "deg",
}, //needs special control
{
label: "Shadow",
@ -345,44 +367,44 @@ export const effects = [
control: InputGroup,
meta: [{ placeholder: "X" }, { placeholder: "Y" }, { placeholder: "B" }],
},
];
]
export const transitions = [
{
label: 'Property',
key: 'transition-property',
label: "Property",
key: "transition-property",
control: OptionSelect,
options: [
'None',
'All',
'Background Color',
'Color',
'Font Size',
'Font Weight',
'Height',
'Margin',
'Opacity',
'Padding',
'Rotate',
'Shadow',
'Width'
]
"None",
"All",
"Background Color",
"Color",
"Font Size",
"Font Weight",
"Height",
"Margin",
"Opacity",
"Padding",
"Rotate",
"Shadow",
"Width",
],
},
{
label: 'Duration',
key: 'transition-timing-function',
label: "Duration",
key: "transition-timing-function",
control: Input,
width: '48px',
textAlign: 'center',
placeholder: 'sec'
width: "48px",
textAlign: "center",
placeholder: "sec",
},
{
label: 'Ease',
key: 'transition-ease',
label: "Ease",
key: "transition-ease",
control: OptionSelect,
options: [ 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out' ]
}
];
options: ["linear", "ease", "ease-in", "ease-out", "ease-in-out"],
},
]
export const all = {
layout,
@ -393,15 +415,15 @@ export const all = {
background,
border,
effects,
transitions
};
transitions,
}
export function excludeProps(props, propsToExclude) {
const modifiedProps = {};
const modifiedProps = {}
for (const prop in props) {
if (!propsToExclude.includes(prop)) {
modifiedProps[prop] = props[prop];
modifiedProps[prop] = props[prop]
}
}
return modifiedProps;
return modifiedProps
}

View file

@ -88,6 +88,8 @@ html, body {
#app {
height: 100%;
box-sizing: border-box;
overflow-y: hidden;
}
h1 {

View file

@ -81,7 +81,7 @@
<style>
.root {
height: 100%;
min-height: 100%;
width: 100%;
display: flex;
flex-direction: column;

View file

@ -31,19 +31,10 @@
margin: 20px 40px;
}
.nav {
overflow: auto;
flex: 0 1 auto;
width: 275px;
height: 100%;
}
@media only screen and (min-width: 1800px) {
.nav {
overflow: auto;
flex: 0 1 auto;
width: 300px;
height: 100%;
}
}
</style>

View file

@ -2,18 +2,17 @@
import { store, backendUiStore } from "builderStore"
import { goto } from "@sveltech/routify"
import { onMount } from "svelte"
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte"
import ComponentsHierarchyChildren from "components/userInterface/ComponentsHierarchyChildren.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
import IconButton from "components/common/IconButton.svelte"
import NewScreen from "components/userInterface/NewScreen.svelte"
import CurrentItemPreview from "components/userInterface/AppPreview"
import PageView from "components/userInterface/PageView.svelte"
import ComponentsPaneSwitcher from "components/userInterface/ComponentsPaneSwitcher.svelte"
import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte"
import Switcher from "components/common/Switcher.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last } from "lodash/fp"
import { AddIcon } from "components/common/Icons"
import FrontendNavigatePane from "components/userInterface/FrontendNavigatePane.svelte"
$: instances = $store.appInstances
@ -27,23 +26,15 @@
}
})
let newScreenPicker
let confirmDeleteDialog
let componentToDelete = ""
const newScreen = () => {
newScreenPicker.show()
}
let settingsView
const settings = () => {
settingsView.show()
}
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
let leftNavSwitcher
const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script>
@ -52,92 +43,42 @@
<div class="ui-nav">
<div class="pages-list-container">
<div class="nav-header">
<span class="navigator-title">Navigate</span>
<span class="components-nav-page">Pages</span>
</div>
<div class="nav-items-container">
<PagesList />
</div>
</div>
<div class="border-line" />
<PageLayout layout={$store.pages[$store.currentPageName]} />
<div class="border-line" />
<div class="components-list-container">
<div class="nav-group-header">
<span class="components-nav-header" style="margin-top: 0;">
Screens
</span>
<div>
<button on:click={newScreen}>
<AddIcon />
</button>
</div>
</div>
<div class="nav-items-container">
<ComponentsHierarchy screens={$store.screens} />
<Switcher bind:this={leftNavSwitcher} tabs={['Navigate', 'Add']}>
<div slot="0">
<FrontendNavigatePane />
</div>
<div slot="1">
<ComponentSelectionList toggleTab={leftNavSwitcher.selectTab} />
</div>
</Switcher>
</div>
<div class="preview-pane">
{#if $store.currentPageName && $store.currentPageName.length > 0}
<CurrentItemPreview />
{/if}
</div>
{#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'}
<div class="components-pane">
<ComponentsPaneSwitcher />
<ComponentPropertiesPanel />
</div>
{/if}
</div>
<NewScreen bind:this={newScreenPicker} />
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)} />
<slot />
<style>
button {
cursor: pointer;
outline: none;
border: none;
border-radius: 5px;
width: 20px;
padding-bottom: 10px;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
}
.root {
display: grid;
grid-template-columns: 275px 1fr 275px;
width: 100%;
background: var(--grey-light);
}
@media only screen and (min-width: 1800px) {
.root {
display: grid;
grid-template-columns: 300px 1fr 300px;
width: 100%;
background: var(--grey-light);
}
flex: 1;
min-height: 0;
align-items: stretch;
}
.ui-nav {
@ -145,9 +86,9 @@
background-color: var(--white);
height: calc(100vh - 69px);
padding: 0;
overflow: scroll;
display: flex;
flex-direction: column;
z-index: 5;
}
.preview-pane {
@ -201,6 +142,7 @@
font-weight: bold;
justify-content: space-between;
align-items: center;
min-height: 0;
}
.nav-group-header > div:nth-child(1) {
@ -210,13 +152,6 @@
margin-right: 5px;
}
.nav-group-header > span:nth-child(3) {
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;
@ -227,19 +162,4 @@
.nav-group-header > div:nth-child(3):hover {
color: var(--primary75);
}
.navigator-title {
font-size: 18px;
color: var(--ink);
font-weight: bold;
padding: 0 20px 20px 20px;
}
.border-line {
border-bottom: 1px solid #d8d8d8;
}
.components-list-container {
padding: 20px 0px 0 0;
}
</style>

View file

@ -117,15 +117,6 @@
</Modal>
<style>
.root {
display: grid;
grid-template-columns: 275px 1fr;
height: 100%;
width: 100%;
background: var(--grey-light);
}
@media only screen and (min-width: 1800px) {
.root {
display: grid;
grid-template-columns: 300px 1fr;
@ -133,7 +124,6 @@
width: 100%;
background: var(--grey-light);
}
}
.main {
grid-column: 2;
@ -207,7 +197,7 @@
.nav-item-title {
font-size: 14px;
color: var(--ink);
font-weight: 500;
font-weight: 400;
margin-left: 12px;
}

View file

@ -1,14 +1,10 @@
import {
generate_css,
generate_screen_css,
generate_array_styles
} from "../src/builderStore/generate_css.js"
describe("generate_css", () => {
test("Check how partially empty arrays are handled", () => {
expect(["", "5", "", ""].map(generate_array_styles)).toEqual(["0px", "5px", "0px", "0px"])
})
test("Check how array styles are output", () => {
expect(generate_css({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0px 10px 0px 15px;")

View file

@ -13,3 +13,6 @@ JWT_SECRET={{cookieKey1}}
# port to run http server on
PORT=4001
# error level for koa-pino
LOG_LEVEL=error

View file

@ -44,7 +44,6 @@
"@budibase/client": "^0.0.32",
"@budibase/core": "^0.0.32",
"@koa/router": "^8.0.0",
"ajv": "^6.12.2",
"bcryptjs": "^2.4.3",
"dotenv": "^8.2.0",
"electron-is-dev": "^1.2.0",
@ -66,6 +65,7 @@
"squirrelly": "^7.5.0",
"tar-fs": "^2.0.0",
"uuid": "^3.3.2",
"validate.js": "^0.13.1",
"yargs": "^13.2.4",
"zlib": "^1.0.5"
},

View file

@ -5,3 +5,4 @@ process.env.JWT_SECRET = "test-jwtsecret"
process.env.CLIENT_ID = "test-client-id"
process.env.BUDIBASE_DIR = tmpdir("budibase-unittests")
process.env.ADMIN_SECRET = "test-admin-secret"
process.env.LOG_LEVEL = "silent"

View file

@ -1,9 +1,7 @@
const CouchDB = require("../../db")
const Ajv = require("ajv")
const validateJs = require("validate.js")
const newid = require("../../db/newid")
const ajv = new Ajv()
exports.save = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const record = ctx.request.body
@ -13,18 +11,18 @@ exports.save = async function(ctx) {
record._id = newid()
}
// validation with ajv
const model = await db.get(record.modelId)
const validate = ajv.compile({
properties: model.schema,
})
const valid = validate(record)
if (!valid) {
const validateResult = await validate({
record,
model,
})
if (!validateResult.valid) {
ctx.status = 400
ctx.body = {
status: 400,
errors: validate.errors,
errors: validateResult.errors,
}
return
}
@ -84,3 +82,29 @@ exports.destroy = async function(ctx) {
}
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
}
exports.validate = async function(ctx) {
const errors = await validate({
instanceId: ctx.params.instanceId,
modelId: ctx.params.modelId,
record: ctx.request.body,
})
ctx.status = 200
ctx.body = errors
}
async function validate({ instanceId, modelId, record, model }) {
if (!model) {
const db = new CouchDB(instanceId)
model = await db.get(modelId)
}
const errors = {}
for (let fieldName in model.schema) {
const res = validateJs.single(
record[fieldName],
model.schema[fieldName].constraints
)
if (res) errors[fieldName] = res
}
return { valid: Object.keys(errors).length === 0, errors }
}

View file

@ -28,6 +28,11 @@ router
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.save
)
.post(
"/api/:instanceId/:modelId/records/validate",
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.validate
)
.delete(
"/api/:instanceId/:modelId/records/:recordId/:revId",
authorized(WRITE_MODEL, ctx => ctx.params.modelId),

View file

@ -27,7 +27,12 @@ exports.createModel = async (request, instanceId, model) => {
type: "model",
key: "name",
schema: {
name: { type: "string" },
name: {
type: "text",
constraints: {
type: "string",
},
},
},
}

View file

@ -25,8 +25,6 @@ describe("/records", () => {
server.close();
})
describe("save, load, update, delete", () => {
beforeEach(async () => {
instance = await createInstance(request, app._id)
model = await createModel(request, instance._id)
@ -37,6 +35,8 @@ describe("/records", () => {
}
})
describe("save, load, update, delete", () => {
const createRecord = async r =>
await request
.post(`/api/${instance._id}/${model._id}/records`)
@ -119,4 +119,31 @@ describe("/records", () => {
.expect(404)
})
})
describe("validate", () => {
it("should return no errors on valid record", async () => {
const result = await request
.post(`/api/${instance._id}/${model._id}/records/validate`)
.send({ name: "ivan" })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(result.body.valid).toBe(true)
expect(Object.keys(result.body.errors)).toEqual([])
})
it("should errors on invalid record", async () => {
const result = await request
.post(`/api/${instance._id}/${model._id}/records/validate`)
.send({ name: 1 })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(result.body.valid).toBe(false)
expect(Object.keys(result.body.errors)).toEqual(["name"])
})
})
})

View file

@ -15,7 +15,7 @@ app.use(
prettyPrint: {
levelFirst: true,
},
level: process.env.NODE_ENV === "jest" ? "silent" : "info",
level: env.LOG_LEVEL || "error",
})
)

View file

@ -194,6 +194,20 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@budibase/client@^0.0.32":
version "0.0.32"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.0.32.tgz#76d9f147563a0bf939eae7f32ce75b2a527ba496"
integrity sha512-jmCCLn0CUoQbL6h623S5IqK6+GYLqX3WzUTZInSb1SCBOM3pI0eLP5HwTR6s7r42SfD0v9jTWRdyTnHiElNj8A==
dependencies:
"@nx-js/compiler-util" "^2.0.0"
bcryptjs "^2.4.3"
deep-equal "^2.0.1"
lodash "^4.17.15"
lunr "^2.3.5"
regexparam "^1.3.0"
shortid "^2.2.8"
svelte "^3.9.2"
"@budibase/core@^0.0.32":
version "0.0.32"
resolved "https://registry.yarnpkg.com/@budibase/core/-/core-0.0.32.tgz#c5d9ab869c5e9596a1ac337aaf041e795b1cc7fa"
@ -655,7 +669,7 @@ ajv-keywords@^3.4.1:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da"
integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.12.2:
ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0:
version "6.12.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
@ -825,6 +839,11 @@ array-equal@^1.0.0:
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
array-filter@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
array-unique@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
@ -897,6 +916,13 @@ atomic-sleep@^1.0.0:
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==
dependencies:
array-filter "^1.0.0"
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@ -1612,6 +1638,26 @@ decompress-response@^3.3.0:
dependencies:
mimic-response "^1.0.0"
deep-equal@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0"
integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA==
dependencies:
es-abstract "^1.17.5"
es-get-iterator "^1.1.0"
is-arguments "^1.0.4"
is-date-object "^1.0.2"
is-regex "^1.0.5"
isarray "^2.0.5"
object-is "^1.1.2"
object-keys "^1.1.1"
object.assign "^4.1.0"
regexp.prototype.flags "^1.3.0"
side-channel "^1.0.2"
which-boxed-primitive "^1.0.1"
which-collection "^1.0.1"
which-typed-array "^1.1.2"
deep-equal@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@ -1965,7 +2011,7 @@ error-inject@^1.0.0:
resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37"
integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=
es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5:
version "1.17.5"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==
@ -1982,6 +2028,19 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
string.prototype.trimleft "^2.1.1"
string.prototype.trimright "^2.1.1"
es-get-iterator@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8"
integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==
dependencies:
es-abstract "^1.17.4"
has-symbols "^1.0.1"
is-arguments "^1.0.4"
is-map "^2.0.1"
is-set "^2.0.1"
is-string "^1.0.5"
isarray "^2.0.5"
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
@ -2967,11 +3026,21 @@ is-accessor-descriptor@^1.0.0:
dependencies:
kind-of "^6.0.0"
is-arguments@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
is-bigint@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4"
integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@ -2979,6 +3048,11 @@ is-binary-path@~2.1.0:
dependencies:
binary-extensions "^2.0.0"
is-boolean-object@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@ -3015,7 +3089,7 @@ is-data-descriptor@^1.0.0:
dependencies:
kind-of "^6.0.0"
is-date-object@^1.0.1:
is-date-object@^1.0.1, is-date-object@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
@ -3090,11 +3164,21 @@ is-installed-globally@^0.3.1:
global-dirs "^2.0.1"
is-path-inside "^3.0.1"
is-map@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
is-npm@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d"
integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==
is-number-object@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@ -3131,11 +3215,21 @@ is-regex@^1.0.5:
dependencies:
has "^1.0.3"
is-set@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
is-string@^1.0.4, is-string@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
is-symbol@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
@ -3152,11 +3246,31 @@ is-type-of@^1.0.0:
is-class-hotfix "~0.0.6"
isstream "~0.1.2"
is-typed-array@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d"
integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==
dependencies:
available-typed-arrays "^1.0.0"
es-abstract "^1.17.4"
foreach "^2.0.5"
has-symbols "^1.0.1"
is-typedarray@^1.0.0, is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-weakmap@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
is-weakset@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
@ -3182,6 +3296,11 @@ isarray@1.0.0, isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isbinaryfile@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b"
@ -4589,6 +4708,14 @@ object-inspect@^1.7.0:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
object-is@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.17.5"
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
@ -5310,6 +5437,19 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
regexp.prototype.flags@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.17.0-next.1"
regexparam@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-1.3.0.tgz#2fe42c93e32a40eff6235d635e0ffa344b92965f"
integrity sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==
regexpp@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
@ -5638,6 +5778,14 @@ shortid@^2.2.8:
dependencies:
nanoid "^2.1.0"
side-channel@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947"
integrity sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==
dependencies:
es-abstract "^1.17.0-next.1"
object-inspect "^1.7.0"
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@ -6046,6 +6194,11 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
svelte@^3.9.2:
version "3.23.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.23.0.tgz#bbcd6887cf588c24a975b14467455abfff9acd3f"
integrity sha512-cnyd96bK/Nw5DnYuB1hzm5cl6+I1fpmdKOteA7KLzU9KGLsLmvWsSkSKbcntzODCLmSySN3HjcgTHRo6/rJNTw==
symbol-tree@^3.2.2:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@ -6458,6 +6611,11 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
validate.js@^0.13.1:
version "0.13.1"
resolved "https://registry.yarnpkg.com/validate.js/-/validate.js-0.13.1.tgz#b58bfac04a0f600a340f62e5227e70d95971e92a"
integrity sha512-PnFM3xiZ+kYmLyTiMgTYmU7ZHkjBZz2/+F0DaALc/uUtVzdCt1wAosvYJ5hFQi/hz8O4zb52FQhHZRC+uVkJ+g==
vary@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@ -6526,11 +6684,44 @@ whatwg-url@^7.0.0:
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
which-boxed-primitive@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"
integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==
dependencies:
is-bigint "^1.0.0"
is-boolean-object "^1.0.0"
is-number-object "^1.0.3"
is-string "^1.0.4"
is-symbol "^1.0.2"
which-collection@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
dependencies:
is-map "^2.0.1"
is-set "^2.0.1"
is-weakmap "^2.0.1"
is-weakset "^2.0.1"
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which-typed-array@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2"
integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==
dependencies:
available-typed-arrays "^1.0.2"
es-abstract "^1.17.5"
foreach "^2.0.5"
function-bind "^1.1.1"
has-symbols "^1.0.1"
is-typed-array "^1.1.3"
which@^1.2.9, which@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"