From b4a8f22b2ee54914294eadb0259fde5f1006879a Mon Sep 17 00:00:00 2001 From: Dean Date: Wed, 11 Oct 2023 10:09:14 +0100 Subject: [PATCH 01/65] Initial commit --- .../design/settings/componentSettings.js | 2 + .../ButtonConfiguration.svelte | 104 ++++++++++ .../ButtonConfiguration/ButtonSetting.svelte | 61 ++++++ .../controls/FieldConfiguration.svelte | 91 --------- .../EditFieldPopover.svelte | 38 ++-- .../FieldConfiguration.svelte | 4 +- .../new/_components/componentStructure.json | 1 + packages/client/manifest.json | 183 ++++++++++++++++++ .../components/app/forms/ButtonGroup.svelte | 38 ++++ .../client/src/components/app/forms/index.js | 1 + 10 files changed, 412 insertions(+), 111 deletions(-) create mode 100644 packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte create mode 100644 packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonSetting.svelte delete mode 100644 packages/builder/src/components/design/settings/controls/FieldConfiguration.svelte create mode 100644 packages/client/src/components/app/forms/ButtonGroup.svelte diff --git a/packages/builder/src/components/design/settings/componentSettings.js b/packages/builder/src/components/design/settings/componentSettings.js index 4c49587372..232b4bef31 100644 --- a/packages/builder/src/components/design/settings/componentSettings.js +++ b/packages/builder/src/components/design/settings/componentSettings.js @@ -23,6 +23,7 @@ import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte" import GridColumnEditor from "./controls/ColumnEditor/GridColumnEditor.svelte" import BarButtonList from "./controls/BarButtonList.svelte" import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte" +import ButtonConfiguration from "./controls/ButtonConfiguration/ButtonConfiguration.svelte" import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte" const componentMap = { @@ -48,6 +49,7 @@ const componentMap = { "filter/relationship": RelationshipFilterEditor, url: URLSelect, fieldConfiguration: FieldConfiguration, + buttonConfiguration: ButtonConfiguration, columns: ColumnEditor, "columns/basic": BasicColumnEditor, "columns/grid": GridColumnEditor, diff --git a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte new file mode 100644 index 0000000000..baa8ad733c --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte @@ -0,0 +1,104 @@ + + +
+ {#if buttonList?.length} + + {/if} +
+ + diff --git a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonSetting.svelte b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonSetting.svelte new file mode 100644 index 0000000000..851cbe289c --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonSetting.svelte @@ -0,0 +1,61 @@ + + +
+
+ +
{item.text}
+
+
+ {console.log("REMOVE ME")}} + /> +
+
+ + diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration.svelte b/packages/builder/src/components/design/settings/controls/FieldConfiguration.svelte deleted file mode 100644 index 80f4829d71..0000000000 --- a/packages/builder/src/components/design/settings/controls/FieldConfiguration.svelte +++ /dev/null @@ -1,91 +0,0 @@ - - -Configure columns - - - Configure the columns in your {subject.toLowerCase()}. - - - - diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte b/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte index 7d2eaae478..72e7784727 100644 --- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte +++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte @@ -61,6 +61,25 @@ dispatch("change", update) } + + const customPositionHandler = (anchorBounds, eleBounds, cfg) => { + let { left, top } = cfg + let percentageOffset = 30 + // left-outside + left = anchorBounds.left - eleBounds.width - 18 + + // shift up from the anchor, if space allows + let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset + let defaultTop = anchorBounds.top - offsetPos + + if (window.innerHeight - defaultTop < eleBounds.height) { + top = window.innerHeight - eleBounds.height - 5 + } else { + top = anchorBounds.top - offsetPos + } + + return { ...cfg, left, top } + } 0} maxHeight={600} - handlePostionUpdate={(anchorBounds, eleBounds, cfg) => { - let { left, top } = cfg - let percentageOffset = 30 - // left-outside - left = anchorBounds.left - eleBounds.width - 18 - - // shift up from the anchor, if space allows - let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset - let defaultTop = anchorBounds.top - offsetPos - - if (window.innerHeight - defaultTop < eleBounds.height) { - top = window.innerHeight - eleBounds.height - 5 - } else { - top = anchorBounds.top - offsetPos - } - - return { ...cfg, left, top } - }} + handlePostionUpdate={customPositionHandler} > diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte index 4169cb7d3d..42651a4d84 100644 --- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte +++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte @@ -50,7 +50,7 @@ updateSanitsedFields(sanitisedValue) unconfigured = buildUnconfiguredOptions(schema, sanitisedFields) fieldList = [...sanitisedFields, ...unconfigured] - .map(buildSudoInstance) + .map(buildPseudoInstance) .filter(x => x != null) } @@ -104,7 +104,7 @@ }) } - const buildSudoInstance = instance => { + const buildPseudoInstance = instance => { if (instance._component) { return instance } diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json index 11a130490a..dd129be11e 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json @@ -36,6 +36,7 @@ "heading", "text", "button", + "buttongroup", "tag", "spectrumcard", "cardstat", diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 4e56ca758d..27e56d94db 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -258,6 +258,189 @@ "description": "Contains your app screens", "static": true }, + + "buttongroup": { + "name": "Button group", + "icon": "Button", + "hasChildren": false, + "settings": [ + { + "section": true, + "name": "Buttons", + "settings": [ + { + "type": "buttonConfiguration", + "key": "buttons", + "nested": true, + "defaultValue" : [{ + "component" : "button", + "props" : { + "type": "cta" + } + },{ + "component" : "button", + "props" : { + "type" : "primary" + } + }] + } + ] + }, + { + "section": true, + "name": "Layout", + "settings": [ + { + "type": "select", + "label": "Direction", + "key": "direction", + "showInBar": true, + "barStyle": "buttons", + "options": [ + { + "label": "Column", + "value": "column", + "barIcon": "ViewColumn", + "barTitle": "Column layout" + }, + { + "label": "Row", + "value": "row", + "barIcon": "ViewRow", + "barTitle": "Row layout" + } + ], + "defaultValue": "column" + }, + { + "type": "select", + "label": "Horiz. align", + "key": "hAlign", + "showInBar": true, + "barStyle": "buttons", + "options": [ + { + "label": "Left", + "value": "left", + "barIcon": "AlignLeft", + "barTitle": "Align left" + }, + { + "label": "Center", + "value": "center", + "barIcon": "AlignCenter", + "barTitle": "Align center" + }, + { + "label": "Right", + "value": "right", + "barIcon": "AlignRight", + "barTitle": "Align right" + }, + { + "label": "Stretch", + "value": "stretch", + "barIcon": "MoveLeftRight", + "barTitle": "Align stretched horizontally" + } + ], + "defaultValue": "left" + }, + { + "type": "select", + "label": "Vert. align", + "key": "vAlign", + "showInBar": true, + "barStyle": "buttons", + "options": [ + { + "label": "Top", + "value": "top", + "barIcon": "AlignTop", + "barTitle": "Align top" + }, + { + "label": "Middle", + "value": "middle", + "barIcon": "AlignMiddle", + "barTitle": "Align middle" + }, + { + "label": "Bottom", + "value": "bottom", + "barIcon": "AlignBottom", + "barTitle": "Align bottom" + }, + { + "label": "Stretch", + "value": "stretch", + "barIcon": "MoveUpDown", + "barTitle": "Align stretched vertically" + } + ], + "defaultValue": "top" + }, + { + "type": "select", + "label": "Size", + "key": "size", + "showInBar": true, + "barStyle": "buttons", + "options": [ + { + "label": "Shrink", + "value": "shrink", + "barIcon": "Minimize", + "barTitle": "Shrink container" + }, + { + "label": "Grow", + "value": "grow", + "barIcon": "Maximize", + "barTitle": "Grow container" + } + ], + "defaultValue": "shrink" + }, + { + "type": "select", + "label": "Gap", + "key": "gap", + "showInBar": true, + "barStyle": "picker", + "options": [ + { + "label": "None", + "value": "N" + }, + { + "label": "Small", + "value": "S" + }, + { + "label": "Medium", + "value": "M" + }, + { + "label": "Large", + "value": "L" + } + ], + "defaultValue": "M" + }, + { + "type": "boolean", + "label": "Wrap", + "key": "wrap", + "showInBar": true, + "barIcon": "ModernGridView", + "barTitle": "Wrap" + } + ] + } + ] + }, + "button": { "name": "Button", "description": "A basic html button that is ready for styling", diff --git a/packages/client/src/components/app/forms/ButtonGroup.svelte b/packages/client/src/components/app/forms/ButtonGroup.svelte new file mode 100644 index 0000000000..222e91a55f --- /dev/null +++ b/packages/client/src/components/app/forms/ButtonGroup.svelte @@ -0,0 +1,38 @@ + + + + + + {#each buttons as { text, type, quiet, disabled, onClick, size }} + + {/each} + + + diff --git a/packages/client/src/components/app/forms/index.js b/packages/client/src/components/app/forms/index.js index 5804d3a79d..24d7f11c0c 100644 --- a/packages/client/src/components/app/forms/index.js +++ b/packages/client/src/components/app/forms/index.js @@ -16,3 +16,4 @@ export { default as jsonfield } from "./JSONField.svelte" export { default as s3upload } from "./S3Upload.svelte" export { default as codescanner } from "./CodeScannerField.svelte" export { default as bbreferencefield } from "./BBReferenceField.svelte" +export { default as buttongroup } from "./ButtonGroup.svelte" \ No newline at end of file From b8c87224f76b0ec77e65072fd78df917bff7a7bf Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 13 Oct 2023 14:53:39 +0100 Subject: [PATCH 02/65] Add/Remove button behaviour, binding label support and default text behaviour. Refactoring and renaming of EditComponentPopoever component --- .../ButtonConfiguration.svelte | 112 ++++++++++++++---- .../ButtonConfiguration/ButtonSetting.svelte | 33 +++--- .../{ => DraggableList}/DraggableList.svelte | 32 ++++- .../controls/DraggableList/drag-handle.svelte | 31 +++++ ...ver.svelte => EditComponentPopover.svelte} | 65 ++++------ .../FieldConfiguration.svelte | 2 +- .../FieldConfiguration/FieldSetting.svelte | 56 ++++++++- packages/client/manifest.json | 22 ++-- .../src/components/app/ButtonGroup.svelte | 37 ++++++ .../components/app/forms/ButtonGroup.svelte | 38 ------ .../client/src/components/app/forms/index.js | 3 +- packages/client/src/components/app/index.js | 1 + 12 files changed, 281 insertions(+), 151 deletions(-) rename packages/builder/src/components/design/settings/controls/{ => DraggableList}/DraggableList.svelte (82%) create mode 100644 packages/builder/src/components/design/settings/controls/DraggableList/drag-handle.svelte rename packages/builder/src/components/design/settings/controls/{FieldConfiguration/EditFieldPopover.svelte => EditComponentPopover.svelte} (63%) create mode 100644 packages/client/src/components/app/ButtonGroup.svelte delete mode 100644 packages/client/src/components/app/forms/ButtonGroup.svelte diff --git a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte index baa8ad733c..fde888d17b 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte @@ -1,5 +1,5 @@
- -
{item.text}
+
{readableText || "Button"}
- {console.log("REMOVE ME")}} - /> + removeButton(item._id)} + />
diff --git a/packages/builder/src/components/design/settings/controls/DraggableList.svelte b/packages/builder/src/components/design/settings/controls/DraggableList/DraggableList.svelte similarity index 82% rename from packages/builder/src/components/design/settings/controls/DraggableList.svelte rename to packages/builder/src/components/design/settings/controls/DraggableList/DraggableList.svelte index c8395b2a1f..1992299e90 100644 --- a/packages/builder/src/components/design/settings/controls/DraggableList.svelte +++ b/packages/builder/src/components/design/settings/controls/DraggableList/DraggableList.svelte @@ -1,10 +1,10 @@
- -
{item.label || item.field}
+ > +
+ + {item.field} +
+ +
{readableText}
@@ -53,4 +81,20 @@ .list-item-body { justify-content: space-between; } + .type-icon { + display: flex; + gap: var(--spacing-m); + margin: var(--spacing-xl); + margin-bottom: 0px; + height: var(--spectrum-alias-item-height-m); + padding: 0px var(--spectrum-alias-item-padding-m); + border-width: var(--spectrum-actionbutton-border-size); + border-radius: var(--spectrum-alias-border-radius-regular); + border: 1px solid + var( + --spectrum-actionbutton-m-border-color, + var(--spectrum-alias-border-color) + ); + align-items: center; + } diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 27e56d94db..a8559d4f79 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -258,7 +258,6 @@ "description": "Contains your app screens", "static": true }, - "buttongroup": { "name": "Button group", "icon": "Button", @@ -272,17 +271,14 @@ "type": "buttonConfiguration", "key": "buttons", "nested": true, - "defaultValue" : [{ - "component" : "button", - "props" : { + "defaultValue": [ + { "type": "cta" + }, + { + "type": "primary" } - },{ - "component" : "button", - "props" : { - "type" : "primary" - } - }] + ] } ] }, @@ -310,7 +306,7 @@ "barTitle": "Row layout" } ], - "defaultValue": "column" + "defaultValue": "row" }, { "type": "select", @@ -440,7 +436,6 @@ } ] }, - "button": { "name": "Button", "description": "A basic html button that is ready for styling", @@ -2592,7 +2587,6 @@ "key": "disabled", "defaultValue": false }, - { "type": "text", "label": "Initial form step", @@ -5875,4 +5869,4 @@ } ] } -} +} \ No newline at end of file diff --git a/packages/client/src/components/app/ButtonGroup.svelte b/packages/client/src/components/app/ButtonGroup.svelte new file mode 100644 index 0000000000..8d6cacd7a5 --- /dev/null +++ b/packages/client/src/components/app/ButtonGroup.svelte @@ -0,0 +1,37 @@ + + + + + {#each buttons as { text, type, quiet, disabled, onClick, size }, idx} + + {/each} + + diff --git a/packages/client/src/components/app/forms/ButtonGroup.svelte b/packages/client/src/components/app/forms/ButtonGroup.svelte deleted file mode 100644 index 222e91a55f..0000000000 --- a/packages/client/src/components/app/forms/ButtonGroup.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - {#each buttons as { text, type, quiet, disabled, onClick, size }} - - {/each} - - - diff --git a/packages/client/src/components/app/forms/index.js b/packages/client/src/components/app/forms/index.js index 24d7f11c0c..38372f32d4 100644 --- a/packages/client/src/components/app/forms/index.js +++ b/packages/client/src/components/app/forms/index.js @@ -15,5 +15,4 @@ export { default as formstep } from "./FormStep.svelte" export { default as jsonfield } from "./JSONField.svelte" export { default as s3upload } from "./S3Upload.svelte" export { default as codescanner } from "./CodeScannerField.svelte" -export { default as bbreferencefield } from "./BBReferenceField.svelte" -export { default as buttongroup } from "./ButtonGroup.svelte" \ No newline at end of file +export { default as bbreferencefield } from "./BBReferenceField.svelte" \ No newline at end of file diff --git a/packages/client/src/components/app/index.js b/packages/client/src/components/app/index.js index 060c15a857..97df3741e1 100644 --- a/packages/client/src/components/app/index.js +++ b/packages/client/src/components/app/index.js @@ -19,6 +19,7 @@ export { default as dataprovider } from "./DataProvider.svelte" export { default as divider } from "./Divider.svelte" export { default as screenslot } from "./ScreenSlot.svelte" export { default as button } from "./Button.svelte" +export { default as buttongroup } from "./ButtonGroup.svelte" export { default as repeater } from "./Repeater.svelte" export { default as text } from "./Text.svelte" export { default as layout } from "./Layout.svelte" From 166c517ff82d4644f37f5e6fe3b4ff2fef2b0d94 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 13 Oct 2023 15:02:18 +0100 Subject: [PATCH 03/65] Fix Block import locations --- packages/client/src/components/app/ButtonGroup.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/src/components/app/ButtonGroup.svelte b/packages/client/src/components/app/ButtonGroup.svelte index 8d6cacd7a5..0af16e6950 100644 --- a/packages/client/src/components/app/ButtonGroup.svelte +++ b/packages/client/src/components/app/ButtonGroup.svelte @@ -1,6 +1,6 @@
- {#if buttonList?.length} + {#if buttonCount} - {#each buttons as { text, type, quiet, disabled, onClick, size }, idx} + {#each buttons as { text, type, quiet, disabled, onClick, size }} Date: Tue, 17 Oct 2023 10:51:06 +0100 Subject: [PATCH 06/65] Fix alert when duplicate auto columns exist --- .../app/[application]/data/table/[tableId]/index.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/index.svelte index 414722a177..a68a782bed 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/table/[tableId]/index.svelte @@ -53,7 +53,8 @@ } .alert-wrap { display: flex; - width: 100%; + flex: 0 0 auto; + margin: -28px -40px 14px -40px; } .alert-wrap :global(> *) { flex: 1; From 5e8e4add4a650604357d5c78c77c7ee22a25c399 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 17 Oct 2023 10:53:08 +0100 Subject: [PATCH 07/65] Make it less painful to delete columns --- .../backend/DataTable/modals/CreateEditColumn.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 7b51e6c839..ff9500f226 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -777,7 +777,8 @@ disabled={deleteColName !== originalName} >

- Are you sure you wish to delete the column {originalName}? + Are you sure you wish to delete the column + (deleteColName = originalName)}>{originalName}? Your data will be deleted and this action cannot be undone - enter the column name to confirm.

From 4e703fdfcc0e644d08fc6de3c73633dcf39bb39b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 17 Oct 2023 11:16:15 +0100 Subject: [PATCH 08/65] Remove concept of rendered columns from grid and instead render all columns --- .../components/grid/cells/HeaderCell.svelte | 4 +- .../grid/cells/RelationshipCell.svelte | 7 ++ .../components/grid/layout/GridBody.svelte | 6 +- .../src/components/grid/layout/GridRow.svelte | 4 +- .../grid/layout/GridScrollWrapper.svelte | 7 +- .../components/grid/layout/HeaderRow.svelte | 4 +- .../grid/layout/NewColumnButton.svelte | 9 +-- .../src/components/grid/layout/NewRow.svelte | 46 ++++++------ .../grid/overlays/ResizeOverlay.svelte | 4 +- .../src/components/grid/stores/viewport.js | 75 ++----------------- 10 files changed, 53 insertions(+), 113 deletions(-) diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index f367e3427f..2397f964e8 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -17,7 +17,7 @@ isResizing, rand, sort, - renderedColumns, + visibleColumns, dispatch, subscribe, config, @@ -50,7 +50,7 @@ $: sortedBy = column.name === $sort.column $: canMoveLeft = orderable && idx > 0 - $: canMoveRight = orderable && idx < $renderedColumns.length - 1 + $: canMoveRight = orderable && idx < $visibleColumns.length - 1 $: sortingLabels = getSortingLabels(column.schema?.type) $: searchable = isColumnSearchable(column) $: resetSearchValue(column.name) diff --git a/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte b/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte index 925c840478..d15acce4fc 100644 --- a/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte @@ -444,6 +444,13 @@ text-decoration: underline; } + .remove { + display: none; + } + .remove.visible { + display: block; + } + .add { background: var(--spectrum-global-color-gray-200); padding: 4px; diff --git a/packages/frontend-core/src/components/grid/layout/GridBody.svelte b/packages/frontend-core/src/components/grid/layout/GridBody.svelte index 762985a4db..0bb2a51fb4 100644 --- a/packages/frontend-core/src/components/grid/layout/GridBody.svelte +++ b/packages/frontend-core/src/components/grid/layout/GridBody.svelte @@ -7,7 +7,7 @@ const { bounds, renderedRows, - renderedColumns, + visibleColumns, rowVerticalInversionIndex, hoveredRowId, dispatch, @@ -17,7 +17,7 @@ let body - $: renderColumnsWidth = $renderedColumns.reduce( + $: columnsWidth = $visibleColumns.reduce( (total, col) => (total += col.width), 0 ) @@ -47,7 +47,7 @@
($hoveredRowId = BlankRowID)} on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)} on:click={() => dispatch("add-row-inline")} diff --git a/packages/frontend-core/src/components/grid/layout/GridRow.svelte b/packages/frontend-core/src/components/grid/layout/GridRow.svelte index 4754d493bf..93dc11f6ed 100644 --- a/packages/frontend-core/src/components/grid/layout/GridRow.svelte +++ b/packages/frontend-core/src/components/grid/layout/GridRow.svelte @@ -10,7 +10,7 @@ focusedCellId, reorder, selectedRows, - renderedColumns, + visibleColumns, hoveredRowId, selectedCellMap, focusedRow, @@ -34,7 +34,7 @@ on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)} on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))} > - {#each $renderedColumns as column, columnIdx (column.name)} + {#each $visibleColumns as column, columnIdx} {@const cellId = `${row._id}-${column.name}`} { - const offsetX = scrollHorizontally ? -1 * scroll.left + hiddenWidths : 0 + const generateStyle = (scroll, rowHeight) => { + const offsetX = scrollHorizontally ? -1 * scroll.left : 0 const offsetY = scrollVertically ? -1 * (scroll.top % rowHeight) : 0 return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);` } diff --git a/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte b/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte index 97b7d054f3..b8655b98b3 100644 --- a/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte +++ b/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte @@ -5,14 +5,14 @@ import HeaderCell from "../cells/HeaderCell.svelte" import { TempTooltip, TooltipType } from "@budibase/bbui" - const { renderedColumns, config, hasNonAutoColumn, datasource, loading } = + const { visibleColumns, config, hasNonAutoColumn, datasource, loading } = getContext("grid")
- {#each $renderedColumns as column, idx} + {#each $visibleColumns as column, idx} diff --git a/packages/frontend-core/src/components/grid/layout/NewColumnButton.svelte b/packages/frontend-core/src/components/grid/layout/NewColumnButton.svelte index d131df26e5..46e9b40fb6 100644 --- a/packages/frontend-core/src/components/grid/layout/NewColumnButton.svelte +++ b/packages/frontend-core/src/components/grid/layout/NewColumnButton.svelte @@ -2,17 +2,16 @@ import { getContext, onMount } from "svelte" import { Icon, Popover, clickOutside } from "@budibase/bbui" - const { renderedColumns, scroll, hiddenColumnsWidth, width, subscribe } = - getContext("grid") + const { visibleColumns, scroll, width, subscribe } = getContext("grid") let anchor let open = false - $: columnsWidth = $renderedColumns.reduce( + $: columnsWidth = $visibleColumns.reduce( (total, col) => (total += col.width), 0 ) - $: end = $hiddenColumnsWidth + columnsWidth - 1 - $scroll.left + $: end = columnsWidth - 1 - $scroll.left $: left = Math.min($width - 40, end) const close = () => { @@ -34,7 +33,7 @@
- {#each $renderedColumns as column, columnIdx} + {#each $visibleColumns as column, columnIdx} {@const cellId = `new-${column.name}`} - {#key cellId} - = $columnHorizontalInversionIndex} - {invertY} - > - {#if column?.schema?.autocolumn} -
Can't edit auto column
- {/if} - {#if isAdding} -
- {/if} - - {/key} + = $columnHorizontalInversionIndex} + {invertY} + > + {#if column?.schema?.autocolumn} +
Can't edit auto column
+ {/if} + {#if isAdding} +
+ {/if} + {/each}
diff --git a/packages/frontend-core/src/components/grid/overlays/ResizeOverlay.svelte b/packages/frontend-core/src/components/grid/overlays/ResizeOverlay.svelte index 13e158b300..9e584ab610 100644 --- a/packages/frontend-core/src/components/grid/overlays/ResizeOverlay.svelte +++ b/packages/frontend-core/src/components/grid/overlays/ResizeOverlay.svelte @@ -2,7 +2,7 @@ import { getContext } from "svelte" import { GutterWidth } from "../lib/constants" - const { resize, renderedColumns, stickyColumn, isReordering, scrollLeft } = + const { resize, visibleColumns, stickyColumn, isReordering, scrollLeft } = getContext("grid") $: offset = GutterWidth + ($stickyColumn?.width || 0) @@ -26,7 +26,7 @@
{/if} - {#each $renderedColumns as column} + {#each $visibleColumns as column}
{ [] ) - // Derive visible columns - const scrollLeftRounded = derived(scrollLeft, $scrollLeft => { - const interval = MinColumnWidth - return Math.round($scrollLeft / interval) * interval - }) - const renderedColumns = derived( - [visibleColumns, scrollLeftRounded, width], - ([$visibleColumns, $scrollLeft, $width], set) => { - if (!$visibleColumns.length) { - set([]) - return - } - let startColIdx = 0 - let rightEdge = $visibleColumns[0].width - while ( - rightEdge < $scrollLeft && - startColIdx < $visibleColumns.length - 1 - ) { - startColIdx++ - rightEdge += $visibleColumns[startColIdx].width - } - let endColIdx = startColIdx + 1 - let leftEdge = rightEdge - while ( - leftEdge < $width + $scrollLeft && - endColIdx < $visibleColumns.length - ) { - leftEdge += $visibleColumns[endColIdx].width - endColIdx++ - } - // Render an additional column on either side to account for - // debounce column updates based on scroll position - const next = $visibleColumns.slice( - Math.max(0, startColIdx - 1), - endColIdx + 1 - ) - const current = get(renderedColumns) - if (JSON.stringify(next) !== JSON.stringify(current)) { - set(next) - } - } - ) - - const hiddenColumnsWidth = derived( - [renderedColumns, visibleColumns], - ([$renderedColumns, $visibleColumns]) => { - const idx = $visibleColumns.findIndex( - col => col.name === $renderedColumns[0]?.name - ) - let width = 0 - if (idx > 0) { - for (let i = 0; i < idx; i++) { - width += $visibleColumns[i].width - } - } - return width - }, - 0 - ) - // Determine the row index at which we should start vertically inverting cell // dropdowns const rowVerticalInversionIndex = derived( @@ -130,12 +69,12 @@ export const deriveStores = context => { // Determine the column index at which we should start horizontally inverting // cell dropdowns const columnHorizontalInversionIndex = derived( - [renderedColumns, scrollLeft, width], - ([$renderedColumns, $scrollLeft, $width]) => { + [visibleColumns, scrollLeft, width], + ([$visibleColumns, $scrollLeft, $width]) => { const cutoff = $width + $scrollLeft - ScrollBarSize * 3 - let inversionIdx = $renderedColumns.length - for (let i = $renderedColumns.length - 1; i >= 0; i--, inversionIdx--) { - const rightEdge = $renderedColumns[i].left + $renderedColumns[i].width + let inversionIdx = $visibleColumns.length + for (let i = $visibleColumns.length - 1; i >= 0; i--, inversionIdx--) { + const rightEdge = $visibleColumns[i].left + $visibleColumns[i].width if (rightEdge + MaxCellRenderWidthOverflow <= cutoff) { break } @@ -148,8 +87,6 @@ export const deriveStores = context => { scrolledRowCount, visualRowCapacity, renderedRows, - renderedColumns, - hiddenColumnsWidth, rowVerticalInversionIndex, columnHorizontalInversionIndex, } From c37538d61125734a32aeb0a178843c61f5b63e25 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 17 Oct 2023 11:51:42 +0100 Subject: [PATCH 09/65] Use CSS content-visibility to improve rendering performance by hiding offscreen grid cells --- .../src/components/grid/cells/DataCell.svelte | 2 + .../src/components/grid/cells/GridCell.svelte | 5 +++ .../src/components/grid/layout/GridRow.svelte | 2 + .../src/components/grid/layout/NewRow.svelte | 2 + .../src/components/grid/stores/viewport.js | 45 ++++++++++++++++++- 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/frontend-core/src/components/grid/cells/DataCell.svelte b/packages/frontend-core/src/components/grid/cells/DataCell.svelte index f9cdef3756..cdaf28978a 100644 --- a/packages/frontend-core/src/components/grid/cells/DataCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/DataCell.svelte @@ -21,6 +21,7 @@ export let invertX = false export let invertY = false export let contentLines = 1 + export let hidden = false const emptyError = writable(null) @@ -78,6 +79,7 @@ {focused} {selectedUser} {readonly} + {hidden} error={$error} on:click={() => focusedCellId.set(cellId)} on:contextmenu={e => menu.actions.open(cellId, e)} diff --git a/packages/frontend-core/src/components/grid/cells/GridCell.svelte b/packages/frontend-core/src/components/grid/cells/GridCell.svelte index fe4bd70ba4..dcc76b9c75 100644 --- a/packages/frontend-core/src/components/grid/cells/GridCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/GridCell.svelte @@ -10,6 +10,7 @@ export let defaultHeight = false export let center = false export let readonly = false + export let hidden = false $: style = getStyle(width, selectedUser) @@ -30,6 +31,7 @@ class:error class:center class:readonly + class:hidden class:default-height={defaultHeight} class:selected-other={selectedUser != null} class:alt={rowIdx % 2 === 1} @@ -81,6 +83,9 @@ .cell.center { align-items: center; } + .cell.hidden { + content-visibility: hidden; + } /* Cell border */ .cell.focused:after, diff --git a/packages/frontend-core/src/components/grid/layout/GridRow.svelte b/packages/frontend-core/src/components/grid/layout/GridRow.svelte index 93dc11f6ed..4a0db40ee8 100644 --- a/packages/frontend-core/src/components/grid/layout/GridRow.svelte +++ b/packages/frontend-core/src/components/grid/layout/GridRow.svelte @@ -19,6 +19,7 @@ isDragging, dispatch, rows, + columnRenderMap, } = getContext("grid") $: rowSelected = !!$selectedRows[row._id] @@ -51,6 +52,7 @@ selectedUser={$selectedCellMap[cellId]} width={column.width} contentLines={$contentLines} + hidden={!$columnRenderMap[column.name]} /> {/each}
diff --git a/packages/frontend-core/src/components/grid/layout/NewRow.svelte b/packages/frontend-core/src/components/grid/layout/NewRow.svelte index cc2d76f536..980f86326d 100644 --- a/packages/frontend-core/src/components/grid/layout/NewRow.svelte +++ b/packages/frontend-core/src/components/grid/layout/NewRow.svelte @@ -31,6 +31,7 @@ refreshing, config, filter, + columnRenderMap, } = getContext("grid") let visible = false @@ -224,6 +225,7 @@ topRow={offset === 0} invertX={columnIdx >= $columnHorizontalInversionIndex} {invertY} + hidden={!$columnRenderMap[column.name]} > {#if column?.schema?.autocolumn}
Can't edit auto column
diff --git a/packages/frontend-core/src/components/grid/stores/viewport.js b/packages/frontend-core/src/components/grid/stores/viewport.js index 3a1b65e2b9..0890792989 100644 --- a/packages/frontend-core/src/components/grid/stores/viewport.js +++ b/packages/frontend-core/src/components/grid/stores/viewport.js @@ -1,7 +1,8 @@ -import { derived } from "svelte/store" +import { derived, get } from "svelte/store" import { MaxCellRenderHeight, MaxCellRenderWidthOverflow, + MinColumnWidth, ScrollBarSize, } from "../lib/constants" @@ -44,6 +45,47 @@ export const deriveStores = context => { [] ) + // Derive visible columns + const scrollLeftRounded = derived(scrollLeft, $scrollLeft => { + const interval = MinColumnWidth + return Math.round($scrollLeft / interval) * interval + }) + const columnRenderMap = derived( + [visibleColumns, scrollLeftRounded, width], + ([$visibleColumns, $scrollLeft, $width]) => { + if (!$visibleColumns.length) { + return {} + } + let startColIdx = 0 + let rightEdge = $visibleColumns[0].width + while ( + rightEdge < $scrollLeft && + startColIdx < $visibleColumns.length - 1 + ) { + startColIdx++ + rightEdge += $visibleColumns[startColIdx].width + } + let endColIdx = startColIdx + 1 + let leftEdge = rightEdge + while ( + leftEdge < $width + $scrollLeft && + endColIdx < $visibleColumns.length + ) { + leftEdge += $visibleColumns[endColIdx].width + endColIdx++ + } + + // Only update the store if different + let next = {} + $visibleColumns + .slice(Math.max(0, startColIdx), endColIdx) + .forEach(col => { + next[col.name] = true + }) + return next + } + ) + // Determine the row index at which we should start vertically inverting cell // dropdowns const rowVerticalInversionIndex = derived( @@ -87,6 +129,7 @@ export const deriveStores = context => { scrolledRowCount, visualRowCapacity, renderedRows, + columnRenderMap, rowVerticalInversionIndex, columnHorizontalInversionIndex, } From dadb36827932c16bd5f71ea1a65ed469488855bb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 18 Oct 2023 08:31:29 +0100 Subject: [PATCH 10/65] Lint and add hover styles for deleting prompts --- .../backend/DataTable/modals/CreateEditColumn.svelte | 7 +++++++ .../src/components/grid/cells/RelationshipCell.svelte | 7 ------- .../frontend-core/src/components/grid/stores/viewport.js | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index ff9500f226..467ae413c3 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -811,4 +811,11 @@ gap: 8px; display: flex; } + b { + transition: color 130ms ease-out; + } + b:hover { + cursor: pointer; + color: var(--spectrum-global-color-gray-900); + } diff --git a/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte b/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte index d15acce4fc..925c840478 100644 --- a/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte @@ -444,13 +444,6 @@ text-decoration: underline; } - .remove { - display: none; - } - .remove.visible { - display: block; - } - .add { background: var(--spectrum-global-color-gray-200); padding: 4px; diff --git a/packages/frontend-core/src/components/grid/stores/viewport.js b/packages/frontend-core/src/components/grid/stores/viewport.js index 0890792989..8df8acd0f4 100644 --- a/packages/frontend-core/src/components/grid/stores/viewport.js +++ b/packages/frontend-core/src/components/grid/stores/viewport.js @@ -1,4 +1,4 @@ -import { derived, get } from "svelte/store" +import { derived } from "svelte/store" import { MaxCellRenderHeight, MaxCellRenderWidthOverflow, From ad54b31667cffa18c9ccf7971fc1af8085a9e233 Mon Sep 17 00:00:00 2001 From: Dean Date: Thu, 19 Oct 2023 12:00:47 +0100 Subject: [PATCH 11/65] Moved label align and button size into styles section of form block. Added tagged component settings section to the style section. General settings and sections now have a tag property for filtering. --- .../Component/ComponentSettingsPanel.svelte | 7 +- .../Component/ComponentSettingsSection.svelte | 20 ++++-- .../Component/DesignSection.svelte | 15 +++++ packages/client/manifest.json | 66 ++++++++++--------- 4 files changed, 70 insertions(+), 38 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte index afcada4138..8b9ff0ae2c 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte @@ -65,7 +65,12 @@ /> {/if} {#if section == "styles"} - + { const settings = definition?.settings ?? [] - const generalSettings = settings.filter(setting => !setting.section) - const customSections = settings.filter(setting => setting.section) + const generalSettings = settings.filter( + setting => !setting.section && setting.tag === tag + ) + const customSections = settings.filter( + setting => setting.section && setting.tag === tag + ) let sections = [ { name: "General", @@ -131,7 +141,7 @@ - {:else if idx === 0 && section.name === "General" && componentDefinition.info} + {:else if idx === 0 && section.name === "General" && componentDefinition.info && !tag} {/if} {/each} -{#if componentDefinition?.block} +{#if componentDefinition?.block && !tag} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/DesignSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/DesignSection.svelte index 444ded7e1f..def1fcf24b 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/DesignSection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/DesignSection.svelte @@ -1,10 +1,12 @@ + + + {#if styles?.length > 0} {#each styles as style} Date: Fri, 20 Oct 2023 10:38:34 +0100 Subject: [PATCH 12/65] Feedback update, add tag to getsections fn definition --- .../_components/Component/ComponentSettingsSection.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte index f2d6c9cb52..6eb4b4c440 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte @@ -26,7 +26,7 @@ tag ) - const getSections = (instance, definition, isScreen) => { + const getSections = (instance, definition, isScreen, tag) => { const settings = definition?.settings ?? [] const generalSettings = settings.filter( setting => !setting.section && setting.tag === tag From 871891c2a2f2e25cb1e821a9dfe8b7c19c199aaf Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 20 Oct 2023 15:35:16 +0100 Subject: [PATCH 13/65] Parse view v1 schema to fix builder crashed caused by 'groupedby' views --- .../components/backend/DataTable/Table.svelte | 44 ++++++++++--------- .../backend/DataTable/ViewDataTable.svelte | 16 ++++++- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/Table.svelte b/packages/builder/src/components/backend/DataTable/Table.svelte index f8087d8a39..45e440eca5 100644 --- a/packages/builder/src/components/backend/DataTable/Table.svelte +++ b/packages/builder/src/components/backend/DataTable/Table.svelte @@ -24,17 +24,21 @@ let selectedRows = [] let customRenderers = [] + let parsedSchema = {} + + $: if (schema) { + parsedSchema = Object.keys(schema).reduce((acc, key) => { + acc[key] = { ...schema[key] } + if (!canBeSortColumn(acc[key].type)) { + acc[key].sortable = false + } + return acc + }, {}) + } $: selectedRows, dispatch("selectionUpdated", selectedRows) $: isUsersTable = tableId === TableNames.USERS $: data && resetSelectedRows() - $: { - Object.values(schema || {}).forEach(col => { - if (!canBeSortColumn(col.type)) { - col.sortable = false - } - }) - } $: { if (isUsersTable) { customRenderers = [ @@ -44,24 +48,24 @@ }, ] UNEDITABLE_USER_FIELDS.forEach(field => { - if (schema[field]) { - schema[field].editable = false + if (parsedSchema[field]) { + parsedSchema[field].editable = false } }) - if (schema.email) { - schema.email.displayName = "Email" + if (parsedSchema.email) { + parsedSchema.email.displayName = "Email" } - if (schema.roleId) { - schema.roleId.displayName = "Role" + if (parsedSchema.roleId) { + parsedSchema.roleId.displayName = "Role" } - if (schema.firstName) { - schema.firstName.displayName = "First Name" + if (parsedSchema.firstName) { + parsedSchema.firstName.displayName = "First Name" } - if (schema.lastName) { - schema.lastName.displayName = "Last Name" + if (parsedSchema.lastName) { + parsedSchema.lastName.displayName = "Last Name" } - if (schema.status) { - schema.status.displayName = "Status" + if (parsedSchema.status) { + parsedSchema.status.displayName = "Status" } } } @@ -97,7 +101,7 @@
{ + if (!view.groupBy) { + return view.schema + } + const { group, field, value } = view.schema + return { + group, + field: { type: field }, + value: { type: value }, + } + } + $: name = view.name + $: schema = parseSchema(view) $: calculation = view.calculation $: supportedFormats = Object.values(ROW_EXPORT_FORMATS).filter(key => { @@ -61,7 +75,7 @@
Date: Mon, 23 Oct 2023 16:47:05 +0100 Subject: [PATCH 14/65] Show 'Creator' instead of 'Admin' for the global user role picker --- .../src/components/common/RoleSelect.svelte | 15 ++++++++++++--- .../_components/BuilderSidePanel.svelte | 11 +++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/components/common/RoleSelect.svelte b/packages/builder/src/components/common/RoleSelect.svelte index 82752554d5..2df61926e1 100644 --- a/packages/builder/src/components/common/RoleSelect.svelte +++ b/packages/builder/src/components/common/RoleSelect.svelte @@ -39,7 +39,15 @@ allowCreator ) => { if (allowedRoles?.length) { - return roles.filter(role => allowedRoles.includes(role._id)) + const filteredRoles = roles.filter(role => + allowedRoles.includes(role._id) + ) + return [ + ...filteredRoles, + ...(allowedRoles.includes(Constants.Roles.CREATOR) + ? [{ _id: Constants.Roles.CREATOR, name: "Creator", enabled: false }] + : []), + ] } let newRoles = [...roles] @@ -129,8 +137,9 @@ getOptionColour={getColor} getOptionIcon={getIcon} isOptionEnabled={option => - option._id !== Constants.Roles.CREATOR || - $licensing.perAppBuildersEnabled} + (option._id !== Constants.Roles.CREATOR || + $licensing.perAppBuildersEnabled) && + option.enabled !== false} {placeholder} {error} /> diff --git a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte index a7d9584330..f9a40b09a6 100644 --- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte @@ -516,6 +516,13 @@ } return null } + + const parseRole = user => { + if (user.isAdminOrGlobalBuilder) { + return Constants.Roles.CREATOR + } + return user.role + } @@ -725,7 +732,7 @@ From cea1c04b73c508d96f1a1a828b964ec07b2858b5 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Tue, 24 Oct 2023 17:16:44 +0200 Subject: [PATCH 15/65] Creators count functionality --- .../backend-core/src/cache/writethrough.ts | 4 +- packages/backend-core/src/users/db.ts | 110 ++++++++++-------- packages/backend-core/src/users/users.ts | 2 +- .../tests/core/users/users.spec.js | 54 +++++++++ .../core/utilities/structures/licenses.ts | 8 ++ packages/types/src/sdk/featureFlag.ts | 3 + packages/types/src/sdk/licensing/billing.ts | 7 ++ packages/types/src/sdk/licensing/plan.ts | 4 + 8 files changed, 142 insertions(+), 50 deletions(-) create mode 100644 packages/backend-core/tests/core/users/users.spec.js diff --git a/packages/backend-core/src/cache/writethrough.ts b/packages/backend-core/src/cache/writethrough.ts index e64c116663..c331d791a6 100644 --- a/packages/backend-core/src/cache/writethrough.ts +++ b/packages/backend-core/src/cache/writethrough.ts @@ -119,8 +119,8 @@ export class Writethrough { this.writeRateMs = writeRateMs } - async put(doc: any) { - return put(this.db, doc, this.writeRateMs) + async put(doc: any, writeRateMs: number = this.writeRateMs) { + return put(this.db, doc, writeRateMs) } async get(id: string) { diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index a2539e836e..daa09bee6f 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -25,12 +25,17 @@ import { import { getAccountHolderFromUserIds, isAdmin, + isCreator, validateUniqueUser, } from "./utils" import { searchExistingEmails } from "./lookup" import { hash } from "../utils" -type QuotaUpdateFn = (change: number, cb?: () => Promise) => Promise +type QuotaUpdateFn = ( + change: number, + creatorsChange: number, + cb?: () => Promise +) => Promise type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise type FeatureFn = () => Promise type GroupGetFn = (ids: string[]) => Promise @@ -245,7 +250,8 @@ export class UserDB { } const change = dbUser ? 0 : 1 // no change if there is existing user - return UserDB.quotas.addUsers(change, async () => { + const creatorsChange = isCreator(dbUser) !== isCreator(user) ? 1 : 0 + return UserDB.quotas.addUsers(change, creatorsChange, async () => { await validateUniqueUser(email, tenantId) let builtUser = await UserDB.buildUser(user, opts, tenantId, dbUser) @@ -307,6 +313,7 @@ export class UserDB { let usersToSave: any[] = [] let newUsers: any[] = [] + let newCreators: any[] = [] const emails = newUsersRequested.map((user: User) => user.email) const existingEmails = await searchExistingEmails(emails) @@ -327,59 +334,66 @@ export class UserDB { } newUser.userGroups = groups newUsers.push(newUser) + if (isCreator(newUser)) { + newCreators.push(newUser) + } } const account = await accountSdk.getAccountByTenantId(tenantId) - return UserDB.quotas.addUsers(newUsers.length, async () => { - // create the promises array that will be called by bulkDocs - newUsers.forEach((user: any) => { - usersToSave.push( - UserDB.buildUser( - user, - { - hashPassword: true, - requirePassword: user.requirePassword, - }, - tenantId, - undefined, // no dbUser - account + return UserDB.quotas.addUsers( + newUsers.length, + newCreators.length, + async () => { + // create the promises array that will be called by bulkDocs + newUsers.forEach((user: any) => { + usersToSave.push( + UserDB.buildUser( + user, + { + hashPassword: true, + requirePassword: user.requirePassword, + }, + tenantId, + undefined, // no dbUser + account + ) ) - ) - }) + }) - const usersToBulkSave = await Promise.all(usersToSave) - await usersCore.bulkUpdateGlobalUsers(usersToBulkSave) + const usersToBulkSave = await Promise.all(usersToSave) + await usersCore.bulkUpdateGlobalUsers(usersToBulkSave) - // Post-processing of bulk added users, e.g. events and cache operations - for (const user of usersToBulkSave) { - // TODO: Refactor to bulk insert users into the info db - // instead of relying on looping tenant creation - await platform.users.addUser(tenantId, user._id, user.email) - await eventHelpers.handleSaveEvents(user, undefined) - } + // Post-processing of bulk added users, e.g. events and cache operations + for (const user of usersToBulkSave) { + // TODO: Refactor to bulk insert users into the info db + // instead of relying on looping tenant creation + await platform.users.addUser(tenantId, user._id, user.email) + await eventHelpers.handleSaveEvents(user, undefined) + } + + const saved = usersToBulkSave.map(user => { + return { + _id: user._id, + email: user.email, + } + }) + + // now update the groups + if (Array.isArray(saved) && groups) { + const groupPromises = [] + const createdUserIds = saved.map(user => user._id) + for (let groupId of groups) { + groupPromises.push(UserDB.groups.addUsers(groupId, createdUserIds)) + } + await Promise.all(groupPromises) + } - const saved = usersToBulkSave.map(user => { return { - _id: user._id, - email: user.email, + successful: saved, + unsuccessful, } - }) - - // now update the groups - if (Array.isArray(saved) && groups) { - const groupPromises = [] - const createdUserIds = saved.map(user => user._id) - for (let groupId of groups) { - groupPromises.push(UserDB.groups.addUsers(groupId, createdUserIds)) - } - await Promise.all(groupPromises) } - - return { - successful: saved, - unsuccessful, - } - }) + ) } static async bulkDelete(userIds: string[]): Promise { @@ -419,11 +433,12 @@ export class UserDB { _deleted: true, })) const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete) + const creatorsToDelete = usersToDelete.filter(isCreator) - await UserDB.quotas.removeUsers(toDelete.length) for (let user of usersToDelete) { await bulkDeleteProcessing(user) } + await UserDB.quotas.removeUsers(toDelete.length, creatorsToDelete.length) // Build Response // index users by id @@ -472,7 +487,8 @@ export class UserDB { await db.remove(userId, dbUser._rev) - await UserDB.quotas.removeUsers(1) + const creatorsToDelete = isCreator(dbUser) ? 1 : 0 + await UserDB.quotas.removeUsers(1, creatorsToDelete) await eventHelpers.handleDeleteEvents(dbUser) await cache.user.invalidateUser(userId) await sessions.invalidateSessions(userId, { reason: "deletion" }) diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index 6237c23972..bad108ab84 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -14,11 +14,11 @@ import { } from "../db" import { BulkDocsResponse, - ContextUser, SearchQuery, SearchQueryOperators, SearchUsersRequest, User, + ContextUser, DatabaseQueryOpts, } from "@budibase/types" import { getGlobalDB } from "../context" diff --git a/packages/backend-core/tests/core/users/users.spec.js b/packages/backend-core/tests/core/users/users.spec.js new file mode 100644 index 0000000000..ae7109344a --- /dev/null +++ b/packages/backend-core/tests/core/users/users.spec.js @@ -0,0 +1,54 @@ +const _ = require('lodash/fp') +const {structures} = require("../../../tests") + +jest.mock("../../../src/context") +jest.mock("../../../src/db") + +const context = require("../../../src/context") +const db = require("../../../src/db") + +const {getCreatorCount} = require('../../../src/users/users') + +describe("Users", () => { + + let getGlobalDBMock + let getGlobalUserParamsMock + let paginationMock + + beforeEach(() => { + jest.resetAllMocks() + + getGlobalDBMock = jest.spyOn(context, "getGlobalDB") + getGlobalUserParamsMock = jest.spyOn(db, "getGlobalUserParams") + paginationMock = jest.spyOn(db, "pagination") + }) + + it("Retrieves the number of creators", async () => { + const getUsers = (offset, limit, creators = false) => { + const range = _.range(offset, limit) + const opts = creators ? {builder: {global: true}} : undefined + return range.map(() => structures.users.user(opts)) + } + const page1Data = getUsers(0, 8) + const page2Data = getUsers(8, 12, true) + getGlobalDBMock.mockImplementation(() => ({ + name : "fake-db", + allDocs: () => ({ + rows: [...page1Data, ...page2Data] + }) + })) + paginationMock.mockImplementationOnce(() => ({ + data: page1Data, + hasNextPage: true, + nextPage: "1" + })) + paginationMock.mockImplementation(() => ({ + data: page2Data, + hasNextPage: false, + nextPage: undefined + })) + const creatorsCount = await getCreatorCount() + expect(creatorsCount).toBe(4) + expect(paginationMock).toHaveBeenCalledTimes(2) + }) +}) diff --git a/packages/backend-core/tests/core/utilities/structures/licenses.ts b/packages/backend-core/tests/core/utilities/structures/licenses.ts index 0e34f2e9bb..bb452f9ad5 100644 --- a/packages/backend-core/tests/core/utilities/structures/licenses.ts +++ b/packages/backend-core/tests/core/utilities/structures/licenses.ts @@ -123,6 +123,10 @@ export function customer(): Customer { export function subscription(): Subscription { return { amount: 10000, + amounts: { + user: 10000, + creator: 0, + }, cancelAt: undefined, currency: "usd", currentPeriodEnd: 0, @@ -131,6 +135,10 @@ export function subscription(): Subscription { duration: PriceDuration.MONTHLY, pastDueAt: undefined, quantity: 0, + quantities: { + user: 0, + creator: 0, + }, status: "active", } } diff --git a/packages/types/src/sdk/featureFlag.ts b/packages/types/src/sdk/featureFlag.ts index 53aa4842c4..e3935bc7ee 100644 --- a/packages/types/src/sdk/featureFlag.ts +++ b/packages/types/src/sdk/featureFlag.ts @@ -1,5 +1,8 @@ export enum FeatureFlag { LICENSING = "LICENSING", + // Feature IDs in Posthog + PER_CREATOR_PER_USER_PRICE = "18873", + PER_CREATOR_PER_USER_PRICE_ALERT = "18530", } export interface TenantFeatureFlags { diff --git a/packages/types/src/sdk/licensing/billing.ts b/packages/types/src/sdk/licensing/billing.ts index 35f366c811..bcbc7abd18 100644 --- a/packages/types/src/sdk/licensing/billing.ts +++ b/packages/types/src/sdk/licensing/billing.ts @@ -5,10 +5,17 @@ export interface Customer { currency: string | null | undefined } +export interface SubscriptionItems { + user: number | undefined + creator: number | undefined +} + export interface Subscription { amount: number + amounts: SubscriptionItems | undefined currency: string quantity: number + quantities: SubscriptionItems | undefined duration: PriceDuration cancelAt: number | null | undefined currentPeriodStart: number diff --git a/packages/types/src/sdk/licensing/plan.ts b/packages/types/src/sdk/licensing/plan.ts index 3e214a01ff..1604dfb8af 100644 --- a/packages/types/src/sdk/licensing/plan.ts +++ b/packages/types/src/sdk/licensing/plan.ts @@ -4,7 +4,9 @@ export enum PlanType { PRO = "pro", /** @deprecated */ TEAM = "team", + /** @deprecated */ PREMIUM = "premium", + PREMIUM_PLUS = "premium_plus", BUSINESS = "business", ENTERPRISE = "enterprise", } @@ -26,10 +28,12 @@ export interface AvailablePrice { currency: string duration: PriceDuration priceId: string + type?: string } export enum PlanModel { PER_USER = "perUser", + PER_CREATOR_PER_USER = "per_creator_per_user", DAY_PASS = "dayPass", } From 102a0824844f58a51ba784290271ed7e1fcdd10c Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Tue, 24 Oct 2023 17:18:13 +0200 Subject: [PATCH 16/65] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index d24c0dc3a3..39bff12817 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit d24c0dc3a30014cbe61860252aa48104cad36376 +Subproject commit 39bff1281715c647f5d0c1db9bbf1d53c9fd4fc6 From b952892c42587c04d1bcc1608c156de215178d23 Mon Sep 17 00:00:00 2001 From: Dean Date: Wed, 25 Oct 2023 12:08:56 +0100 Subject: [PATCH 17/65] Generalised the fix as not all calculations will have a groupby per PR feedback --- .../src/components/backend/DataTable/Table.svelte | 4 +++- .../backend/DataTable/ViewDataTable.svelte | 15 +-------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/Table.svelte b/packages/builder/src/components/backend/DataTable/Table.svelte index 45e440eca5..f7eccd5242 100644 --- a/packages/builder/src/components/backend/DataTable/Table.svelte +++ b/packages/builder/src/components/backend/DataTable/Table.svelte @@ -28,7 +28,9 @@ $: if (schema) { parsedSchema = Object.keys(schema).reduce((acc, key) => { - acc[key] = { ...schema[key] } + acc[key] = + typeof schema[key] === "string" ? { type: schema[key] } : schema[key] + if (!canBeSortColumn(acc[key].type)) { acc[key].sortable = false } diff --git a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte index 12e4e3c270..6068439dbd 100644 --- a/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/ViewDataTable.svelte @@ -19,21 +19,8 @@ let loading = false let type = "internal" - // Grouped views have a fixed schema layout - const parseSchema = view => { - if (!view.groupBy) { - return view.schema - } - const { group, field, value } = view.schema - return { - group, - field: { type: field }, - value: { type: value }, - } - } - $: name = view.name - $: schema = parseSchema(view) + $: schema = view.schema $: calculation = view.calculation $: supportedFormats = Object.values(ROW_EXPORT_FORMATS).filter(key => { From dccc60c9b083a5a03aa2bca491474ec311330ded Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Thu, 26 Oct 2023 07:05:00 +0000 Subject: [PATCH 18/65] Bump version to 2.11.45 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 7d14875c97..384473120b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.11.44", + "version": "2.11.45", "npmClient": "yarn", "packages": [ "packages/*" From 7ddaf6479f934af59832269f06bc2f4c144aad17 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 26 Oct 2023 09:54:38 +0200 Subject: [PATCH 19/65] Clean unused pipelines --- .../workflows/release-singleimage-test.yml | 72 ------------------- package.json | 1 - 2 files changed, 73 deletions(-) delete mode 100644 .github/workflows/release-singleimage-test.yml diff --git a/.github/workflows/release-singleimage-test.yml b/.github/workflows/release-singleimage-test.yml deleted file mode 100644 index c3a14226ce..0000000000 --- a/.github/workflows/release-singleimage-test.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Test - -on: - workflow_dispatch: - -env: - CI: true - PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - REGISTRY_URL: registry.hub.docker.com - NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} -jobs: - build: - name: "build" - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18.x] - steps: - - name: "Checkout" - uses: actions/checkout@v4 - with: - submodules: true - token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: "yarn" - - name: Setup QEMU - uses: docker/setup-qemu-action@v3 - - name: Setup Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v3 - - name: Run Yarn - run: yarn - - name: Run Yarn Build - run: yarn build --scope @budibase/server --scope @budibase/worker - - name: Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_API_KEY }} - - name: Get the latest release version - id: version - run: | - release_version=$(cat lerna.json | jq -r '.version') - echo $release_version - echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - - name: Tag and release Budibase service docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - pull: true - platforms: linux/amd64,linux/arm64 - build-args: BUDIBASE_VERSION=0.0.0+test - tags: budibase/budibase-test:test - file: ./hosting/single/Dockerfile.v2 - cache-from: type=registry,ref=budibase/budibase-test:test - cache-to: type=inline - - name: Tag and release Budibase Azure App Service docker image - uses: docker/build-push-action@v2 - with: - context: . - push: true - platforms: linux/amd64 - build-args: | - TARGETBUILD=aas - BUDIBASE_VERSION=0.0.0+test - tags: budibase/budibase-test:aas - file: ./hosting/single/Dockerfile.v2 diff --git a/package.json b/package.json index 100a306a35..d3f4903e6c 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "build:sdk": "lerna run --stream build:sdk", "deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular", "release": "lerna publish from-package --yes --force-publish --no-git-tag-version --no-push --no-git-reset", - "release:develop": "yarn release --dist-tag develop", "restore": "yarn run clean && yarn && yarn run build", "nuke": "yarn run nuke:packages && yarn run nuke:docker", "nuke:packages": "yarn run restore", From 389848efbefe4e37d3bf397d16fda5ad04e9abdc Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 26 Oct 2023 11:05:07 +0200 Subject: [PATCH 20/65] Add update versions v2 --- scripts/updateWorkepaceVersions.V2.sh | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100755 scripts/updateWorkepaceVersions.V2.sh diff --git a/scripts/updateWorkepaceVersions.V2.sh b/scripts/updateWorkepaceVersions.V2.sh new file mode 100755 index 0000000000..634bcbcfb0 --- /dev/null +++ b/scripts/updateWorkepaceVersions.V2.sh @@ -0,0 +1,8 @@ +#!/bin/bash +version=$1 +echo "Setting version $version" +yarn lerna exec "yarn version --no-git-tag-version --new-version=$version" +echo "Updating dependencies" +node scripts/syncLocalDependencies.js $version +echo "Syncing yarn workspace" +yarn From 6032216e2f4b9f21fb640f554fe1a68c731cce9e Mon Sep 17 00:00:00 2001 From: Dean Date: Thu, 26 Oct 2023 10:13:22 +0100 Subject: [PATCH 21/65] Added fix for scenario where componentdefinition was queried befor it existed. Fix to ensure an empty general settings block is not shown --- .../Component/ComponentSettingsSection.svelte | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte index cd91e974ce..6dc9078f2c 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte @@ -34,10 +34,14 @@ setting => setting.section && setting.tag === tag ) let sections = [ - { - name: "General", - settings: generalSettings, - }, + ...(generalSettings?.length + ? [ + { + name: "General", + settings: generalSettings, + }, + ] + : []), ...(customSections || []), ] @@ -142,7 +146,7 @@ - {:else if idx === 0 && section.name === "General" && componentDefinition.info && !tag} + {:else if idx === 0 && section.name === "General" && componentDefinition?.info && !tag} Date: Thu, 26 Oct 2023 11:24:02 +0200 Subject: [PATCH 22/65] Fix typo --- ...pdateWorkepaceVersions.V2.sh => updateWorkspaceVersions.V2.sh} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/{updateWorkepaceVersions.V2.sh => updateWorkspaceVersions.V2.sh} (100%) diff --git a/scripts/updateWorkepaceVersions.V2.sh b/scripts/updateWorkspaceVersions.V2.sh similarity index 100% rename from scripts/updateWorkepaceVersions.V2.sh rename to scripts/updateWorkspaceVersions.V2.sh From 5909b4a7c723c8e13fc735e7844fcf00331f8b0b Mon Sep 17 00:00:00 2001 From: Gerard Burns Date: Thu, 26 Oct 2023 11:24:43 +0100 Subject: [PATCH 23/65] Add template for grid blocks (#12086) * wip * linting --------- Co-authored-by: Mihail Hadzhiev <102024164+MihailHadzhiev2022@users.noreply.github.com> --- .../store/screenTemplates/rowListScreen.js | 22 ++++++++++++++---- .../NewScreen/CreateScreenModal.svelte | 8 +++++-- .../NewScreen/DatasourceModal.svelte | 6 ++++- .../design/_components/NewScreen/grid.png | Bin 0 -> 24819 bytes .../design/_components/NewScreen/index.svelte | 11 +++++++++ 5 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/grid.png diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js index b17bd99e10..59bcd0d5e8 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js @@ -2,14 +2,14 @@ import sanitizeUrl from "./utils/sanitizeUrl" import { Screen } from "./utils/Screen" import { Component } from "./utils/Component" -export default function (datasources) { +export default function (datasources, mode = "table") { if (!Array.isArray(datasources)) { return [] } return datasources.map(datasource => { return { name: `${datasource.label} - List`, - create: () => createScreen(datasource), + create: () => createScreen(datasource, mode), id: ROW_LIST_TEMPLATE, resourceId: datasource.resourceId, } @@ -40,10 +40,24 @@ const generateTableBlock = datasource => { return tableBlock } -const createScreen = datasource => { +const generateGridBlock = datasource => { + const gridBlock = new Component("@budibase/standard-components/gridblock") + gridBlock + .customProps({ + table: datasource, + }) + .instanceName(`${datasource.label} - Grid block`) + return gridBlock +} + +const createScreen = (datasource, mode) => { return new Screen() .route(rowListUrl(datasource)) .instanceName(`${datasource.label} - List`) - .addChild(generateTableBlock(datasource)) + .addChild( + mode === "table" + ? generateTableBlock(datasource) + : generateGridBlock(datasource) + ) .json() } diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte index 9a96242b30..92ed3dcfc7 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte @@ -12,6 +12,7 @@ import { capitalise } from "helpers" import { goto } from "@roxi/routify" + let mode let pendingScreen // Modal refs @@ -100,14 +101,15 @@ } // Handler for NewScreenModal - export const show = mode => { + export const show = newMode => { + mode = newMode selectedTemplates = null blankScreenUrl = null screenMode = mode pendingScreen = null screenAccessRole = Roles.BASIC - if (mode === "table") { + if (mode === "table" || mode === "grid") { datasourceModal.show() } else if (mode === "blank") { let templates = getTemplates($tables.list) @@ -123,6 +125,7 @@ // Handler for DatasourceModal confirmation, move to screen access select const confirmScreenDatasources = async ({ templates }) => { + console.log(templates) selectedTemplates = templates screenAccessRoleModal.show() } @@ -177,6 +180,7 @@ diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/DatasourceModal.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/DatasourceModal.svelte index a866cd23d4..731c60a406 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/DatasourceModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/DatasourceModal.svelte @@ -7,6 +7,7 @@ import rowListScreen from "builderStore/store/screenTemplates/rowListScreen" import DatasourceTemplateRow from "./DatasourceTemplateRow.svelte" + export let mode export let onCancel export let onConfirm export let initialScreens = [] @@ -24,7 +25,10 @@ screen => screen.resourceId !== resourceId ) } else { - selectedScreens = [...selectedScreens, rowListScreen([datasource])[0]] + selectedScreens = [ + ...selectedScreens, + rowListScreen([datasource], mode)[0], + ] } } diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/grid.png b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/grid.png new file mode 100644 index 0000000000000000000000000000000000000000..c3efa30a67d733434fade938263e4aba9fd7cdee GIT binary patch literal 24819 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

K!sjL$Lv!Wkn)K>4}0WK}T!0MY-`Gz%=`-%HF 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 y4eyTF0000View, edit and delete rows on a table + +

createScreenModal.show("grid")}> +
+ +
+
+ Grid + View and manipulate rows on a grid +
+
From 8db0ff79a6c6d8b5061b1e55a5aa30a6ab0af07e Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 26 Oct 2023 12:44:36 +0200 Subject: [PATCH 24/65] Increase yarn timeouts --- packages/server/Dockerfile.v2 | 4 ++-- packages/worker/Dockerfile.v2 | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/Dockerfile.v2 b/packages/server/Dockerfile.v2 index 881c21299e..f737570fcd 100644 --- a/packages/server/Dockerfile.v2 +++ b/packages/server/Dockerfile.v2 @@ -44,7 +44,7 @@ RUN chmod +x ./scripts/removeWorkspaceDependencies.sh WORKDIR /string-templates COPY packages/string-templates/package.json package.json RUN ../scripts/removeWorkspaceDependencies.sh package.json -RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true +RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000 COPY packages/string-templates . @@ -57,7 +57,7 @@ COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies. RUN chmod +x ./scripts/removeWorkspaceDependencies.sh RUN ./scripts/removeWorkspaceDependencies.sh package.json -RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true \ +RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000 \ # Remove unneeded data from file system to reduce image size && yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python jq \ && rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp diff --git a/packages/worker/Dockerfile.v2 b/packages/worker/Dockerfile.v2 index a8be432827..4706ca155a 100644 --- a/packages/worker/Dockerfile.v2 +++ b/packages/worker/Dockerfile.v2 @@ -19,7 +19,7 @@ RUN chmod +x ./scripts/removeWorkspaceDependencies.sh WORKDIR /string-templates COPY packages/string-templates/package.json package.json RUN ../scripts/removeWorkspaceDependencies.sh package.json -RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true +RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000 COPY packages/string-templates . @@ -30,7 +30,7 @@ RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-te RUN ../scripts/removeWorkspaceDependencies.sh package.json -RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true +RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000 # Remove unneeded data from file system to reduce image size RUN apk del .gyp \ && yarn cache clean From 44f9c64ed72b87c8a62e6b4754f107e1545bf07a Mon Sep 17 00:00:00 2001 From: Gerard Burns Date: Thu, 26 Oct 2023 13:06:25 +0100 Subject: [PATCH 25/65] Prevent the key user being used in rest queries (#12072) * Add warning about unusable user binding * linting * remove unnecessary safe nav operators * change regex to capture property access of user binding --- .../integration/RestQueryViewer.svelte | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/builder/src/components/integration/RestQueryViewer.svelte b/packages/builder/src/components/integration/RestQueryViewer.svelte index 254f65fcaf..e6913b0953 100644 --- a/packages/builder/src/components/integration/RestQueryViewer.svelte +++ b/packages/builder/src/components/integration/RestQueryViewer.svelte @@ -196,8 +196,36 @@ } } + const validateQuery = async () => { + const forbiddenBindings = /{{\s?user(\.(\w|\$)*\s?|\s?)}}/g + const bindingError = new Error( + "'user' is a protected binding and cannot be used" + ) + + if (forbiddenBindings.test(url)) { + throw bindingError + } + + if (forbiddenBindings.test(query.fields.requestBody ?? "")) { + throw bindingError + } + + Object.values(requestBindings).forEach(bindingValue => { + if (forbiddenBindings.test(bindingValue)) { + throw bindingError + } + }) + + Object.values(query.fields.headers).forEach(headerValue => { + if (forbiddenBindings.test(headerValue)) { + throw bindingError + } + }) + } + async function runQuery() { try { + await validateQuery() response = await queries.preview(buildQuery()) if (response.rows.length === 0) { notifications.info("Request did not return any data") From 5266d80afe559a3f964ee8e65a926c0e2dffcf1a Mon Sep 17 00:00:00 2001 From: Dean Date: Thu, 26 Oct 2023 16:09:01 +0100 Subject: [PATCH 26/65] Fix to ensure single integer ids are parsed correctly --- packages/client/src/utils/buttonActions.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js index 18d6b3de3c..9b4640dbb4 100644 --- a/packages/client/src/utils/buttonActions.js +++ b/packages/client/src/utils/buttonActions.js @@ -103,7 +103,6 @@ const fetchRowHandler = async action => { const deleteRowHandler = async action => { const { tableId, rowId: rowConfig, notificationOverride } = action.parameters - if (tableId && rowConfig) { try { let requestConfig @@ -129,9 +128,11 @@ const deleteRowHandler = async action => { requestConfig = [parsedRowConfig] } else if (Array.isArray(parsedRowConfig)) { requestConfig = parsedRowConfig + } else if (Number.isInteger(parsedRowConfig)) { + requestConfig = [String(parsedRowConfig)] } - if (!requestConfig.length) { + if (!requestConfig && !parsedRowConfig) { notificationStore.actions.warning("No valid rows were supplied") return false } From 9616e8e5519cc59ca7dabd286a4d97190eecdb9c Mon Sep 17 00:00:00 2001 From: melohagan <101575380+melohagan@users.noreply.github.com> Date: Thu, 26 Oct 2023 16:28:12 +0100 Subject: [PATCH 27/65] Custom role navigation links (#12056) * Refactor routing unit tests * Filter out higher level roles in preview * unit test * Refactor --------- Co-authored-by: Michael Drury --- packages/server/src/api/controllers/role.ts | 24 +++++++++++++++++- .../server/src/api/routes/tests/role.spec.js | 20 +++++++++++++++ .../src/api/routes/tests/routing.spec.js | 25 ++++++++----------- packages/server/src/constants/screens.ts | 14 ++++++++--- .../server/src/tests/utilities/structures.ts | 19 ++++++++++++-- 5 files changed, 81 insertions(+), 21 deletions(-) diff --git a/packages/server/src/api/controllers/role.ts b/packages/server/src/api/controllers/role.ts index ed23009706..3697bbe925 100644 --- a/packages/server/src/api/controllers/role.ts +++ b/packages/server/src/api/controllers/role.ts @@ -1,4 +1,10 @@ -import { context, db as dbCore, events, roles } from "@budibase/backend-core" +import { + context, + db as dbCore, + events, + roles, + Header, +} from "@budibase/backend-core" import { getUserMetadataParams, InternalTables } from "../../db/utils" import { Database, Role, UserCtx, UserRoles } from "@budibase/types" import { sdk as sharedSdk } from "@budibase/shared-core" @@ -143,4 +149,20 @@ export async function accessible(ctx: UserCtx) { } else { ctx.body = await roles.getUserRoleIdHierarchy(roleId!) } + + // If a custom role is provided in the header, filter out higher level roles + const roleHeader = ctx.header?.[Header.PREVIEW_ROLE] as string + if (roleHeader && !Object.keys(roles.BUILTIN_ROLE_IDS).includes(roleHeader)) { + const inherits = (await roles.getRole(roleHeader))?.inherits + const orderedRoles = ctx.body.reverse() + let filteredRoles = [roleHeader] + for (let role of orderedRoles) { + filteredRoles = [role, ...filteredRoles] + if (role === inherits) { + break + } + } + filteredRoles.pop() + ctx.body = [roleHeader, ...filteredRoles] + } } diff --git a/packages/server/src/api/routes/tests/role.spec.js b/packages/server/src/api/routes/tests/role.spec.js index c8e383d5ed..d133a69d64 100644 --- a/packages/server/src/api/routes/tests/role.spec.js +++ b/packages/server/src/api/routes/tests/role.spec.js @@ -158,5 +158,25 @@ describe("/roles", () => { expect(res.body.length).toBe(1) expect(res.body[0]).toBe("PUBLIC") }) + + it("should not fetch higher level accessible roles when a custom role header is provided", async () => { + await createRole({ + name: `CUSTOM_ROLE`, + inherits: roles.BUILTIN_ROLE_IDS.BASIC, + permissionId: permissions.BuiltinPermissionID.READ_ONLY, + version: "name", + }) + const res = await request + .get("/api/roles/accessible") + .set({ + ...config.defaultHeaders(), + "x-budibase-role": "CUSTOM_ROLE" + }) + .expect(200) + expect(res.body.length).toBe(3) + expect(res.body[0]).toBe("CUSTOM_ROLE") + expect(res.body[1]).toBe("BASIC") + expect(res.body[2]).toBe("PUBLIC") + }) }) }) diff --git a/packages/server/src/api/routes/tests/routing.spec.js b/packages/server/src/api/routes/tests/routing.spec.js index ff6d7aba1d..4076f4879c 100644 --- a/packages/server/src/api/routes/tests/routing.spec.js +++ b/packages/server/src/api/routes/tests/routing.spec.js @@ -1,5 +1,5 @@ const setup = require("./utilities") -const { basicScreen } = setup.structures +const { basicScreen, powerScreen } = setup.structures const { checkBuilderEndpoint, runInProd } = require("./utilities/TestFunctions") const { roles } = require("@budibase/backend-core") const { BUILTIN_ROLE_IDS } = roles @@ -12,19 +12,14 @@ const route = "/test" describe("/routing", () => { let request = setup.getRequest() let config = setup.getConfig() - let screen, screen2 + let basic, power afterAll(setup.afterAll) beforeAll(async () => { await config.init() - screen = basicScreen() - screen.routing.route = route - screen = await config.createScreen(screen) - screen2 = basicScreen() - screen2.routing.roleId = BUILTIN_ROLE_IDS.POWER - screen2.routing.route = route - screen2 = await config.createScreen(screen2) + basic = await config.createScreen(basicScreen(route)) + power = await config.createScreen(powerScreen(route)) await config.publish() }) @@ -61,8 +56,8 @@ describe("/routing", () => { expect(res.body.routes[route]).toEqual({ subpaths: { [route]: { - screenId: screen._id, - roleId: screen.routing.roleId + screenId: basic._id, + roleId: basic.routing.roleId } } }) @@ -80,8 +75,8 @@ describe("/routing", () => { expect(res.body.routes[route]).toEqual({ subpaths: { [route]: { - screenId: screen2._id, - roleId: screen2.routing.roleId + screenId: power._id, + roleId: power.routing.roleId } } }) @@ -101,8 +96,8 @@ describe("/routing", () => { expect(res.body.routes).toBeDefined() expect(res.body.routes[route].subpaths[route]).toBeDefined() const subpath = res.body.routes[route].subpaths[route] - expect(subpath.screens[screen2.routing.roleId]).toEqual(screen2._id) - expect(subpath.screens[screen.routing.roleId]).toEqual(screen._id) + expect(subpath.screens[power.routing.roleId]).toEqual(power._id) + expect(subpath.screens[basic.routing.roleId]).toEqual(basic._id) }) it("make sure it is a builder only endpoint", async () => { diff --git a/packages/server/src/constants/screens.ts b/packages/server/src/constants/screens.ts index 23e36a65b8..6c88b0f957 100644 --- a/packages/server/src/constants/screens.ts +++ b/packages/server/src/constants/screens.ts @@ -1,7 +1,15 @@ import { roles } from "@budibase/backend-core" import { BASE_LAYOUT_PROP_IDS } from "./layouts" -export function createHomeScreen() { +export function createHomeScreen( + config: { + roleId: string + route: string + } = { + roleId: roles.BUILTIN_ROLE_IDS.BASIC, + route: "/", + } +) { return { description: "", url: "", @@ -40,8 +48,8 @@ export function createHomeScreen() { gap: "M", }, routing: { - route: "/", - roleId: roles.BUILTIN_ROLE_IDS.BASIC, + route: config.route, + roleId: config.roleId, }, name: "home-screen", } diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index d3e92ea34d..6d236062a8 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -20,6 +20,7 @@ import { SourceName, Table, } from "@budibase/types" +const { BUILTIN_ROLE_IDS } = roles export function basicTable(): Table { return { @@ -322,8 +323,22 @@ export function basicUser(role: string) { } } -export function basicScreen() { - return createHomeScreen() +export function basicScreen(route: string = "/") { + return createHomeScreen({ + roleId: BUILTIN_ROLE_IDS.BASIC, + route, + }) +} + +export function powerScreen(route: string = "/") { + return createHomeScreen({ + roleId: BUILTIN_ROLE_IDS.POWER, + route, + }) +} + +export function customScreen(config: { roleId: string; route: string }) { + return createHomeScreen(config) } export function basicLayout() { From 5a80487c77f331ddf054825fa0b04d4e32cec179 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 26 Oct 2023 17:54:26 +0100 Subject: [PATCH 28/65] Fix redirect loop when accessing a group as a global builder but not an admin. --- packages/pro | 2 +- .../api/routes/global/tests/groups.spec.ts | 37 ++++++++++++++++++- .../worker/src/tests/TestConfiguration.ts | 17 +++++++-- .../worker/src/tests/structures/groups.ts | 8 ++-- 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/packages/pro b/packages/pro index d24c0dc3a3..1911442b93 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit d24c0dc3a30014cbe61860252aa48104cad36376 +Subproject commit 1911442b93670d71edb4d9e19b8e0677bbad6c47 diff --git a/packages/worker/src/api/routes/global/tests/groups.spec.ts b/packages/worker/src/api/routes/global/tests/groups.spec.ts index afeaae952c..8f0739a812 100644 --- a/packages/worker/src/api/routes/global/tests/groups.spec.ts +++ b/packages/worker/src/api/routes/global/tests/groups.spec.ts @@ -1,7 +1,7 @@ import { events } from "@budibase/backend-core" import { generator } from "@budibase/backend-core/tests" import { structures, TestConfiguration, mocks } from "../../../../tests" -import { UserGroup } from "@budibase/types" +import { User, UserGroup } from "@budibase/types" mocks.licenses.useGroups() @@ -231,4 +231,39 @@ describe("/api/global/groups", () => { }) }) }) + + describe("with global builder role", () => { + let builder: User + let group: UserGroup + + beforeAll(async () => { + builder = await config.createUser({ + builder: { global: true }, + admin: { global: false }, + }) + await config.createSession(builder) + + let resp = await config.api.groups.saveGroup( + structures.groups.UserGroup() + ) + group = resp.body as UserGroup + }) + + it("find should return 200", async () => { + await config.withUser(builder, async () => { + await config.api.groups.searchUsers(group._id!, { + emailSearch: `user1`, + }) + }) + }) + + it("update should return 200", async () => { + await config.withUser(builder, async () => { + await config.api.groups.updateGroupUsers(group._id!, { + add: [builder._id!], + remove: [], + }) + }) + }) + }) }) diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index 7e9792c9e3..d4fcbeebd6 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -190,6 +190,16 @@ class TestConfiguration { } } + async withUser(user: User, f: () => Promise) { + const oldUser = this.user + this.user = user + try { + await f() + } finally { + this.user = oldUser + } + } + authHeaders(user: User) { const authToken: AuthToken = { userId: user._id!, @@ -257,9 +267,10 @@ class TestConfiguration { }) } - async createUser(user?: User) { - if (!user) { - user = structures.users.user() + async createUser(opts?: Partial) { + let user = structures.users.user() + if (user) { + user = { ...user, ...opts } } const response = await this._req(user, null, controllers.users.save) const body = response as SaveUserResponse diff --git a/packages/worker/src/tests/structures/groups.ts b/packages/worker/src/tests/structures/groups.ts index b0d6bb8fc0..d39dd74eb8 100644 --- a/packages/worker/src/tests/structures/groups.ts +++ b/packages/worker/src/tests/structures/groups.ts @@ -1,8 +1,8 @@ import { generator } from "@budibase/backend-core/tests" import { db } from "@budibase/backend-core" -import { UserGroupRoles } from "@budibase/types" +import { UserGroup as UserGroupType, UserGroupRoles } from "@budibase/types" -export const UserGroup = () => { +export function UserGroup(): UserGroupType { const appsCount = generator.integer({ min: 0, max: 3 }) const roles = Array.from({ length: appsCount }).reduce( (p: UserGroupRoles, v) => { @@ -14,13 +14,11 @@ export const UserGroup = () => { {} ) - let group = { - apps: [], + return { color: generator.color(), icon: generator.word(), name: generator.word(), roles: roles, users: [], } - return group } From 8b9a8bf5f2ddaaa14b52c74aa00329958f7fca6a Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 27 Oct 2023 10:46:00 +0100 Subject: [PATCH 29/65] Updating pro reference. --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index d24c0dc3a3..5ed0ee2aca 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit d24c0dc3a30014cbe61860252aa48104cad36376 +Subproject commit 5ed0ee2aca9d754d80cd46bae412b24621afa47e From 09052cb1a6bca50f18dec6578719247c06699dfc Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 27 Oct 2023 10:47:09 +0100 Subject: [PATCH 30/65] Revert "Updating pro reference." This reverts commit 8b9a8bf5f2ddaaa14b52c74aa00329958f7fca6a. --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 5ed0ee2aca..d24c0dc3a3 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 5ed0ee2aca9d754d80cd46bae412b24621afa47e +Subproject commit d24c0dc3a30014cbe61860252aa48104cad36376 From ae69f9dd920a85a48fcc848bf4de1ee28eac2f6b Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 27 Oct 2023 10:47:50 +0100 Subject: [PATCH 31/65] Update pro reference. --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 1911442b93..5ed0ee2aca 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 1911442b93670d71edb4d9e19b8e0677bbad6c47 +Subproject commit 5ed0ee2aca9d754d80cd46bae412b24621afa47e From 5dd61f8994c477a4c804c939557c959e543bef17 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 27 Oct 2023 11:47:01 +0100 Subject: [PATCH 32/65] Remove APIDoc comments. --- packages/server/src/api/routes/row.ts | 190 ------------------------ packages/server/src/api/routes/table.ts | 115 -------------- 2 files changed, 305 deletions(-) diff --git a/packages/server/src/api/routes/row.ts b/packages/server/src/api/routes/row.ts index c29cb65eac..516bfd20c6 100644 --- a/packages/server/src/api/routes/row.ts +++ b/packages/server/src/api/routes/row.ts @@ -11,128 +11,24 @@ const { PermissionType, PermissionLevel } = permissions const router: Router = new Router() router - /** - * @api {get} /api/:sourceId/:rowId/enrich Get an enriched row - * @apiName Get an enriched row - * @apiGroup rows - * @apiPermission table read access - * @apiDescription This API is only useful when dealing with rows that have relationships. - * Normally when a row is a returned from the API relationships will only have the structure - * `{ primaryDisplay: "name", _id: ... }` but this call will return the full related rows - * for each relationship instead. - * - * @apiParam {string} rowId The ID of the row which is to be retrieved and enriched. - * - * @apiSuccess {object} row The response body will be the enriched row. - */ .get( "/api/:sourceId/:rowId/enrich", paramSubResource("sourceId", "rowId"), authorized(PermissionType.TABLE, PermissionLevel.READ), rowController.fetchEnrichedRow ) - /** - * @api {get} /api/:sourceId/rows Get all rows in a table - * @apiName Get all rows in a table - * @apiGroup rows - * @apiPermission table read access - * @apiDescription This is a deprecated endpoint that should not be used anymore, instead use the search endpoint. - * This endpoint gets all of the rows within the specified table - it is not heavily used - * due to its lack of support for pagination. With SQL tables this will retrieve up to a limit and then - * will simply stop. - * - * @apiParam {string} sourceId The ID of the table to retrieve all rows within. - * - * @apiSuccess {object[]} rows The response body will be an array of all rows found. - */ .get( "/api/:sourceId/rows", paramResource("sourceId"), authorized(PermissionType.TABLE, PermissionLevel.READ), rowController.fetch ) - /** - * @api {get} /api/:sourceId/rows/:rowId Retrieve a single row - * @apiName Retrieve a single row - * @apiGroup rows - * @apiPermission table read access - * @apiDescription This endpoint retrieves only the specified row. If you wish to retrieve - * a row by anything other than its _id field, use the search endpoint. - * - * @apiParam {string} sourceId The ID of the table to retrieve a row from. - * @apiParam {string} rowId The ID of the row to retrieve. - * - * @apiSuccess {object} body The response body will be the row that was found. - */ .get( "/api/:sourceId/rows/:rowId", paramSubResource("sourceId", "rowId"), authorized(PermissionType.TABLE, PermissionLevel.READ), rowController.find ) - /** - * @api {post} /api/:sourceId/search Search for rows in a table - * @apiName Search for rows in a table - * @apiGroup rows - * @apiPermission table read access - * @apiDescription This is the primary method of accessing rows in Budibase, the data provider - * and data UI in the builder are built atop this. All filtering, sorting and pagination is - * handled through this, for internal and external (datasource plus, e.g. SQL) tables. - * - * @apiParam {string} sourceId The ID of the table to retrieve rows from. - * - * @apiParam (Body) {boolean} [paginate] If pagination is required then this should be set to true, - * defaults to false. - * @apiParam (Body) {object} [query] This contains a set of filters which should be applied, if none - * specified then the request will be unfiltered. An example with all of the possible query - * options has been supplied below. - * @apiParam (Body) {number} [limit] This sets a limit for the number of rows that will be returned, - * this will be implemented at the database level if supported for performance reasons. This - * is useful when paginating to set exactly how many rows per page. - * @apiParam (Body) {string} [bookmark] If pagination is enabled then a bookmark will be returned - * with each successful search request, this should be supplied back to get the next page. - * @apiParam (Body) {object} [sort] If sort is desired this should contain the name of the column to - * sort on. - * @apiParam (Body) {string} [sortOrder] If sort is enabled then this can be either "descending" or - * "ascending" as required. - * @apiParam (Body) {string} [sortType] If sort is enabled then you must specify the type of search - * being used, either "string" or "number". This is only used for internal tables. - * - * @apiParamExample {json} Example: - * { - * "tableId": "ta_70260ff0b85c467ca74364aefc46f26d", - * "query": { - * "string": {}, - * "fuzzy": {}, - * "range": { - * "columnName": { - * "high": 20, - * "low": 10, - * } - * }, - * "equal": { - * "columnName": "someValue" - * }, - * "notEqual": {}, - * "empty": {}, - * "notEmpty": {}, - * "oneOf": { - * "columnName": ["value"] - * } - * }, - * "limit": 10, - * "sort": "name", - * "sortOrder": "descending", - * "sortType": "string", - * "paginate": true - * } - * - * @apiSuccess {object[]} rows An array of rows that was found based on the supplied parameters. - * @apiSuccess {boolean} hasNextPage If pagination was enabled then this specifies whether or - * not there is another page after this request. - * @apiSuccess {string} bookmark The bookmark to be sent with the next request to get the next - * page. - */ .post( "/api/:sourceId/search", internalSearchValidator(), @@ -148,30 +44,6 @@ router authorized(PermissionType.TABLE, PermissionLevel.READ), rowController.search ) - /** - * @api {post} /api/:sourceId/rows Creates a new row - * @apiName Creates a new row - * @apiGroup rows - * @apiPermission table write access - * @apiDescription This API will create a new row based on the supplied body. If the - * body includes an "_id" field then it will update an existing row if the field - * links to one. Please note that "_id", "_rev" and "tableId" are fields that are - * already used by Budibase tables and cannot be used for columns. - * - * @apiParam {string} sourceId The ID of the table to save a row to. - * - * @apiParam (Body) {string} [_id] If the row exists already then an ID for the row must be provided. - * @apiParam (Body) {string} [_rev] If working with an existing row for an internal table its revision - * must also be provided. - * @apiParam (Body) {string} tableId The ID of the table should also be specified in the row body itself. - * @apiParam (Body) {any} [any] Any field supplied in the body will be assessed to see if it matches - * a column in the specified table. All other fields will be dropped and not stored. - * - * @apiSuccess {string} _id The ID of the row that was just saved, if it was just created this - * is the rows new ID. - * @apiSuccess {string} [_rev] If saving to an internal table a revision will also be returned. - * @apiSuccess {object} body The contents of the row that was saved will be returned as well. - */ .post( "/api/:sourceId/rows", paramResource("sourceId"), @@ -179,14 +51,6 @@ router trimViewRowInfo, rowController.save ) - /** - * @api {patch} /api/:sourceId/rows Updates a row - * @apiName Update a row - * @apiGroup rows - * @apiPermission table write access - * @apiDescription This endpoint is identical to the row creation endpoint but instead it will - * error if an _id isn't provided, it will only function for existing rows. - */ .patch( "/api/:sourceId/rows", paramResource("sourceId"), @@ -194,52 +58,12 @@ router trimViewRowInfo, rowController.patch ) - /** - * @api {post} /api/:sourceId/rows/validate Validate inputs for a row - * @apiName Validate inputs for a row - * @apiGroup rows - * @apiPermission table write access - * @apiDescription When attempting to save a row you may want to check if the row is valid - * given the table schema, this will iterate through all the constraints on the table and - * check if the request body is valid. - * - * @apiParam {string} sourceId The ID of the table the row is to be validated for. - * - * @apiParam (Body) {any} [any] Any fields provided in the request body will be tested - * against the table schema and constraints. - * - * @apiSuccess {boolean} valid If inputs provided are acceptable within the table schema this - * will be true, if it is not then then errors property will be populated. - * @apiSuccess {object} [errors] A key value map of information about fields on the input - * which do not match the table schema. The key name will be the column names that have breached - * the schema. - */ .post( "/api/:sourceId/rows/validate", paramResource("sourceId"), authorized(PermissionType.TABLE, PermissionLevel.WRITE), rowController.validate ) - /** - * @api {delete} /api/:sourceId/rows Delete rows - * @apiName Delete rows - * @apiGroup rows - * @apiPermission table write access - * @apiDescription This endpoint can delete a single row, or delete them in a bulk - * fashion. - * - * @apiParam {string} sourceId The ID of the table the row is to be deleted from. - * - * @apiParam (Body) {object[]} [rows] If bulk deletion is desired then provide the rows in this - * key of the request body that are to be deleted. - * @apiParam (Body) {string} [_id] If deleting a single row then provide its ID in this field. - * @apiParam (Body) {string} [_rev] If deleting a single row from an internal table then provide its - * revision here. - * - * @apiSuccess {object[]|object} body If deleting bulk then the response body will be an array - * of the deleted rows, if deleting a single row then the body will contain a "row" property which - * is the deleted row. - */ .delete( "/api/:sourceId/rows", paramResource("sourceId"), @@ -247,20 +71,6 @@ router trimViewRowInfo, rowController.destroy ) - - /** - * @api {post} /api/:sourceId/rows/exportRows Export Rows - * @apiName Export rows - * @apiGroup rows - * @apiPermission table write access - * @apiDescription This API can export a number of provided rows - * - * @apiParam {string} sourceId The ID of the table the row is to be deleted from. - * - * @apiParam (Body) {object[]} [rows] The row IDs which are to be exported - * - * @apiSuccess {object[]|object} - */ .post( "/api/:sourceId/rows/exportRows", paramResource("sourceId"), diff --git a/packages/server/src/api/routes/table.ts b/packages/server/src/api/routes/table.ts index 7ffa5acb3e..0172d9844d 100644 --- a/packages/server/src/api/routes/table.ts +++ b/packages/server/src/api/routes/table.ts @@ -9,99 +9,13 @@ const { BUILDER, PermissionLevel, PermissionType } = permissions const router: Router = new Router() router - /** - * @api {get} /api/tables Fetch all tables - * @apiName Fetch all tables - * @apiGroup tables - * @apiPermission table read access - * @apiDescription This endpoint retrieves all of the tables which have been created in - * an app. This includes all of the external and internal tables; to tell the difference - * between these look for the "type" property on each table, either being "internal" or "external". - * - * @apiSuccess {object[]} body The response body will be the list of tables that was found - as - * this does not take any parameters the only error scenario is no access. - */ .get("/api/tables", authorized(BUILDER), tableController.fetch) - /** - * @api {get} /api/tables/:id Fetch a single table - * @apiName Fetch a single table - * @apiGroup tables - * @apiPermission table read access - * @apiDescription Retrieves a single table this could be be internal or external based on - * the provided table ID. - * - * @apiParam {string} id The ID of the table which is to be retrieved. - * - * @apiSuccess {object[]} body The response body will be the table that was found. - */ .get( "/api/tables/:tableId", paramResource("tableId"), authorized(PermissionType.TABLE, PermissionLevel.READ, { schema: true }), tableController.find ) - /** - * @api {post} /api/tables Save a table - * @apiName Save a table - * @apiGroup tables - * @apiPermission builder - * @apiDescription Create or update a table with this endpoint, this will function for both internal - * external tables. - * - * @apiParam (Body) {string} [_id] If updating an existing table then the ID of the table must be specified. - * @apiParam (Body) {string} [_rev] If updating an existing internal table then the revision must also be specified. - * @apiParam (Body) {string} type] This should either be "internal" or "external" depending on the table type - - * this will default to internal. - * @apiParam (Body) {string} [sourceId] If creating an external table then this should be set to the datasource ID. If - * building an internal table this does not need to be set, although it will be returned as "bb_internal". - * @apiParam (Body) {string} name The name of the table, this will be used in the UI. To rename the table simply - * supply the table structure to this endpoint with the name changed. - * @apiParam (Body) {object} schema A key value object which has all of the columns in the table as the keys in this - * object. For each column a "type" and "constraints" must be specified, with some types requiring further information. - * More information about the schema structure can be found in the Typescript definitions. - * @apiParam (Body) {string} [primaryDisplay] The name of the column which should be used when displaying rows - * from this table as relationships. - * @apiParam (Body) {object[]} [indexes] Specifies the search indexes - this is deprecated behaviour with the introduction - * of lucene indexes. This functionality is only available for internal tables. - * @apiParam (Body) {object} [_rename] If a column is to be renamed then the "old" column name should be set in this - * structure, and the "updated", new column name should also be supplied. The schema should also be updated, this field - * lets the server know that a field hasn't just been deleted, that the data has moved to a new name, this will fix - * the rows in the table. This functionality is only available for internal tables. - * @apiParam (Body) {object[]} [rows] When creating a table using a compatible data source, an array of objects to be imported into the new table can be provided. - * - * @apiParamExample {json} Example: - * { - * "_id": "ta_05541307fa0f4044abee071ca2a82119", - * "_rev": "10-0fbe4e78f69b255d79f1017e2eeef807", - * "type": "internal", - * "views": {}, - * "name": "tableName", - * "schema": { - * "column": { - * "type": "string", - * "constraints": { - * "type": "string", - * "length": { - * "maximum": null - * }, - * "presence": false - * }, - * "name": "column" - * }, - * }, - * "primaryDisplay": "column", - * "indexes": [], - * "sourceId": "bb_internal", - * "_rename": { - * "old": "columnName", - * "updated": "newColumnName", - * }, - * "rows": [] - * } - * - * @apiSuccess {object} table The response body will contain the table structure after being cleaned up and - * saved to the database. - */ .post( "/api/tables", // allows control over updating a table @@ -125,41 +39,12 @@ router authorized(BUILDER), tableController.validateExistingTableImport ) - /** - * @api {post} /api/tables/:tableId/:revId Delete a table - * @apiName Delete a table - * @apiGroup tables - * @apiPermission builder - * @apiDescription This endpoint will delete a table and all of its associated data, for this reason it is - * quite dangerous - it will work for internal and external tables. - * - * @apiParam {string} tableId The ID of the table which is to be deleted. - * @apiParam {string} [revId] If deleting an internal table then the revision must also be supplied (_rev), for - * external tables this can simply be set to anything, e.g. "external". - * - * @apiSuccess {string} message A message stating that the table was deleted successfully. - */ .delete( "/api/tables/:tableId/:revId", paramResource("tableId"), authorized(BUILDER), tableController.destroy ) - /** - * @api {post} /api/tables/:tableId/:revId Import CSV to existing table - * @apiName Import CSV to existing table - * @apiGroup tables - * @apiPermission builder - * @apiDescription This endpoint will import data to existing tables, internal or external. It is used in combination - * with the CSV validation endpoint. Take the output of the CSV validation endpoint and pass it to this endpoint to - * import the data; please note this will only import fields that already exist on the table/match the type. - * - * @apiParam {string} tableId The ID of the table which the data should be imported to. - * - * @apiParam (Body) {object[]} rows An array of objects representing the rows to be imported, key-value pairs not matching the table schema will be ignored. - * - * @apiSuccess {string} message A message stating that the data was imported successfully. - */ .post( "/api/tables/:tableId/import", paramResource("tableId"), From 2160f4e5e205729e1af2afce6cf586c3f537e6f0 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 27 Oct 2023 16:24:02 +0100 Subject: [PATCH 33/65] Add valid extension list to shared-core. --- packages/shared-core/src/constants.ts | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/shared-core/src/constants.ts b/packages/shared-core/src/constants.ts index 725c246e2f..312e69c896 100644 --- a/packages/shared-core/src/constants.ts +++ b/packages/shared-core/src/constants.ts @@ -96,3 +96,46 @@ export enum BuilderSocketEvent { export const SocketSessionTTL = 60 export const ValidQueryNameRegex = /^[^()]*$/ export const ValidColumnNameRegex = /^[_a-zA-Z0-9\s]*$/g + +export const ValidFileExtensions = [ + "avif", + "css", + "csv", + "docx", + "drawio", + "editorconfig", + "edl", + "enc", + "export", + "geojson", + "gif", + "htm", + "html", + "ics", + "iqy", + "jfif", + "jpeg", + "jpg", + "json", + "log", + "md", + "mid", + "odt", + "pdf", + "png", + "ris", + "rtf", + "svg", + "tex", + "toml", + "twig", + "txt", + "url", + "wav", + "webp", + "xls", + "xlsx", + "xml", + "yaml", + "yml", +] From 6bb6f106d53ee3e1f866e4317e73f62cba1d207b Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 27 Oct 2023 16:46:30 +0100 Subject: [PATCH 34/65] Apply valid file types to AttachmentCell. --- packages/bbui/src/Form/Dropzone.svelte | 2 ++ .../src/components/grid/cells/AttachmentCell.svelte | 3 +++ packages/shared-core/src/constants.ts | 1 - 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/bbui/src/Form/Dropzone.svelte b/packages/bbui/src/Form/Dropzone.svelte index 2a6fa1c57b..94742ea08d 100644 --- a/packages/bbui/src/Form/Dropzone.svelte +++ b/packages/bbui/src/Form/Dropzone.svelte @@ -17,6 +17,7 @@ export let fileTags = [] export let maximum = undefined export let compact = false + export let extensions = undefined const dispatch = createEventDispatcher() const onChange = e => { @@ -39,6 +40,7 @@ {fileTags} {maximum} {compact} + {extensions} on:change={onChange} /> diff --git a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte index a27c31bbe5..4e2a6025e5 100644 --- a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte @@ -2,6 +2,7 @@ import { onMount } from "svelte" import { getContext } from "svelte" import { Dropzone } from "@budibase/bbui" + import { ValidFileExtensions } from "@budibase/shared-core" export let value export let focused = false @@ -13,6 +14,7 @@ const { API, notifications } = getContext("grid") const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"] + const validExtensions = ValidFileExtensions.map(ext => `.${ext}`).join(", ") let isOpen = false @@ -96,6 +98,7 @@ {value} compact on:change={e => onChange(e.detail)} + extensions={validExtensions} {processFiles} {deleteAttachments} {handleFileTooLarge} diff --git a/packages/shared-core/src/constants.ts b/packages/shared-core/src/constants.ts index 312e69c896..e7c6feb20a 100644 --- a/packages/shared-core/src/constants.ts +++ b/packages/shared-core/src/constants.ts @@ -96,7 +96,6 @@ export enum BuilderSocketEvent { export const SocketSessionTTL = 60 export const ValidQueryNameRegex = /^[^()]*$/ export const ValidColumnNameRegex = /^[_a-zA-Z0-9\s]*$/g - export const ValidFileExtensions = [ "avif", "css", From 5539ff9c9ce292d844c7d88b4a8384e45310b259 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 27 Oct 2023 16:53:32 +0100 Subject: [PATCH 35/65] Apply valid file types to RowFieldControl and AttackmentField. --- .../src/components/backend/DataTable/RowFieldControl.svelte | 5 ++++- .../client/src/components/app/forms/AttachmentField.svelte | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index 61b706e28e..9d52cb815e 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -13,6 +13,7 @@ import { capitalise } from "helpers" import LinkedRowSelector from "components/common/LinkedRowSelector.svelte" import Editor from "../../integration/QueryEditor.svelte" + import { ValidFileExtensions } from "@budibase/shared-core" export let defaultValue export let meta @@ -20,6 +21,8 @@ export let readonly export let error + let validExtensions = ValidFileExtensions.map(ext => `.${ext}`).join(", ") + const resolveTimeStamp = timestamp => { if (!timestamp) { return null @@ -59,7 +62,7 @@ bind:value /> {:else if type === "attachment"} - + {:else if type === "boolean"} {:else if type === "array" && meta.constraints.inclusion.length !== 0} diff --git a/packages/client/src/components/app/forms/AttachmentField.svelte b/packages/client/src/components/app/forms/AttachmentField.svelte index e24115ebc0..2effe607ae 100644 --- a/packages/client/src/components/app/forms/AttachmentField.svelte +++ b/packages/client/src/components/app/forms/AttachmentField.svelte @@ -2,13 +2,14 @@ import Field from "./Field.svelte" import { CoreDropzone } from "@budibase/bbui" import { getContext } from "svelte" + import { ValidFileExtensions } from "@budibase/shared-core" export let field export let label export let disabled = false export let compact = false export let validation - export let extensions + export let extensions = ValidFileExtensions.map(ext => `.${ext}`).join(", ") export let onChange export let maximum = undefined From 6ecf831f028c650d6df68fa54c9fa518e68f70ac Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 27 Oct 2023 17:10:03 +0100 Subject: [PATCH 36/65] Updating pro submodule reference. --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 5ed0ee2aca..4506399e0d 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 5ed0ee2aca9d754d80cd46bae412b24621afa47e +Subproject commit 4506399e0d0297554cacbef1f436884aabdb9741 From f1aa32e4461b604c551915953940a990f0670404 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 27 Oct 2023 17:19:39 +0100 Subject: [PATCH 37/65] Truncate file size on the grid, validate extension in the attachment API. --- packages/bbui/src/Form/Core/Dropzone.svelte | 10 ++++++---- .../backend/DataTable/RowFieldControl.svelte | 5 ++++- .../server/src/api/controllers/static/index.ts | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte index e9ee75bd8b..0b6a9bb94f 100644 --- a/packages/bbui/src/Form/Core/Dropzone.svelte +++ b/packages/bbui/src/Form/Core/Dropzone.svelte @@ -159,8 +159,10 @@ {#if selectedImage.size}
{#if selectedImage.size <= BYTES_IN_MB} - {`${selectedImage.size / BYTES_IN_KB} KB`} - {:else}{`${selectedImage.size / BYTES_IN_MB} MB`}{/if} + {`${(selectedImage.size / BYTES_IN_KB).toFixed(1)} KB`} + {:else}{`${(selectedImage.size / BYTES_IN_MB).toFixed( + 1 + )} MB`}{/if}
{/if} {#if !disabled} @@ -203,8 +205,8 @@ {#if file.size}
{#if file.size <= BYTES_IN_MB} - {`${file.size / BYTES_IN_KB} KB`} - {:else}{`${file.size / BYTES_IN_MB} MB`}{/if} + {`${(file.size / BYTES_IN_KB).toFixed(1)} KB`} + {:else}{`${(file.size / BYTES_IN_MB).toFixed(1)} MB`}{/if}
{/if} {#if !disabled} diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index 9d52cb815e..ea1161fe9b 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -14,6 +14,7 @@ import LinkedRowSelector from "components/common/LinkedRowSelector.svelte" import Editor from "../../integration/QueryEditor.svelte" import { ValidFileExtensions } from "@budibase/shared-core" + import { admin } from "stores/portal" export let defaultValue export let meta @@ -21,7 +22,9 @@ export let readonly export let error - let validExtensions = ValidFileExtensions.map(ext => `.${ext}`).join(", ") + let validExtensions = $admin.cloud + ? ValidFileExtensions.map(ext => `.${ext}`).join(", ") + : "*" const resolveTimeStamp = timestamp => { if (!timestamp) { diff --git a/packages/server/src/api/controllers/static/index.ts b/packages/server/src/api/controllers/static/index.ts index 984cb16c06..e8d403ad12 100644 --- a/packages/server/src/api/controllers/static/index.ts +++ b/packages/server/src/api/controllers/static/index.ts @@ -1,3 +1,5 @@ +import { ValidFileExtensions } from "@budibase/shared-core" + require("svelte/register") import { join } from "../../../utilities/centralPath" @@ -17,6 +19,7 @@ import fs from "fs" import sdk from "../../../sdk" import * as pro from "@budibase/pro" import { App, Ctx } from "@budibase/types" +import environment from "../../../environment" const send = require("koa-send") @@ -78,6 +81,17 @@ export const uploadFile = async function (ctx: Ctx) { const uploads = files.map(async (file: any) => { const fileExtension = [...file.name.split(".")].pop() + if ( + !environment.SELF_HOSTED && + !ValidFileExtensions.includes(fileExtension) + ) { + ctx.throw( + 400, + `Invalid file extension. Valid extensions are: ${ValidFileExtensions.join( + ", " + )}` + ) + } // filenames converted to UUIDs so they are unique const processedFileName = `${uuid.v4()}.${fileExtension}` From 84ba840dbcfea6e059c3872999f6ac716783eee5 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 27 Oct 2023 17:29:05 +0100 Subject: [PATCH 38/65] Apply valid file type change to AttachmentField in cloud only. --- .../client/src/components/app/forms/AttachmentField.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/app/forms/AttachmentField.svelte b/packages/client/src/components/app/forms/AttachmentField.svelte index 2effe607ae..e57510c770 100644 --- a/packages/client/src/components/app/forms/AttachmentField.svelte +++ b/packages/client/src/components/app/forms/AttachmentField.svelte @@ -3,13 +3,16 @@ import { CoreDropzone } from "@budibase/bbui" import { getContext } from "svelte" import { ValidFileExtensions } from "@budibase/shared-core" + import { environmentStore } from "../../../stores/index.js" export let field export let label export let disabled = false export let compact = false export let validation - export let extensions = ValidFileExtensions.map(ext => `.${ext}`).join(", ") + export let extensions = $environmentStore.cloud + ? ValidFileExtensions.map(ext => `.${ext}`).join(", ") + : "*" export let onChange export let maximum = undefined From d4929ea3b61e5774aabf4f7fe758a66b5e872f21 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 27 Oct 2023 18:03:06 +0100 Subject: [PATCH 39/65] Fixing an issue where unpublished apps with custom roles, when used in groups would cause users to be unable to login. --- packages/backend-core/src/security/roles.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index b05cf79c8c..02421fd1d0 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -122,7 +122,9 @@ export async function roleToNumber(id?: string) { if (isBuiltin(id)) { return builtinRoleToNumber(id) } - const hierarchy = (await getUserRoleHierarchy(id)) as RoleDoc[] + const hierarchy = (await getUserRoleHierarchy(id, { + defaultPublic: true, + })) as RoleDoc[] for (let role of hierarchy) { if (isBuiltin(role?.inherits)) { return builtinRoleToNumber(role.inherits) + 1 @@ -177,7 +179,7 @@ export async function getRole( role = Object.assign(role, dbRole) // finalise the ID role._id = getExternalRoleID(role._id, role.version) - } catch (err) { + } catch (err: any) { if (!isBuiltin(roleId) && opts?.defaultPublic) { return cloneDeep(BUILTIN_ROLES.PUBLIC) } @@ -192,12 +194,15 @@ export async function getRole( /** * Simple function to get all the roles based on the top level user role ID. */ -async function getAllUserRoles(userRoleId?: string): Promise { +async function getAllUserRoles( + userRoleId?: string, + opts?: { defaultPublic?: boolean } +): Promise { // admins have access to all roles if (userRoleId === BUILTIN_IDS.ADMIN) { return getAllRoles() } - let currentRole = await getRole(userRoleId) + let currentRole = await getRole(userRoleId, opts) let roles = currentRole ? [currentRole] : [] let roleIds = [userRoleId] // get all the inherited roles @@ -226,12 +231,16 @@ export async function getUserRoleIdHierarchy( * Returns an ordered array of the user's inherited role IDs, this can be used * to determine if a user can access something that requires a specific role. * @param userRoleId The user's role ID, this can be found in their access token. + * @param opts optional - if want to default to public use this. * @returns returns an ordered array of the roles, with the first being their * highest level of access and the last being the lowest level. */ -export async function getUserRoleHierarchy(userRoleId?: string) { +export async function getUserRoleHierarchy( + userRoleId?: string, + opts?: { defaultPublic?: boolean } +) { // special case, if they don't have a role then they are a public user - return getAllUserRoles(userRoleId) + return getAllUserRoles(userRoleId, opts) } // this function checks that the provided permissions are in an array format From 330059991e0d6f6f0cf0af5c3ad3199c044b5509 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 27 Oct 2023 18:04:28 +0100 Subject: [PATCH 40/65] Removing any. --- packages/backend-core/src/security/roles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index 02421fd1d0..0d33031de5 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -179,7 +179,7 @@ export async function getRole( role = Object.assign(role, dbRole) // finalise the ID role._id = getExternalRoleID(role._id, role.version) - } catch (err: any) { + } catch (err) { if (!isBuiltin(roleId) && opts?.defaultPublic) { return cloneDeep(BUILTIN_ROLES.PUBLIC) } From 887383bdb6a4b54d7ab747571415f112f83f2e14 Mon Sep 17 00:00:00 2001 From: Michael Drury Date: Fri, 27 Oct 2023 18:12:21 +0100 Subject: [PATCH 41/65] Update README.md Removing dead link. --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 9deb16cd4f..7827d4e48a 100644 --- a/README.md +++ b/README.md @@ -126,13 +126,6 @@ You can learn more about the Budibase API at the following places: - [Build an app with Budibase and Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/) -

- Budibase data -

-

- -


- ## 🏁 Get started Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean. From 1221808c67c65989b24a7540ab0b31f44989e9ff Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Mon, 30 Oct 2023 09:00:20 +0100 Subject: [PATCH 42/65] Updata pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 39bff12817..3820c0c93a 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 39bff1281715c647f5d0c1db9bbf1d53c9fd4fc6 +Subproject commit 3820c0c93a3e448e10a60a9feb5396844b537ca8 From 725e3aa4ef8431990fa9a54d9857f12f95da9a5d Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 10:11:06 +0100 Subject: [PATCH 43/65] Use image v2 on build:docker --- packages/server/package.json | 2 +- packages/worker/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 4a858f3be9..b89fe86a84 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -18,7 +18,7 @@ "test": "bash scripts/test.sh", "test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit", "test:watch": "jest --watch", - "build:docker": "yarn build && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION --build-arg BUDIBASE_VERSION=$BUDIBASE_RELEASE_VERSION", + "build:docker": "yarn nx build && docker build ../.. -t app-service --label version=$BUDIBASE_RELEASE_VERSION --build-arg BUDIBASE_VERSION=$BUDIBASE_RELEASE_VERSION -f Dockerfile.v2", "run:docker": "node dist/index.js", "run:docker:cluster": "pm2-runtime start pm2.config.js", "dev:stack:up": "node scripts/dev/manage.js up", diff --git a/packages/worker/package.json b/packages/worker/package.json index 205bf3309a..dd847e6df4 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -20,7 +20,7 @@ "run:docker": "node dist/index.js", "debug": "yarn build && node --expose-gc --inspect=9223 dist/index.js", "run:docker:cluster": "pm2-runtime start pm2.config.js", - "build:docker": "yarn build && docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION --build-arg BUDIBASE_VERSION=$BUDIBASE_RELEASE_VERSION", + "build:docker": "yarn nx build && docker build ../.. -t worker-service --label version=$BUDIBASE_RELEASE_VERSION --build-arg BUDIBASE_VERSION=$BUDIBASE_RELEASE_VERSION -f Dockerfile.v2", "dev:stack:init": "node ./scripts/dev/manage.js init", "dev:builder": "npm run dev:stack:init && nodemon", "dev:built": "yarn run dev:stack:init && yarn run run:docker", From b542040ad4fa3d379842348633fbe15546c816b0 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 10:27:59 +0100 Subject: [PATCH 44/65] Use v2 for single image --- .github/workflows/release-singleimage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-singleimage.yml b/.github/workflows/release-singleimage.yml index f7f87f6e4c..4d35916f4d 100644 --- a/.github/workflows/release-singleimage.yml +++ b/.github/workflows/release-singleimage.yml @@ -67,7 +67,7 @@ jobs: push: true platforms: linux/amd64,linux/arm64 tags: budibase/budibase,budibase/budibase:${{ env.RELEASE_VERSION }} - file: ./hosting/single/Dockerfile + file: ./hosting/single/Dockerfile.v2 - name: Tag and release Budibase Azure App Service docker image uses: docker/build-push-action@v2 with: @@ -76,4 +76,4 @@ jobs: platforms: linux/amd64 build-args: TARGETBUILD=aas tags: budibase/budibase-aas,budibase/budibase-aas:${{ env.RELEASE_VERSION }} - file: ./hosting/single/Dockerfile + file: ./hosting/single/Dockerfile.v2 From b0ef79bbd9f9a7ab3a8bbe2f5513426ff2b61362 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 11:36:32 +0100 Subject: [PATCH 45/65] Build for both amd and arm platforms --- packages/server/package.json | 2 +- packages/worker/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index b89fe86a84..c37959d33d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -18,7 +18,7 @@ "test": "bash scripts/test.sh", "test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit", "test:watch": "jest --watch", - "build:docker": "yarn nx build && docker build ../.. -t app-service --label version=$BUDIBASE_RELEASE_VERSION --build-arg BUDIBASE_VERSION=$BUDIBASE_RELEASE_VERSION -f Dockerfile.v2", + "build:docker": "yarn nx build && docker buildx build ../.. -t app-service --label version=$BUDIBASE_RELEASE_VERSION --build-arg BUDIBASE_VERSION=$BUDIBASE_RELEASE_VERSION -f Dockerfile.v2 --platform linux/amd64,linux/arm64", "run:docker": "node dist/index.js", "run:docker:cluster": "pm2-runtime start pm2.config.js", "dev:stack:up": "node scripts/dev/manage.js up", diff --git a/packages/worker/package.json b/packages/worker/package.json index dd847e6df4..a391db533b 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -20,7 +20,7 @@ "run:docker": "node dist/index.js", "debug": "yarn build && node --expose-gc --inspect=9223 dist/index.js", "run:docker:cluster": "pm2-runtime start pm2.config.js", - "build:docker": "yarn nx build && docker build ../.. -t worker-service --label version=$BUDIBASE_RELEASE_VERSION --build-arg BUDIBASE_VERSION=$BUDIBASE_RELEASE_VERSION -f Dockerfile.v2", + "build:docker": "yarn nx build && docker buildx build ../.. -t worker-service --label version=$BUDIBASE_RELEASE_VERSION --build-arg BUDIBASE_VERSION=$BUDIBASE_RELEASE_VERSION -f Dockerfile.v2 --platform linux/amd64,linux/arm64", "dev:stack:init": "node ./scripts/dev/manage.js init", "dev:builder": "npm run dev:stack:init && nodemon", "dev:built": "yarn run dev:stack:init && yarn run run:docker", From 6267d9b601e7b350388b5adb139e6ef6c9cb2b16 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 11:39:25 +0100 Subject: [PATCH 46/65] Test building images --- .github/workflows/budibase_ci.yml | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 77867c8617..3ec87f7244 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -42,6 +42,39 @@ jobs: - run: yarn --frozen-lockfile - run: yarn lint + test-release-images: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + fetch-depth: 0 + + - uses: actions/setup-node@v1 + with: + node-version: 18.x + + - run: yarn install --frozen-lockfile + - name: Update versions + run: ./scripts/updateVersions.sh + - run: yarn lint + - run: yarn build + - run: yarn build:sdk + + - name: "Get Current tag" + id: currenttag + run: | + version=$(./scripts/getCurrentVersion.sh) + echo "Using tag $version" + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Build/release Docker images + run: | + yarn lerna run --stream build:docker + env: + BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.version }} + build: runs-on: ubuntu-latest steps: From ac67a17b9393979b65f8bf4335a4f83232872e97 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 11:45:05 +0100 Subject: [PATCH 47/65] Use buildx --- .github/workflows/budibase_ci.yml | 6 +++++- .github/workflows/release-master.yml | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 3ec87f7244..13245a7fa1 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -51,9 +51,10 @@ jobs: token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} fetch-depth: 0 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: node-version: 18.x + cache: "yarn" - run: yarn install --frozen-lockfile - name: Update versions @@ -69,6 +70,9 @@ jobs: echo "Using tag $version" echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Setup Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 - name: Build/release Docker images run: | yarn lerna run --stream build:docker diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index 9ab8530341..4c5a3f4a1e 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -63,6 +63,9 @@ jobs: echo "Using tag $version" echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Setup Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 - name: Build/release Docker images run: | docker login -u $DOCKER_USER -p $DOCKER_PASSWORD From 24eed537edc45d8febb9c119f5a4f54f7f296558 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 12:00:55 +0100 Subject: [PATCH 48/65] Fix timeouts --- packages/server/Dockerfile | 2 +- packages/worker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/Dockerfile b/packages/server/Dockerfile index e1b3b208c7..ea4c5b217a 100644 --- a/packages/server/Dockerfile +++ b/packages/server/Dockerfile @@ -38,7 +38,7 @@ RUN apt update && apt upgrade -y \ COPY package.json . COPY dist/yarn.lock . -RUN yarn install --production=true \ +RUN yarn install --production=true --network-timeout 1000000 \ # Remove unneeded data from file system to reduce image size && yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python \ && rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp diff --git a/packages/worker/Dockerfile b/packages/worker/Dockerfile index 4230ee86f8..50f1bb78b9 100644 --- a/packages/worker/Dockerfile +++ b/packages/worker/Dockerfile @@ -14,7 +14,7 @@ RUN yarn global add pm2 COPY package.json . COPY dist/yarn.lock . -RUN yarn install --production=true +RUN yarn install --production=true --network-timeout 1000000 # Remove unneeded data from file system to reduce image size RUN apk del .gyp \ && yarn cache clean From d9c34f3f465476291b2cad1b539e0e5e3ceb649e Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 12:40:05 +0100 Subject: [PATCH 49/65] Remove build docker in ci pipelines --- .github/workflows/budibase_ci.yml | 37 ------------------------------- 1 file changed, 37 deletions(-) diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 13245a7fa1..77867c8617 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -42,43 +42,6 @@ jobs: - run: yarn --frozen-lockfile - run: yarn lint - test-release-images: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - submodules: true - token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - fetch-depth: 0 - - - uses: actions/setup-node@v3 - with: - node-version: 18.x - cache: "yarn" - - - run: yarn install --frozen-lockfile - - name: Update versions - run: ./scripts/updateVersions.sh - - run: yarn lint - - run: yarn build - - run: yarn build:sdk - - - name: "Get Current tag" - id: currenttag - run: | - version=$(./scripts/getCurrentVersion.sh) - echo "Using tag $version" - echo "version=$version" >> "$GITHUB_OUTPUT" - - - name: Setup Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 - - name: Build/release Docker images - run: | - yarn lerna run --stream build:docker - env: - BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.version }} - build: runs-on: ubuntu-latest steps: From 9229ab6896c81c0bc7d545ba3bb3ee5c70840a87 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Mon, 30 Oct 2023 12:12:56 +0000 Subject: [PATCH 50/65] Bump version to 2.12.0 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 384473120b..3179bc3b2e 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.11.45", + "version": "2.12.0", "npmClient": "yarn", "packages": [ "packages/*" From a141598a1cb269ab5deb2b939a861a753d8b236d Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 14:26:28 +0100 Subject: [PATCH 51/65] Remove unused scripts --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index d3f4903e6c..03257d2d06 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,6 @@ "build:specs": "lerna run --stream specs", "build:docker": "lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -", "build:docker:proxy": "docker build hosting/proxy -t proxy-service", - "build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -", - "build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -", "build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild", "build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild", "build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -", From 6631a7a11d8704a075bec9fc7516ccfb08aac141 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 14:34:55 +0100 Subject: [PATCH 52/65] Push docker images via docker/build-push-action --- .github/workflows/release-master.yml | 39 +++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index 4c5a3f4a1e..a8af6a4da1 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -66,14 +66,47 @@ jobs: - name: Setup Docker Buildx id: buildx uses: docker/setup-buildx-action@v1 - - name: Build/release Docker images + + - name: Docker login run: | docker login -u $DOCKER_USER -p $DOCKER_PASSWORD - yarn build:docker env: DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} - BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.version }} + + - name: Build docker (budibase/worker) + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + build-args: | + BUDIBASE_VERSION=${{ env.BUDIBASE_VERSION }} + tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + file: ./packages/worker/Dockerfile.v2 + cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest + cache-to: type=inline + env: + IMAGE_NAME: budibase/worker + IMAGE_TAG: ${{ inputs.image_tag }} + BUDIBASE_VERSION: ${{ steps.currenttag.outputs.version }} + + - name: Build docker (budibase/apps) + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + build-args: | + BUDIBASE_VERSION=${{ env.BUDIBASE_VERSION }} + tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + file: ./packages/server/Dockerfile.v2 + cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest + cache-to: type=inline + env: + IMAGE_NAME: budibase/apps + IMAGE_TAG: ${{ inputs.image_tag }} + BUDIBASE_VERSION: ${{ steps.currenttag.outputs.version }} release-helm-chart: needs: [release-images] From 0cc978f86050ffb40082013fb788fd07e81fb9b5 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 14:37:33 +0100 Subject: [PATCH 53/65] Push proxy dockerfile --- .github/workflows/release-master.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index a8af6a4da1..4a89d8462f 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -74,7 +74,7 @@ jobs: DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} - - name: Build docker (budibase/worker) + - name: Build worker docker uses: docker/build-push-action@v5 with: context: . @@ -91,7 +91,7 @@ jobs: IMAGE_TAG: ${{ inputs.image_tag }} BUDIBASE_VERSION: ${{ steps.currenttag.outputs.version }} - - name: Build docker (budibase/apps) + - name: Build server docker uses: docker/build-push-action@v5 with: context: . @@ -108,6 +108,19 @@ jobs: IMAGE_TAG: ${{ inputs.image_tag }} BUDIBASE_VERSION: ${{ steps.currenttag.outputs.version }} + - name: Build proxy docker + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + file: ./hosting/proxy/Dockerfile + cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest + cache-to: type=inline + env: + IMAGE_NAME: proxy-service + IMAGE_TAG: ${{ inputs.image_tag }} + release-helm-chart: needs: [release-images] runs-on: ubuntu-latest From a80ea2f2c05747fe4607e1bcafac5506b6d5b4f7 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 14:38:26 +0100 Subject: [PATCH 54/65] Clean scripts --- package.json | 2 -- packages/server/package.json | 1 - packages/worker/package.json | 1 - 3 files changed, 4 deletions(-) diff --git a/package.json b/package.json index 03257d2d06..417fb31e0e 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,6 @@ "lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"", "lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint", "build:specs": "lerna run --stream specs", - "build:docker": "lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -", - "build:docker:proxy": "docker build hosting/proxy -t proxy-service", "build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild", "build:docker:airgap:single": "SINGLE_IMAGE=1 node hosting/scripts/airgapped/airgappedDockerBuild", "build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -", diff --git a/packages/server/package.json b/packages/server/package.json index c37959d33d..c845f7889d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -18,7 +18,6 @@ "test": "bash scripts/test.sh", "test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit", "test:watch": "jest --watch", - "build:docker": "yarn nx build && docker buildx build ../.. -t app-service --label version=$BUDIBASE_RELEASE_VERSION --build-arg BUDIBASE_VERSION=$BUDIBASE_RELEASE_VERSION -f Dockerfile.v2 --platform linux/amd64,linux/arm64", "run:docker": "node dist/index.js", "run:docker:cluster": "pm2-runtime start pm2.config.js", "dev:stack:up": "node scripts/dev/manage.js up", diff --git a/packages/worker/package.json b/packages/worker/package.json index a391db533b..ec86575395 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -20,7 +20,6 @@ "run:docker": "node dist/index.js", "debug": "yarn build && node --expose-gc --inspect=9223 dist/index.js", "run:docker:cluster": "pm2-runtime start pm2.config.js", - "build:docker": "yarn nx build && docker buildx build ../.. -t worker-service --label version=$BUDIBASE_RELEASE_VERSION --build-arg BUDIBASE_VERSION=$BUDIBASE_RELEASE_VERSION -f Dockerfile.v2 --platform linux/amd64,linux/arm64", "dev:stack:init": "node ./scripts/dev/manage.js init", "dev:builder": "npm run dev:stack:init && nodemon", "dev:built": "yarn run dev:stack:init && yarn run run:docker", From 0dd655e75dec40415c0e660af74dc2ee72c0802b Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 14:39:03 +0100 Subject: [PATCH 55/65] Remove sh --- .github/workflows/release-master.yml | 2 +- hosting/scripts/linux/release-to-docker-hub.sh | 18 ------------------ 2 files changed, 1 insertion(+), 19 deletions(-) delete mode 100755 hosting/scripts/linux/release-to-docker-hub.sh diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index 4a89d8462f..24d71a35a6 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -118,7 +118,7 @@ jobs: cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest cache-to: type=inline env: - IMAGE_NAME: proxy-service + IMAGE_NAME: budibase/proxy-service IMAGE_TAG: ${{ inputs.image_tag }} release-helm-chart: diff --git a/hosting/scripts/linux/release-to-docker-hub.sh b/hosting/scripts/linux/release-to-docker-hub.sh deleted file mode 100755 index 599a10f914..0000000000 --- a/hosting/scripts/linux/release-to-docker-hub.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -tag=$1 - -if [[ ! "$tag" ]]; then - echo "No tag present. You must pass a tag to this script" - exit 1 -fi - -echo "Tagging images with tag: $tag" - -docker tag proxy-service budibase/proxy:$tag -docker tag app-service budibase/apps:$tag -docker tag worker-service budibase/worker:$tag - -docker push --all-tags budibase/apps -docker push --all-tags budibase/worker -docker push --all-tags budibase/proxy From 469114823600edb7f156fae9eef9514efed9b981 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 14:44:48 +0100 Subject: [PATCH 56/65] Cache yarn --- .github/workflows/release-master.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index 24d71a35a6..c2bee8da40 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -36,6 +36,7 @@ jobs: - uses: actions/setup-node@v1 with: node-version: 18.x + cache: yarn - run: yarn install --frozen-lockfile - name: Update versions From b74f11e0e2d23a67c6c6f30d404b2f719d4cbd32 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 14:53:14 +0100 Subject: [PATCH 57/65] Fix proxy image name --- .github/workflows/release-master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index c2bee8da40..5d67101f4e 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -119,7 +119,7 @@ jobs: cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest cache-to: type=inline env: - IMAGE_NAME: budibase/proxy-service + IMAGE_NAME: budibase/proxy IMAGE_TAG: ${{ inputs.image_tag }} release-helm-chart: From ac3c9a374cbefa12e51e7dc2354128cc344f5240 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 15:02:36 +0100 Subject: [PATCH 58/65] Fix image tag --- .github/workflows/release-master.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index 5d67101f4e..e0adddc6a8 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -89,7 +89,7 @@ jobs: cache-to: type=inline env: IMAGE_NAME: budibase/worker - IMAGE_TAG: ${{ inputs.image_tag }} + IMAGE_TAG: ${{ steps.currenttag.outputs.version }} BUDIBASE_VERSION: ${{ steps.currenttag.outputs.version }} - name: Build server docker @@ -106,7 +106,7 @@ jobs: cache-to: type=inline env: IMAGE_NAME: budibase/apps - IMAGE_TAG: ${{ inputs.image_tag }} + IMAGE_TAG: ${{ steps.currenttag.outputs.version }} BUDIBASE_VERSION: ${{ steps.currenttag.outputs.version }} - name: Build proxy docker @@ -120,7 +120,7 @@ jobs: cache-to: type=inline env: IMAGE_NAME: budibase/proxy - IMAGE_TAG: ${{ inputs.image_tag }} + IMAGE_TAG: ${{ steps.currenttag.outputs.version }} release-helm-chart: needs: [release-images] From 436d6a1585a9d904b0339e7331d24a43e3ba135f Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 30 Oct 2023 14:39:12 +0000 Subject: [PATCH 59/65] Revert frontend changes to filter out file extensions in the upload box. --- packages/bbui/src/Form/Dropzone.svelte | 2 -- .../components/backend/DataTable/RowFieldControl.svelte | 8 +------- .../src/components/app/forms/AttachmentField.svelte | 6 +----- .../src/components/grid/cells/AttachmentCell.svelte | 3 --- 4 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/bbui/src/Form/Dropzone.svelte b/packages/bbui/src/Form/Dropzone.svelte index 94742ea08d..2a6fa1c57b 100644 --- a/packages/bbui/src/Form/Dropzone.svelte +++ b/packages/bbui/src/Form/Dropzone.svelte @@ -17,7 +17,6 @@ export let fileTags = [] export let maximum = undefined export let compact = false - export let extensions = undefined const dispatch = createEventDispatcher() const onChange = e => { @@ -40,7 +39,6 @@ {fileTags} {maximum} {compact} - {extensions} on:change={onChange} /> diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index ea1161fe9b..61b706e28e 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -13,8 +13,6 @@ import { capitalise } from "helpers" import LinkedRowSelector from "components/common/LinkedRowSelector.svelte" import Editor from "../../integration/QueryEditor.svelte" - import { ValidFileExtensions } from "@budibase/shared-core" - import { admin } from "stores/portal" export let defaultValue export let meta @@ -22,10 +20,6 @@ export let readonly export let error - let validExtensions = $admin.cloud - ? ValidFileExtensions.map(ext => `.${ext}`).join(", ") - : "*" - const resolveTimeStamp = timestamp => { if (!timestamp) { return null @@ -65,7 +59,7 @@ bind:value /> {:else if type === "attachment"} - + {:else if type === "boolean"} {:else if type === "array" && meta.constraints.inclusion.length !== 0} diff --git a/packages/client/src/components/app/forms/AttachmentField.svelte b/packages/client/src/components/app/forms/AttachmentField.svelte index e57510c770..e24115ebc0 100644 --- a/packages/client/src/components/app/forms/AttachmentField.svelte +++ b/packages/client/src/components/app/forms/AttachmentField.svelte @@ -2,17 +2,13 @@ import Field from "./Field.svelte" import { CoreDropzone } from "@budibase/bbui" import { getContext } from "svelte" - import { ValidFileExtensions } from "@budibase/shared-core" - import { environmentStore } from "../../../stores/index.js" export let field export let label export let disabled = false export let compact = false export let validation - export let extensions = $environmentStore.cloud - ? ValidFileExtensions.map(ext => `.${ext}`).join(", ") - : "*" + export let extensions export let onChange export let maximum = undefined diff --git a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte index 4e2a6025e5..a27c31bbe5 100644 --- a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte @@ -2,7 +2,6 @@ import { onMount } from "svelte" import { getContext } from "svelte" import { Dropzone } from "@budibase/bbui" - import { ValidFileExtensions } from "@budibase/shared-core" export let value export let focused = false @@ -14,7 +13,6 @@ const { API, notifications } = getContext("grid") const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"] - const validExtensions = ValidFileExtensions.map(ext => `.${ext}`).join(", ") let isOpen = false @@ -98,7 +96,6 @@ {value} compact on:change={e => onChange(e.detail)} - extensions={validExtensions} {processFiles} {deleteAttachments} {handleFileTooLarge} From 344256a80588543427987e93b32c67d25766783e Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 15:57:40 +0100 Subject: [PATCH 60/65] Fix proxy build --- .github/workflows/release-master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index e0adddc6a8..e1feaa1e7c 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -112,7 +112,7 @@ jobs: - name: Build proxy docker uses: docker/build-push-action@v5 with: - context: . + context: ./hosting/proxy push: true tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} file: ./hosting/proxy/Dockerfile From 674f40a06ee49dc1ffa7d56e8b79466e8dab7903 Mon Sep 17 00:00:00 2001 From: Samuel-Martineau Date: Fri, 27 Oct 2023 17:21:30 -0400 Subject: [PATCH 61/65] Fix how attachment URLs are handled --- packages/server/src/utilities/rowProcessor/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts index cf3875b2ea..604f872c81 100644 --- a/packages/server/src/utilities/rowProcessor/index.ts +++ b/packages/server/src/utilities/rowProcessor/index.ts @@ -241,7 +241,7 @@ export async function outputProcessing( continue } row[property].forEach((attachment: RowAttachment) => { - attachment.url = objectStore.getAppFileUrl(attachment.key) + attachment.url ??= objectStore.getAppFileUrl(attachment.key) }) } } else if ( From b3a4a921aa195aacf68c5dd8dfd84f27b45e1cf1 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 30 Oct 2023 16:47:47 +0100 Subject: [PATCH 62/65] Build multi platform proxy --- .github/workflows/release-master.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index e1feaa1e7c..df25182cd6 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -114,6 +114,7 @@ jobs: with: context: ./hosting/proxy push: true + platforms: linux/amd64,linux/arm64 tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} file: ./hosting/proxy/Dockerfile cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest From af59039d1c8a5735f0325c73c1c0da846c845c31 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 30 Oct 2023 16:46:27 +0000 Subject: [PATCH 63/65] Add tests for attachment processing endpoint. --- .../src/api/controllers/static/index.ts | 100 ++++++++++-------- .../src/api/routes/tests/attachment.spec.ts | 49 +++++++++ .../src/api/routes/tests/static.spec.js | 8 +- .../src/api/routes/tests/webhook.spec.ts | 8 +- .../integrations/tests/googlesheets.spec.ts | 9 +- .../src/tests/utilities/TestConfiguration.ts | 47 ++++---- .../src/tests/utilities/api/attachment.ts | 35 ++++++ .../server/src/tests/utilities/api/index.ts | 3 + packages/types/src/api/web/app/attachment.ts | 9 ++ packages/types/src/api/web/app/index.ts | 1 + 10 files changed, 199 insertions(+), 70 deletions(-) create mode 100644 packages/server/src/api/routes/tests/attachment.spec.ts create mode 100644 packages/server/src/tests/utilities/api/attachment.ts create mode 100644 packages/types/src/api/web/app/attachment.ts diff --git a/packages/server/src/api/controllers/static/index.ts b/packages/server/src/api/controllers/static/index.ts index e8d403ad12..8fbc0db910 100644 --- a/packages/server/src/api/controllers/static/index.ts +++ b/packages/server/src/api/controllers/static/index.ts @@ -13,35 +13,21 @@ import { } from "../../../utilities/fileSystem" import env from "../../../environment" import { DocumentType } from "../../../db/utils" -import { context, objectStore, utils, configs } from "@budibase/backend-core" +import { + context, + objectStore, + utils, + configs, + BadRequestError, +} from "@budibase/backend-core" import AWS from "aws-sdk" import fs from "fs" import sdk from "../../../sdk" import * as pro from "@budibase/pro" -import { App, Ctx } from "@budibase/types" -import environment from "../../../environment" +import { App, Ctx, ProcessAttachmentResponse, Upload } from "@budibase/types" const send = require("koa-send") -async function prepareUpload({ s3Key, bucket, metadata, file }: any) { - const response = await objectStore.upload({ - bucket, - metadata, - filename: s3Key, - path: file.path, - type: file.type, - }) - - // don't store a URL, work this out on the way out as the URL could change - return { - size: file.size, - name: file.name, - url: objectStore.getAppFileUrl(s3Key), - extension: [...file.name.split(".")].pop(), - key: response.Key, - } -} - export const toggleBetaUiFeature = async function (ctx: Ctx) { const cookieName = `beta:${ctx.params.feature}` @@ -75,34 +61,58 @@ export const serveBuilder = async function (ctx: Ctx) { await send(ctx, ctx.file, { root: builderPath }) } -export const uploadFile = async function (ctx: Ctx) { +export const uploadFile = async function ( + ctx: Ctx<{}, ProcessAttachmentResponse> +) { const file = ctx.request?.files?.file + if (!file) { + throw new BadRequestError("No file provided") + } + let files = file && Array.isArray(file) ? Array.from(file) : [file] - const uploads = files.map(async (file: any) => { - const fileExtension = [...file.name.split(".")].pop() - if ( - !environment.SELF_HOSTED && - !ValidFileExtensions.includes(fileExtension) - ) { - ctx.throw( - 400, - `Invalid file extension. Valid extensions are: ${ValidFileExtensions.join( - ", " - )}` - ) - } - // filenames converted to UUIDs so they are unique - const processedFileName = `${uuid.v4()}.${fileExtension}` + ctx.body = await Promise.all( + files.map(async file => { + if (!file.name) { + throw new BadRequestError( + "Attempted to upload a file without a filename" + ) + } - return prepareUpload({ - file, - s3Key: `${context.getProdAppId()}/attachments/${processedFileName}`, - bucket: ObjectStoreBuckets.APPS, + const extension = [...file.name.split(".")].pop() + if (!extension) { + throw new BadRequestError( + `File "${file.name}" has no extension, an extension is required to upload a file` + ) + } + + if (!env.SELF_HOSTED && !ValidFileExtensions.includes(extension)) { + throw new BadRequestError( + `File "${file.name}" has an invalid extension: "${extension}"` + ) + } + + // filenames converted to UUIDs so they are unique + const processedFileName = `${uuid.v4()}.${extension}` + + const s3Key = `${context.getProdAppId()}/attachments/${processedFileName}` + + const response = await objectStore.upload({ + bucket: ObjectStoreBuckets.APPS, + filename: s3Key, + path: file.path, + type: file.type, + }) + + return { + size: file.size, + name: file.name, + url: objectStore.getAppFileUrl(s3Key), + extension, + key: response.Key, + } }) - }) - - ctx.body = await Promise.all(uploads) + ) } export const deleteObjects = async function (ctx: Ctx) { diff --git a/packages/server/src/api/routes/tests/attachment.spec.ts b/packages/server/src/api/routes/tests/attachment.spec.ts new file mode 100644 index 0000000000..14d2e845f6 --- /dev/null +++ b/packages/server/src/api/routes/tests/attachment.spec.ts @@ -0,0 +1,49 @@ +import * as setup from "./utilities" +import { APIError } from "@budibase/types" + +describe("/api/applications/:appId/sync", () => { + let config = setup.getConfig() + + afterAll(setup.afterAll) + beforeAll(async () => { + await config.init() + }) + + describe("/api/attachments/process", () => { + it("should accept an image file upload", async () => { + let resp = await config.api.attachment.process( + "1px.jpg", + Buffer.from([0]) + ) + expect(resp.length).toBe(1) + + let upload = resp[0] + expect(upload.url.endsWith(".jpg")).toBe(true) + expect(upload.extension).toBe("jpg") + expect(upload.size).toBe(1) + expect(upload.name).toBe("1px.jpg") + }) + + it("should reject an upload with a malicious file extension", async () => { + await config.withEnv({ SELF_HOSTED: undefined }, async () => { + let resp = (await config.api.attachment.process( + "ohno.exe", + Buffer.from([0]), + { expectStatus: 400 } + )) as unknown as APIError + expect(resp.message).toContain("invalid extension") + }) + }) + + it("should reject an upload with no file", async () => { + let resp = (await config.api.attachment.process( + undefined as any, + undefined as any, + { + expectStatus: 400, + } + )) as unknown as APIError + expect(resp.message).toContain("No file provided") + }) + }) +}) diff --git a/packages/server/src/api/routes/tests/static.spec.js b/packages/server/src/api/routes/tests/static.spec.js index 13d963d057..a28d9ecd79 100644 --- a/packages/server/src/api/routes/tests/static.spec.js +++ b/packages/server/src/api/routes/tests/static.spec.js @@ -5,11 +5,15 @@ describe("/static", () => { let request = setup.getRequest() let config = setup.getConfig() let app + let cleanupEnv - afterAll(setup.afterAll) + afterAll(() => { + setup.afterAll() + cleanupEnv() + }) beforeAll(async () => { - config.modeSelf() + cleanupEnv = config.setEnv({ SELF_HOSTED: "true" }) app = await config.init() }) diff --git a/packages/server/src/api/routes/tests/webhook.spec.ts b/packages/server/src/api/routes/tests/webhook.spec.ts index e7046d07c8..118bfca95f 100644 --- a/packages/server/src/api/routes/tests/webhook.spec.ts +++ b/packages/server/src/api/routes/tests/webhook.spec.ts @@ -8,11 +8,15 @@ describe("/webhooks", () => { let request = setup.getRequest() let config = setup.getConfig() let webhook: Webhook + let cleanupEnv: () => void - afterAll(setup.afterAll) + afterAll(() => { + setup.afterAll() + cleanupEnv() + }) const setupTest = async () => { - config.modeSelf() + cleanupEnv = config.setEnv({ SELF_HOSTED: "true" }) await config.init() const autoConfig = basicAutomation() autoConfig.definition.trigger.schema = { diff --git a/packages/server/src/integrations/tests/googlesheets.spec.ts b/packages/server/src/integrations/tests/googlesheets.spec.ts index 748baddc39..a38c6bda45 100644 --- a/packages/server/src/integrations/tests/googlesheets.spec.ts +++ b/packages/server/src/integrations/tests/googlesheets.spec.ts @@ -35,13 +35,18 @@ import { FieldType, Table, TableSchema } from "@budibase/types" describe("Google Sheets Integration", () => { let integration: any, config = new TestConfiguration() + let cleanupEnv: () => void beforeAll(() => { - config.setGoogleAuth("test") + cleanupEnv = config.setEnv({ + GOOGLE_CLIENT_ID: "test", + GOOGLE_CLIENT_SECRET: "test", + }) }) afterAll(async () => { - await config.end() + cleanupEnv() + config.end() }) beforeEach(async () => { diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index cec8c8aa12..5096b054a6 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -58,6 +58,7 @@ import { } from "@budibase/types" import API from "./api" +import { cloneDeep } from "lodash" type DefaultUserValues = { globalUserId: string @@ -188,30 +189,38 @@ class TestConfiguration { } } - // MODES - setMultiTenancy = (value: boolean) => { - env._set("MULTI_TENANCY", value) - coreEnv._set("MULTI_TENANCY", value) + async withEnv(newEnvVars: Partial, f: () => Promise) { + let cleanup = this.setEnv(newEnvVars) + try { + await f() + } finally { + cleanup() + } } - setSelfHosted = (value: boolean) => { - env._set("SELF_HOSTED", value) - coreEnv._set("SELF_HOSTED", value) - } + /* + * Sets the environment variables to the given values and returns a function + * that can be called to reset the environment variables to their original values. + */ + setEnv(newEnvVars: Partial): () => void { + const oldEnv = cloneDeep(env) + const oldCoreEnv = cloneDeep(coreEnv) - setGoogleAuth = (value: string) => { - env._set("GOOGLE_CLIENT_ID", value) - env._set("GOOGLE_CLIENT_SECRET", value) - coreEnv._set("GOOGLE_CLIENT_ID", value) - coreEnv._set("GOOGLE_CLIENT_SECRET", value) - } + let key: keyof typeof newEnvVars + for (key in newEnvVars) { + env._set(key, newEnvVars[key]) + coreEnv._set(key, newEnvVars[key]) + } - modeCloud = () => { - this.setSelfHosted(false) - } + return () => { + for (const [key, value] of Object.entries(oldEnv)) { + env._set(key, value) + } - modeSelf = () => { - this.setSelfHosted(true) + for (const [key, value] of Object.entries(oldCoreEnv)) { + coreEnv._set(key, value) + } + } } // UTILS diff --git a/packages/server/src/tests/utilities/api/attachment.ts b/packages/server/src/tests/utilities/api/attachment.ts new file mode 100644 index 0000000000..a466f1a67e --- /dev/null +++ b/packages/server/src/tests/utilities/api/attachment.ts @@ -0,0 +1,35 @@ +import { + APIError, + Datasource, + ProcessAttachmentResponse, +} from "@budibase/types" +import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" +import fs from "fs" + +export class AttachmentAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + process = async ( + name: string, + file: Buffer | fs.ReadStream | string, + { expectStatus } = { expectStatus: 200 } + ): Promise => { + const result = await this.request + .post(`/api/attachments/process`) + .attach("file", file, name) + .set(this.config.defaultHeaders()) + + if (result.statusCode !== expectStatus) { + throw new Error( + `Expected status ${expectStatus} but got ${ + result.statusCode + }, body: ${JSON.stringify(result.body)}` + ) + } + + return result.body + } +} diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts index fce8237760..30ef7c478d 100644 --- a/packages/server/src/tests/utilities/api/index.ts +++ b/packages/server/src/tests/utilities/api/index.ts @@ -7,6 +7,7 @@ import { DatasourceAPI } from "./datasource" import { LegacyViewAPI } from "./legacyView" import { ScreenAPI } from "./screen" import { ApplicationAPI } from "./application" +import { AttachmentAPI } from "./attachment" export default class API { table: TableAPI @@ -17,6 +18,7 @@ export default class API { datasource: DatasourceAPI screen: ScreenAPI application: ApplicationAPI + attachment: AttachmentAPI constructor(config: TestConfiguration) { this.table = new TableAPI(config) @@ -27,5 +29,6 @@ export default class API { this.datasource = new DatasourceAPI(config) this.screen = new ScreenAPI(config) this.application = new ApplicationAPI(config) + this.attachment = new AttachmentAPI(config) } } diff --git a/packages/types/src/api/web/app/attachment.ts b/packages/types/src/api/web/app/attachment.ts new file mode 100644 index 0000000000..792bdf3885 --- /dev/null +++ b/packages/types/src/api/web/app/attachment.ts @@ -0,0 +1,9 @@ +export interface Upload { + size: number + name: string + url: string + extension: string + key: string +} + +export type ProcessAttachmentResponse = Upload[] diff --git a/packages/types/src/api/web/app/index.ts b/packages/types/src/api/web/app/index.ts index 276d7fa7c1..f5b876009b 100644 --- a/packages/types/src/api/web/app/index.ts +++ b/packages/types/src/api/web/app/index.ts @@ -5,3 +5,4 @@ export * from "./view" export * from "./rows" export * from "./table" export * from "./permission" +export * from "./attachment" From db398a839e056c1db9ecaf8176291f07b72d2831 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Mon, 30 Oct 2023 16:54:07 +0000 Subject: [PATCH 64/65] Bump version to 2.12.1 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 3179bc3b2e..6df4a4c4cd 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.12.0", + "version": "2.12.1", "npmClient": "yarn", "packages": [ "packages/*" From ca9491ce67694db85ff941f639310b5b1c9be56a Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 30 Oct 2023 16:55:57 +0000 Subject: [PATCH 65/65] Surface error message from attachments API to user. --- packages/builder/src/components/common/Dropzone.svelte | 2 +- .../src/components/grid/cells/AttachmentCell.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/components/common/Dropzone.svelte b/packages/builder/src/components/common/Dropzone.svelte index fd2359fd91..daa6ad1807 100644 --- a/packages/builder/src/components/common/Dropzone.svelte +++ b/packages/builder/src/components/common/Dropzone.svelte @@ -23,7 +23,7 @@ try { return await API.uploadBuilderAttachment(data) } catch (error) { - notifications.error("Failed to upload attachment") + notifications.error(error.message || "Failed to upload attachment") return [] } } diff --git a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte index a27c31bbe5..fc0001d55e 100644 --- a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte @@ -55,7 +55,7 @@ try { return await API.uploadBuilderAttachment(data) } catch (error) { - $notifications.error("Failed to upload attachment") + $notifications.error(error.message || "Failed to upload attachment") return [] } }