diff --git a/packages/builder/package.json b/packages/builder/package.json index e4b9040088..8645f285af 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -112,7 +112,7 @@ "rollup-plugin-terser": "^7.0.2", "rollup-plugin-url": "^2.2.2", "start-server-and-test": "^1.11.0", - "svelte": "^3.24.1", + "svelte": "^3.29.0", "svelte-jester": "^1.0.6" }, "gitHead": "115189f72a850bfb52b65ec61d932531bf327072" diff --git a/packages/builder/src/builderStore/getNewComponentName.js b/packages/builder/src/builderStore/getNewComponentName.js index b3ddc4e953..9ab8ef16dc 100644 --- a/packages/builder/src/builderStore/getNewComponentName.js +++ b/packages/builder/src/builderStore/getNewComponentName.js @@ -8,8 +8,9 @@ export default function(component, state) { const findMatches = props => { walkProps(props, c => { - if ((c._instanceName || "").startsWith(capitalised)) { - matchingComponents.push(c._instanceName) + const thisInstanceName = get_capitalised_name(c._instanceName) + if ((thisInstanceName || "").startsWith(capitalised)) { + matchingComponents.push(thisInstanceName) } }) } diff --git a/packages/builder/src/builderStore/store/index.js b/packages/builder/src/builderStore/store/index.js index 70b88eb778..6941c74b22 100644 --- a/packages/builder/src/builderStore/store/index.js +++ b/packages/builder/src/builderStore/store/index.js @@ -124,17 +124,18 @@ const saveScreen = store => screen => { } const _saveScreen = async (store, s, screen) => { - const currentPageScreens = s.pages[s.currentPageName]._screens + const pageName = s.currentPageName || "main" + const currentPageScreens = s.pages[pageName]._screens await api - .post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen) + .post(`/_builder/api/${s.appId}/pages/${pageName}/screen`, screen) .then(() => { if (currentPageScreens.includes(screen)) return const screens = [...currentPageScreens, screen] store.update(innerState => { - innerState.pages[s.currentPageName]._screens = screens + innerState.pages[pageName]._screens = screens innerState.screens = screens innerState.currentPreviewItem = screen innerState.allScreens = [...innerState.allScreens, screen] @@ -153,27 +154,17 @@ const _saveScreen = async (store, s, screen) => { return s } -const createScreen = store => (screenName, route, layoutComponentName) => { +const createScreen = store => async screen => { + let savePromise store.update(state => { - const rootComponent = state.components[layoutComponentName] - - const newScreen = { - description: "", - url: "", - _css: "", - props: createProps(rootComponent).props, - } - newScreen.route = route - newScreen.name = newScreen.props._id - newScreen.props._instanceName = screenName || "" - state.currentPreviewItem = newScreen - state.currentComponentInfo = newScreen.props + state.currentPreviewItem = screen + state.currentComponentInfo = screen.props state.currentFrontEndType = "screen" - - _saveScreen(store, state, newScreen) - + savePromise = _saveScreen(store, state, screen) + regenerateCssForCurrentScreen(state) return state }) + await savePromise } const setCurrentScreen = store => screenName => { diff --git a/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js b/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js new file mode 100644 index 0000000000..a8ab27df3d --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js @@ -0,0 +1,22 @@ +export default { + name: `Create from scratch`, + create: () => createScreen(), +} + +const createScreen = () => ({ + props: { + _id: "", + _component: "@budibase/standard-components/container", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + type: "div", + _children: [], + _instanceName: "", + }, + route: "", + name: "screen-id", +}) diff --git a/packages/builder/src/builderStore/store/screenTemplates/emptyNewRecordScreen.js b/packages/builder/src/builderStore/store/screenTemplates/emptyNewRecordScreen.js new file mode 100644 index 0000000000..db6910809f --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/emptyNewRecordScreen.js @@ -0,0 +1,22 @@ +export default { + name: `New Row (Empty)`, + create: () => createScreen(), +} + +const createScreen = () => ({ + props: { + _id: "", + _component: "@budibase/standard-components/newrow", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + _children: [], + _instanceName: "", + model: "", + }, + route: "", + name: "screen-id", +}) diff --git a/packages/builder/src/builderStore/store/screenTemplates/emptyRecordDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/emptyRecordDetailScreen.js new file mode 100644 index 0000000000..5c0b31a2df --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/emptyRecordDetailScreen.js @@ -0,0 +1,22 @@ +export default { + name: `Row Detail (Empty)`, + create: () => createScreen(), +} + +const createScreen = () => ({ + props: { + _id: "", + _component: "@budibase/standard-components/rowdetail", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + _children: [], + _instanceName: "", + model: "", + }, + route: "", + name: "screen-id", +}) diff --git a/packages/builder/src/builderStore/store/screenTemplates/index.js b/packages/builder/src/builderStore/store/screenTemplates/index.js new file mode 100644 index 0000000000..55a93c6e4d --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/index.js @@ -0,0 +1,35 @@ +import newRecordScreen from "./newRecordScreen" +import recordDetailScreen from "./recordDetailScreen" +import recordListScreen from "./recordListScreen" +import emptyNewRecordScreen from "./emptyNewRecordScreen" +import createFromScratchScreen from "./createFromScratchScreen" +import emptyRecordDetailScreen from "./emptyRecordDetailScreen" +import { generateNewIdsForComponent } from "../../storeUtils" +import { uuid } from "builderStore/uuid" + +const allTemplates = models => [ + createFromScratchScreen, + ...newRecordScreen(models), + ...recordDetailScreen(models), + ...recordListScreen(models), + emptyNewRecordScreen, + emptyRecordDetailScreen, +] + +// allows us to apply common behaviour to all create() functions +const createTemplateOverride = (frontendState, create) => () => { + const screen = create() + for (let component of screen.props._children) { + generateNewIdsForComponent(component, frontendState, false) + } + screen.props._id = uuid() + screen.name = screen.props._id + screen.route = screen.route.toLowerCase() + return screen +} + +export default (frontendState, models) => + allTemplates(models).map(template => ({ + ...template, + create: createTemplateOverride(frontendState, template.create), + })) diff --git a/packages/builder/src/builderStore/store/screenTemplates/newRecordScreen.js b/packages/builder/src/builderStore/store/screenTemplates/newRecordScreen.js new file mode 100644 index 0000000000..bbe9b0d911 --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/newRecordScreen.js @@ -0,0 +1,135 @@ +export default function(models) { + return models.map(model => { + const fields = Object.keys(model.schema) + const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Add Row" + return { + name: `${model.name} - New`, + create: () => createScreen(model, heading), + id: NEW_RECORD_TEMPLATE, + } + }) +} + +export const NEW_RECORD_TEMPLATE = "NEW_RECORD_TEMPLATE" + +const createScreen = (model, heading) => ({ + props: { + _id: "", + _component: "@budibase/standard-components/newrow", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + model: model._id, + _children: [ + { + _id: "", + _component: "@budibase/standard-components/heading", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + className: "", + text: heading, + type: "h1", + _instanceName: "Heading 1", + _children: [], + }, + { + _id: "", + _component: "@budibase/standard-components/dataform", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + _instanceName: `${model.name} Form`, + _children: [], + }, + { + _id: "", + _component: "@budibase/standard-components/container", + _styles: { + normal: { + display: "flex", + "flex-direction": "row", + "align-items": "center", + "justify-content": "flex-end", + }, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + className: "", + onLoad: [], + type: "div", + _instanceName: "Buttons Container", + _children: [ + { + _id: "", + _component: "@budibase/standard-components/button", + _styles: { + normal: { + "margin-right": "20px", + }, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + text: "Back", + className: "", + disabled: false, + onClick: [ + { + parameters: { + url: `/${model.name.toLowerCase()}`, + }, + "##eventHandlerType": "Navigate To", + }, + ], + _instanceName: "Back Button", + _children: [], + }, + { + _id: "", + _component: "@budibase/standard-components/button", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + text: "Save", + className: "", + disabled: false, + onClick: [ + { + parameters: { + contextPath: "data", + modelId: model._id, + }, + "##eventHandlerType": "Save Record", + }, + ], + _instanceName: "Save Button", + _children: [], + }, + ], + }, + ], + _instanceName: `${model.name} - New`, + _code: "", + }, + route: `/${model.name.toLowerCase()}/new`, + name: "", +}) diff --git a/packages/builder/src/builderStore/store/screenTemplates/recordDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/recordDetailScreen.js new file mode 100644 index 0000000000..fb4c3230ee --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/recordDetailScreen.js @@ -0,0 +1,135 @@ +export default function(models) { + return models.map(model => { + const fields = Object.keys(model.schema) + const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Detail" + return { + name: `${model.name} - Detail`, + create: () => createScreen(model, heading), + id: RECORD_DETAIL_TEMPLATE, + } + }) +} + +export const RECORD_DETAIL_TEMPLATE = "RECORD_DETAIL_TEMPLATE" + +const createScreen = (model, heading) => ({ + props: { + _id: "", + _component: "@budibase/standard-components/rowdetail", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + model: model._id, + _children: [ + { + _id: "", + _component: "@budibase/standard-components/heading", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + className: "", + text: heading, + type: "h1", + _instanceName: "Heading 1", + _children: [], + }, + { + _id: "", + _component: "@budibase/standard-components/dataform", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + _instanceName: `${model.name} Form`, + _children: [], + }, + { + _id: "", + _component: "@budibase/standard-components/container", + _styles: { + normal: { + display: "flex", + "flex-direction": "row", + "align-items": "center", + "justify-content": "flex-end", + }, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + className: "", + onLoad: [], + type: "div", + _instanceName: "Buttons Container", + _children: [ + { + _id: "", + _component: "@budibase/standard-components/button", + _styles: { + normal: { + "margin-right": "20px", + }, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + text: "Back", + className: "", + disabled: false, + onClick: [ + { + parameters: { + url: `/${model.name.toLowerCase()}`, + }, + "##eventHandlerType": "Navigate To", + }, + ], + _instanceName: "Back Button", + _children: [], + }, + { + _id: "", + _component: "@budibase/standard-components/button", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + text: "Save", + className: "", + disabled: false, + onClick: [ + { + parameters: { + contextPath: "data", + modelId: model._id, + }, + "##eventHandlerType": "Save Record", + }, + ], + _instanceName: "Save Button", + _children: [], + }, + ], + }, + ], + _instanceName: `${model.name} - Detail`, + _code: "", + }, + route: `/${model.name.toLowerCase()}/:id`, + name: "", +}) diff --git a/packages/builder/src/builderStore/store/screenTemplates/recordListScreen.js b/packages/builder/src/builderStore/store/screenTemplates/recordListScreen.js new file mode 100644 index 0000000000..32f4d0696b --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/recordListScreen.js @@ -0,0 +1,118 @@ +export default function(models) { + return models.map(model => { + return { + name: `${model.name} - List`, + create: () => createScreen(model), + id: RECORD_LIST_TEMPLATE, + } + }) +} + +export const RECORD_LIST_TEMPLATE = "RECORD_LIST_TEMPLATE" + +const createScreen = model => ({ + props: { + _id: "", + _component: "@budibase/standard-components/container", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + type: "div", + _children: [ + { + _id: "", + _component: "@budibase/standard-components/container", + _styles: { + normal: { + display: "flex", + "flex-direction": "row", + "justify-content": "space-between", + "align-items": "center", + }, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + className: "", + onLoad: [], + type: "div", + _instanceName: "Header", + _children: [ + { + _id: "", + _component: "@budibase/standard-components/heading", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + className: "", + text: `${model.name} List`, + type: "h1", + _instanceName: "Heading 1", + _children: [], + }, + { + _id: "", + _component: "@budibase/standard-components/button", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + text: "Create New", + className: "", + disabled: false, + onClick: [ + { + parameters: { + url: `/${model.name}/new`, + }, + "##eventHandlerType": "Navigate To", + }, + ], + _instanceName: "Create New Button", + _children: [], + }, + ], + }, + { + _id: "", + _component: "@budibase/standard-components/datatable", + _styles: { + normal: {}, + hover: {}, + active: {}, + selected: {}, + }, + _code: "", + datasource: { + label: "Deals", + name: `all_${model._id}`, + modelId: model._id, + isModel: true, + }, + stripeColor: "", + borderColor: "", + backgroundColor: "", + color: "", + _instanceName: `${model.name} Table`, + _children: [], + }, + ], + _instanceName: `${model.name} - List`, + _code: "", + className: "", + onLoad: [], + }, + route: `/${model.name.toLowerCase()}`, + name: "", +}) diff --git a/packages/builder/src/builderStore/storeUtils.js b/packages/builder/src/builderStore/storeUtils.js index 2efffc9d4c..aeff3f2528 100644 --- a/packages/builder/src/builderStore/storeUtils.js +++ b/packages/builder/src/builderStore/storeUtils.js @@ -85,10 +85,10 @@ export const regenerateCssForCurrentScreen = state => { return state } -export const generateNewIdsForComponent = (c, state) => +export const generateNewIdsForComponent = (c, state, changeName = true) => walkProps(c, p => { p._id = uuid() - p._instanceName = getNewComponentName(p._component, state) + if (changeName) p._instanceName = getNewComponentName(p._component, state) }) export const getComponentDefinition = (state, name) => diff --git a/packages/builder/src/components/backend/ModelNavigator/modals/CreateTableModal.svelte b/packages/builder/src/components/backend/ModelNavigator/modals/CreateTableModal.svelte index 1cc0e8894c..c4b62d6cec 100644 --- a/packages/builder/src/components/backend/ModelNavigator/modals/CreateTableModal.svelte +++ b/packages/builder/src/components/backend/ModelNavigator/modals/CreateTableModal.svelte @@ -1,11 +1,21 @@ diff --git a/packages/builder/src/components/settings/tabs/APIKeys.svelte b/packages/builder/src/components/settings/tabs/APIKeys.svelte index 8a3c9fc710..8e569596a4 100644 --- a/packages/builder/src/components/settings/tabs/APIKeys.svelte +++ b/packages/builder/src/components/settings/tabs/APIKeys.svelte @@ -1,6 +1,7 @@ + +
+ {#if idFields.length === 0} +
+ Update record can only be used within a component that provides data, such + as a List +
+ {:else} + + + {/if} + + {#if parameters.contextPath} + + {/if} + +
+ + diff --git a/packages/builder/src/components/userInterface/EventsEditor/actions/index.js b/packages/builder/src/components/userInterface/EventsEditor/actions/index.js index e1a85e307e..85f90d124b 100644 --- a/packages/builder/src/components/userInterface/EventsEditor/actions/index.js +++ b/packages/builder/src/components/userInterface/EventsEditor/actions/index.js @@ -1,6 +1,5 @@ import NavigateTo from "./NavigateTo.svelte" -import UpdateRecord from "./UpdateRecord.svelte" -import CreateRecord from "./CreateRecord.svelte" +import SaveRecord from "./SaveRecord.svelte" // defines what actions are available, when adding a new one // the component is the setup panel for the action @@ -9,15 +8,11 @@ import CreateRecord from "./CreateRecord.svelte" export default [ { - name: "Create Record", - component: CreateRecord, + name: "Save Record", + component: SaveRecord, }, { name: "Navigate To", component: NavigateTo, }, - { - name: "Update Record", - component: UpdateRecord, - }, ] diff --git a/packages/builder/src/components/userInterface/NewScreenModal.svelte b/packages/builder/src/components/userInterface/NewScreenModal.svelte index d38d4bbb7b..0a9bc81987 100644 --- a/packages/builder/src/components/userInterface/NewScreenModal.svelte +++ b/packages/builder/src/components/userInterface/NewScreenModal.svelte @@ -1,27 +1,49 @@ + + + + - + diff --git a/packages/builder/src/components/userInterface/SettingsView.svelte b/packages/builder/src/components/userInterface/SettingsView.svelte index 159093fe8e..0460e15ad6 100644 --- a/packages/builder/src/components/userInterface/SettingsView.svelte +++ b/packages/builder/src/components/userInterface/SettingsView.svelte @@ -11,6 +11,7 @@ export let componentDefinition = {} export let componentInstance = {} export let onChange = () => {} + export let onScreenPropChange = () => {} export let displayNameField = false export let screenOrPageInstance @@ -91,7 +92,7 @@ label={def.label} key={def.key} value={screenOrPageInstance[def.key]} - {onChange} + onChange={onScreenPropChange} props={{ ...excludeProps(def, ['control', 'label']) }} /> {/each}
diff --git a/packages/builder/src/components/userInterface/propertyCategories.js b/packages/builder/src/components/userInterface/propertyCategories.js index 9c622a383a..bae44df7f5 100644 --- a/packages/builder/src/components/userInterface/propertyCategories.js +++ b/packages/builder/src/components/userInterface/propertyCategories.js @@ -361,19 +361,18 @@ export const typography = [ label: "Font", key: "font-family", control: OptionSelect, - defaultValue: "initial", + defaultValue: "Arial", options: [ - "initial", "Arial", "Arial Black", "Cursive", "Courier", "Comic Sans MS", "Helvetica", + "Helvetica Neue", "Impact", "Inter", "Lucida Sans Unicode", - "Open Sans", "Roboto", "Roboto Mono", "Times New Roman", @@ -467,9 +466,9 @@ export const background = [ label: "Gradient", key: "background-image", control: OptionSelect, - defaultValue: "None", + defaultValue: "", options: [ - { label: "None", value: "None" }, + { label: "Select option", value: "" }, { label: "Warm Flame", value: "linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);", @@ -518,9 +517,9 @@ export const background = [ }, { label: "Image", - key: "background-image", + key: "background", control: Input, - placeholder: "Src", + placeholder: "url", }, ] @@ -665,7 +664,7 @@ export const transitions = [ control: OptionSelect, textAlign: "center", placeholder: "sec", - options: ["0.2ms", "0.4ms", "0.8ms", "1s", "2s", "4s"], + options: ["0.4s", "0.6s", "0.8s", "1s", "2s", "4s"], }, { label: "Ease", diff --git a/packages/builder/src/components/userInterface/temporaryPanelStructure.js b/packages/builder/src/components/userInterface/temporaryPanelStructure.js index 296ae26606..3f853efcfb 100644 --- a/packages/builder/src/components/userInterface/temporaryPanelStructure.js +++ b/packages/builder/src/components/userInterface/temporaryPanelStructure.js @@ -583,23 +583,7 @@ export default { icon: "ri-file-edit-line", properties: { design: { ...all }, - settings: [ - { - label: "Table", - key: "model", - control: ModelSelect, - }, - { - label: "Title", - key: "title", - control: Input, - }, - { - label: "Button Text", - key: "buttonText", - control: Input, - }, - ], + settings: [], }, }, { @@ -608,23 +592,7 @@ export default { icon: "ri-file-edit-line", properties: { design: { ...all }, - settings: [ - { - label: "Table", - key: "model", - control: ModelSelect, - }, - { - label: "Title", - key: "title", - control: Input, - }, - { - label: "Button Text", - key: "buttonText", - control: Input, - }, - ], + settings: [], }, }, ], @@ -1125,8 +1093,8 @@ export default { // children: [], // }, { - name: "Record Detail", - _component: "@budibase/standard-components/recorddetail", + name: "Row Detail", + _component: "@budibase/standard-components/rowdetail", description: "Loads a record, using an id from the URL, which can be used with {{ context }}, in children", icon: "ri-profile-line", @@ -1136,6 +1104,18 @@ export default { }, children: [], }, + { + name: "New Row", + _component: "@budibase/standard-components/newrow", + description: + "Sets up a new record for creation, which can be used with {{ context }}, in children", + icon: "ri-profile-line", + properties: { + design: { ...all }, + settings: [{ label: "Table", key: "model", control: ModelSelect }], + }, + children: [], + }, // { // name: "Map", // _component: "@budibase/standard-components/datamap", diff --git a/packages/builder/yarn.lock b/packages/builder/yarn.lock index 94a7501bce..dfeebc7214 100644 --- a/packages/builder/yarn.lock +++ b/packages/builder/yarn.lock @@ -5850,10 +5850,10 @@ svelte-portal@^1.0.0: resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3" integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q== -svelte@^3.24.1: - version "3.25.1" - resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.25.1.tgz#218def1243fea5a97af6eb60f5e232315bb57ac4" - integrity sha512-IbrVKTmuR0BvDw4ii8/gBNy8REu7nWTRy9uhUz+Yuae5lIjWgSGwKlWtJGC2Vg95s+UnXPqDu0Kk/sUwe0t2GQ== +svelte@^3.29.0: + version "3.29.0" + resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.29.0.tgz#80acac4254341ad8f3301e5ef03f4127ea967d96" + integrity sha512-f+A65eyOQ5ujETLy+igNXtlr6AEjAQLYd1yJE1VwNiXMQO5Z/Vmiy3rL+zblV/9jd7rtTTWqO1IcuXsP2Qv0OA== symbol-observable@^1.1.0: version "1.2.0" diff --git a/packages/client/src/api/index.js b/packages/client/src/api/index.js index 34441d563b..f66cfcf69b 100644 --- a/packages/client/src/api/index.js +++ b/packages/client/src/api/index.js @@ -52,14 +52,14 @@ const apiOpts = { delete: del, } -const createRecord = async params => +const saveRecord = async (params, state) => await post({ url: `/api/${params.modelId}/records`, - body: makeRecordRequestBody(params), + body: makeRecordRequestBody(params, state), }) -const updateRecord = async params => { - const record = makeRecordRequestBody(params) +const updateRecord = async (params, state) => { + const record = makeRecordRequestBody(params, state) record._id = params._id await patch({ url: `/api/${params.modelId}/records/${params._id}`, @@ -67,8 +67,14 @@ const updateRecord = async params => { }) } -const makeRecordRequestBody = parameters => { - const body = {} +const makeRecordRequestBody = (parameters, state) => { + // start with the record thats currently in context + const body = { ...(state.data || {}) } + + // dont send the model + if (body._model) delete body._model + + // then override with supplied parameters for (let fieldName in parameters.fields) { const field = parameters.fields[fieldName] @@ -95,6 +101,6 @@ const makeRecordRequestBody = parameters => { export default { authenticate: authenticate(apiOpts), - createRecord, + saveRecord, updateRecord, } diff --git a/packages/client/src/state/eventHandlers.js b/packages/client/src/state/eventHandlers.js index 05d8ef2fa3..fa80605b42 100644 --- a/packages/client/src/state/eventHandlers.js +++ b/packages/client/src/state/eventHandlers.js @@ -6,8 +6,8 @@ export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType" export const eventHandlers = routeTo => { const handlers = { "Navigate To": param => routeTo(param && param.url), - "Create Record": api.createRecord, "Update Record": api.updateRecord, + "Save Record": api.saveRecord, "Trigger Workflow": api.triggerWorkflow, } @@ -19,7 +19,7 @@ export const eventHandlers = routeTo => { const handler = handlers[action[EVENT_TYPE_MEMBER_NAME]] const parameters = createParameters(action.parameters, state) if (handler) { - await handler(parameters) + await handler(parameters, state) } } } diff --git a/packages/client/src/state/store.js b/packages/client/src/state/store.js index 6464589b03..cf284ec08e 100644 --- a/packages/client/src/state/store.js +++ b/packages/client/src/state/store.js @@ -95,7 +95,7 @@ const getState = contextStoreKey => contextStoreKey ? contextStores[contextStoreKey].state : rootState const getStore = contextStoreKey => - contextStoreKey ? contextStores[contextStoreKey] : rootStore + contextStoreKey ? contextStores[contextStoreKey].store : rootStore export default { subscribe, diff --git a/packages/server/src/api/controllers/static.js b/packages/server/src/api/controllers/static.js index f14b794210..5aaa9ab125 100644 --- a/packages/server/src/api/controllers/static.js +++ b/packages/server/src/api/controllers/static.js @@ -136,7 +136,7 @@ exports.performLocalFileProcessing = async function(ctx) { } exports.serveApp = async function(ctx) { - const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated" + const mainOrAuth = ctx.auth.authenticated ? "main" : "unauthenticated" // default to homedir const appPath = resolve( @@ -154,7 +154,7 @@ exports.serveApp = async function(ctx) { // only set the appId cookie for /appId .. we COULD check for valid appIds // but would like to avoid that DB hit const looksLikeAppId = /^(app_)?[0-9a-f]{32}$/.test(appId) - if (looksLikeAppId && !ctx.isAuthenticated) { + if (looksLikeAppId && !ctx.auth.authenticated) { const anonUser = { userId: "ANON", accessLevelId: ANON_LEVEL_ID, @@ -200,7 +200,7 @@ exports.serveAttachment = async function(ctx) { exports.serveAppAsset = async function(ctx) { // default to homedir - const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated" + const mainOrAuth = ctx.auth.authenticated ? "main" : "unauthenticated" const appPath = resolve( budibaseAppsDir(), diff --git a/packages/server/src/app.js b/packages/server/src/app.js index 7560c9cfa4..4157534365 100644 --- a/packages/server/src/app.js +++ b/packages/server/src/app.js @@ -24,6 +24,7 @@ app.use( ) app.context.eventEmitter = eventEmitter +app.context.auth = {} // api routes app.use(api.routes()) diff --git a/packages/server/src/db/linkedRecords/index.js b/packages/server/src/db/linkedRecords/index.js index 115553696c..ba93f3d2e8 100644 --- a/packages/server/src/db/linkedRecords/index.js +++ b/packages/server/src/db/linkedRecords/index.js @@ -1,5 +1,6 @@ const LinkController = require("./LinkController") const { IncludeDocs, getLinkDocuments, createLinkView } = require("./linkUtils") +const _ = require("lodash") /** * This functionality makes sure that when records with links are created, updated or deleted they are processed @@ -88,23 +89,23 @@ exports.attachLinkInfo = async (instanceId, records) => { records = [records] wasArray = false } + let modelIds = [...new Set(records.map(el => el.modelId))] // start by getting all the link values for performance reasons - let responses = await Promise.all( - records.map(record => - getLinkDocuments({ - instanceId, - modelId: record.modelId, - recordId: record._id, - includeDocs: IncludeDocs.EXCLUDE, - }) + let responses = _.flatten( + await Promise.all( + modelIds.map(modelId => + getLinkDocuments({ + instanceId, + modelId: modelId, + includeDocs: IncludeDocs.EXCLUDE, + }) + ) ) ) - // can just use an index to access responses, order maintained - let index = 0 // now iterate through the records and all field information for (let record of records) { // get all links for record, ignore fieldName for now - const linkVals = responses[index++] + const linkVals = responses.filter(el => el.thisId === record._id) for (let linkVal of linkVals) { // work out which link pertains to this record if (!(record[linkVal.fieldName] instanceof Array)) { diff --git a/packages/server/src/db/linkedRecords/linkUtils.js b/packages/server/src/db/linkedRecords/linkUtils.js index 7680a2603f..f5fe1f6786 100644 --- a/packages/server/src/db/linkedRecords/linkUtils.js +++ b/packages/server/src/db/linkedRecords/linkUtils.js @@ -27,10 +27,12 @@ exports.createLinkView = async instanceId => { let doc2 = doc.doc2 emit([doc1.modelId, doc1.recordId], { id: doc2.recordId, + thisId: doc1.recordId, fieldName: doc1.fieldName, }) emit([doc2.modelId, doc2.recordId], { id: doc1.recordId, + thisId: doc2.recordId, fieldName: doc2.fieldName, }) } diff --git a/packages/server/src/middleware/authenticated.js b/packages/server/src/middleware/authenticated.js index 93ee66b6d4..1203ea0033 100644 --- a/packages/server/src/middleware/authenticated.js +++ b/packages/server/src/middleware/authenticated.js @@ -20,8 +20,10 @@ module.exports = async (ctx, next) => { if (builderToken) { try { const jwtPayload = jwt.verify(builderToken, ctx.config.jwtSecret) - ctx.apiKey = jwtPayload.apiKey - ctx.isAuthenticated = jwtPayload.accessLevelId === BUILDER_LEVEL_ID + ctx.auth = { + apiKey: jwtPayload.apiKey, + authenticated: jwtPayload.accessLevelId === BUILDER_LEVEL_ID, + } ctx.user = { ...jwtPayload, accessLevel: await getAccessLevel( @@ -38,14 +40,13 @@ module.exports = async (ctx, next) => { } if (!appToken) { - ctx.isAuthenticated = false + ctx.auth.authenticated = false await next() return } try { const jwtPayload = jwt.verify(appToken, ctx.config.jwtSecret) - ctx.apiKey = jwtPayload.apiKey ctx.user = { ...jwtPayload, accessLevel: await getAccessLevel( @@ -53,7 +54,10 @@ module.exports = async (ctx, next) => { jwtPayload.accessLevelId ), } - ctx.isAuthenticated = ctx.user.accessLevelId !== ANON_LEVEL_ID + ctx.auth = { + authenticated: ctx.user.accessLevelId !== ANON_LEVEL_ID, + apiKey: jwtPayload.apiKey, + } } catch (err) { ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text) } diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js index 4cce4c4670..bd09029471 100644 --- a/packages/server/src/middleware/authorized.js +++ b/packages/server/src/middleware/authorized.js @@ -5,9 +5,36 @@ const { BUILDER_LEVEL_ID, BUILDER, } = require("../utilities/accessLevels") +const environment = require("../environment") +const { apiKeyTable } = require("../db/dynamoClient") module.exports = (permName, getItemId) => async (ctx, next) => { - if (!ctx.isAuthenticated) { + if ( + environment.CLOUD && + ctx.headers["x-api-key"] && + ctx.headers["x-instanceid"] + ) { + // api key header passed by external webhook + const apiKeyInfo = await apiKeyTable.get({ + primary: ctx.headers["x-api-key"], + }) + + if (apiKeyInfo) { + ctx.auth = { + authenticated: true, + external: true, + apiKey: ctx.headers["x-api-key"], + } + ctx.user = { + instanceId: ctx.headers["x-instanceid"], + } + return next() + } + + ctx.throw(403, "API key invalid") + } + + if (!ctx.auth.authenticated) { ctx.throw(403, "Session not authenticated") } diff --git a/packages/server/src/middleware/usageQuota.js b/packages/server/src/middleware/usageQuota.js index e82305dc12..778f51f9d8 100644 --- a/packages/server/src/middleware/usageQuota.js +++ b/packages/server/src/middleware/usageQuota.js @@ -55,7 +55,7 @@ module.exports = async (ctx, next) => { return next() } try { - await usageQuota.update(ctx.apiKey, property, usage) + await usageQuota.update(ctx.auth.apiKey, property, usage) return next() } catch (err) { ctx.throw(403, err) diff --git a/packages/standard-components/components.json b/packages/standard-components/components.json index e600b264be..c7d59ddde4 100644 --- a/packages/standard-components/components.json +++ b/packages/standard-components/components.json @@ -218,20 +218,12 @@ "dataform": { "description": "an HTML table that fetches data from a table or view and displays it.", "data": true, - "props": { - "model": "models", - "title": "string", - "buttonText": "string" - } + "props": {} }, "dataformwide": { "description": "an HTML table that fetches data from a table or view and displays it.", "data": true, - "props": { - "model": "models", - "title": "string", - "buttonText": "string" - } + "props": {} }, "datalist": { "description": "A configurable data list that attaches to your backend models.", @@ -269,11 +261,22 @@ "destinationUrl": "string" } }, - "recorddetail": { + "rowdetail": { "description": "Loads a record, using an ID in the url", "context": "model", "children": true, "data": true, + "baseComponent": true, + "props": { + "model": "models" + } + }, + "newrow": { + "description": "Prepares a new record for creation", + "context": "model", + "children": true, + "data": true, + "baseComponent": true, "props": { "model": "models" } @@ -715,7 +718,7 @@ "default": "div" } }, - "container": true, + "baseComponent": true, "tags": [ "div", "container", diff --git a/packages/standard-components/package.json b/packages/standard-components/package.json index af12bfbf19..8ed9aeab0f 100644 --- a/packages/standard-components/package.json +++ b/packages/standard-components/package.json @@ -16,17 +16,17 @@ "@budibase/client": "^0.2.0", "@rollup/plugin-commonjs": "^11.1.0", "lodash": "^4.17.15", - "rollup": "^1.11.0", + "rollup": "^2.11.2", "rollup-plugin-commonjs": "^10.0.2", "rollup-plugin-json": "^4.0.0", "rollup-plugin-livereload": "^1.0.1", "rollup-plugin-node-resolve": "^5.0.0", "rollup-plugin-postcss": "^3.1.5", - "rollup-plugin-svelte": "^5.0.0", - "rollup-plugin-terser": "^5.1.1", + "rollup-plugin-svelte": "^5.0.3", + "rollup-plugin-terser": "^7.0.2", "shortid": "^2.2.15", "sirv-cli": "^0.4.4", - "svelte": "^3.12.1" + "svelte": "^3.29.0" }, "keywords": [ "svelte" diff --git a/packages/standard-components/src/Form.svelte b/packages/standard-components/src/Form.svelte index 937a58f271..c9777b52b4 100644 --- a/packages/standard-components/src/Form.svelte +++ b/packages/standard-components/src/Form.svelte @@ -1,168 +1,61 @@ -
- {#if title} -

{title}

- {/if} -
- - {#each fields as field} -
- {#if !(schema[field].type === 'boolean' && !wide)} - - {/if} - {#if schema[field].type === 'options'} - - {:else if schema[field].type === 'datetime'} - - {:else if schema[field].type === 'boolean'} - - {:else if schema[field].type === 'number'} - - {:else if schema[field].type === 'string'} - - {:else if schema[field].type === 'attachment'} - - {:else if schema[field].type === 'link'} - - {/if} -
- {/each} -
- +
+ + {#each fields as field} +
+ {#if !(schema[field].type === 'boolean' && !wide)} + + {/if} + {#if schema[field].type === 'options'} + + {:else if schema[field].type === 'datetime'} + + {:else if schema[field].type === 'boolean'} + + {:else if schema[field].type === 'number'} + + {:else if schema[field].type === 'string'} + + {:else if schema[field].type === 'attachment'} + + {:else if schema[field].type === 'link'} + + {/if}
-
- + {/each} +
diff --git a/packages/standard-components/src/NewRow.svelte b/packages/standard-components/src/NewRow.svelte new file mode 100644 index 0000000000..fe63fd27dd --- /dev/null +++ b/packages/standard-components/src/NewRow.svelte @@ -0,0 +1,37 @@ + + +
diff --git a/packages/standard-components/src/RecordDetail.svelte b/packages/standard-components/src/RowDetail.svelte similarity index 70% rename from packages/standard-components/src/RecordDetail.svelte rename to packages/standard-components/src/RowDetail.svelte index aa9d39c0cc..dc767a6b6e 100644 --- a/packages/standard-components/src/RecordDetail.svelte +++ b/packages/standard-components/src/RowDetail.svelte @@ -20,6 +20,7 @@ if (response.status === 200) { const allRecords = await response.json() if (allRecords.length > 0) return allRecords[0] + return { modelId: model } } } @@ -29,31 +30,35 @@ let record // if srcdoc, then we assume this is the builder preview if (pathParts.length === 0 || pathParts[0] === "srcdoc") { - record = await fetchFirstRecord() - } else { - const id = pathParts[pathParts.length - 1] - const GET_RECORD_URL = `/api/${model}/records/${id}` + if (model) record = await fetchFirstRecord() + } else if (_bb.routeParams().id) { + const GET_RECORD_URL = `/api/${model}/records/${_bb.routeParams().id}` const response = await _bb.api.get(GET_RECORD_URL) if (response.status === 200) { record = await response.json() + } else { + throw new Error("Failed to fetch record.", response) } + } else { + throw new Exception("Record ID was not supplied to RowDetail") } if (record) { // Fetch model schema so we can check for linked records - const model = await fetchModel(record.modelId) - for (let key of Object.keys(model.schema)) { - if (model.schema[key].type === "link") { + const modelObj = await fetchModel(record.modelId) + for (let key of Object.keys(modelObj.schema)) { + if (modelObj.schema[key].type === "link") { record[key] = Array.isArray(record[key]) ? record[key].length : 0 } } + record._model = modelObj + _bb.attachChildren(target, { - hydrate: false, context: record, }) } else { - throw new Error("Failed to fetch record.", response) + _bb.attachChildren(target) } } diff --git a/packages/standard-components/src/index.js b/packages/standard-components/src/index.js index 0ce5088cb8..c5a3c6085a 100644 --- a/packages/standard-components/src/index.js +++ b/packages/standard-components/src/index.js @@ -26,7 +26,8 @@ export { default as embed } from "./Embed.svelte" export { default as stackedlist } from "./StackedList.svelte" export { default as card } from "./Card.svelte" export { default as cardhorizontal } from "./CardHorizontal.svelte" -export { default as recorddetail } from "./RecordDetail.svelte" +export { default as rowdetail } from "./RowDetail.svelte" +export { default as newrow } from "./NewRow.svelte" export { default as datepicker } from "./DatePicker.svelte" export * from "./Chart" export { default as icon } from "./Icon.svelte"