From d03fdb6df94f6d17ad0c1a79ed38f1196e70f535 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 3 Oct 2023 17:35:00 +0100 Subject: [PATCH 01/95] Add initial support for query datasources in grids --- packages/client/manifest.json | 5 +- .../src/components/app/GridBlock.svelte | 7 +- .../src/components/grid/cells/DataCell.svelte | 2 +- .../src/components/grid/layout/NewRow.svelte | 6 +- .../src/components/grid/stores/config.js | 11 +- .../src/components/grid/stores/datasource.js | 25 +++- .../src/components/grid/stores/index.js | 2 + .../src/components/grid/stores/query.js | 108 ++++++++++++++++++ .../src/components/grid/stores/rows.js | 10 +- .../src/components/grid/stores/table.js | 7 +- .../src/components/grid/stores/viewV2.js | 16 +-- .../src/fetch/{fetchData.js => index.js} | 11 ++ packages/frontend-core/src/index.js | 2 +- 13 files changed, 178 insertions(+), 34 deletions(-) create mode 100644 packages/frontend-core/src/components/grid/stores/query.js rename packages/frontend-core/src/fetch/{fetchData.js => index.js} (72%) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 4e56ca758d..b1391e36fd 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -5545,12 +5545,11 @@ "width": 600, "height": 400 }, - "info": "Grid Blocks are only compatible with internal or SQL tables", "settings": [ { - "type": "table", + "type": "dataSource", "label": "Data", - "key": "table", + "key": "datasource", "required": true }, { diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index 9bdea52124..1cdbcf00ff 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -4,7 +4,7 @@ import { getContext } from "svelte" import { Grid } from "@budibase/frontend-core" - export let table + export let datasource export let allowAddRows = true export let allowEditRows = true export let allowDeleteRows = true @@ -15,6 +15,9 @@ export let fixedRowHeight = null export let columns = null + // Legacy settings + export let table + const component = getContext("component") const { styleable, API, builderStore, notificationStore } = getContext("sdk") @@ -38,7 +41,7 @@ class:in-builder={$builderStore.inBuilder} > { [props, hasNonAutoColumn], ([$props, $hasNonAutoColumn]) => { let config = { ...$props } + const type = $props.datasource?.type // Disable some features if we're editing a view - if ($props.datasource?.type === "viewV2") { + if (type === "viewV2") { config.canEditColumns = false } @@ -48,6 +49,14 @@ export const deriveStores = context => { config.canAddRows = false } + // Disable features for non DS+ + if (!["table", "viewV2"].includes(type)) { + config.canAddRows = false + config.canEditRows = false + config.canDeleteRows = false + config.canExpandRows = false + } + return config } ) diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.js index 3f4347953e..e56b37a5f3 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -1,4 +1,5 @@ import { derived, get, writable } from "svelte/store" +import { getDatasourceDefinition } from "../../../fetch" export const createStores = () => { const definition = writable(null) @@ -19,6 +20,14 @@ export const deriveStores = context => { } let newSchema = { ...$definition?.schema } + // Ensure schema is configured as objects. + // Certain datasources like queries use primitives. + Object.keys(newSchema).forEach(key => { + if (typeof newSchema[key] !== "object") { + newSchema[key] = { type: newSchema[key] } + } + }) + // Apply schema overrides Object.keys($schemaOverrides || {}).forEach(field => { if (newSchema[field]) { @@ -48,7 +57,16 @@ export const deriveStores = context => { } export const createActions = context => { - const { datasource, definition, config, dispatch, table, viewV2 } = context + const { + API, + datasource, + definition, + config, + dispatch, + table, + viewV2, + query, + } = context // Gets the appropriate API for the configured datasource type const getAPI = () => { @@ -58,6 +76,8 @@ export const createActions = context => { return table case "viewV2": return viewV2 + case "query": + return query default: return null } @@ -65,7 +85,8 @@ export const createActions = context => { // Refreshes the datasource definition const refreshDefinition = async () => { - return await getAPI()?.actions.refreshDefinition() + const def = await getDatasourceDefinition({ API, datasource }) + definition.set(def) } // Saves the datasource definition diff --git a/packages/frontend-core/src/components/grid/stores/index.js b/packages/frontend-core/src/components/grid/stores/index.js index 7b73ea8be6..5a46227b49 100644 --- a/packages/frontend-core/src/components/grid/stores/index.js +++ b/packages/frontend-core/src/components/grid/stores/index.js @@ -17,6 +17,7 @@ import * as Filter from "./filter" import * as Notifications from "./notifications" import * as Table from "./table" import * as ViewV2 from "./viewV2" +import * as Query from "./query" import * as Datasource from "./datasource" const DependencyOrderedStores = [ @@ -26,6 +27,7 @@ const DependencyOrderedStores = [ Scroll, Table, ViewV2, + Query, Datasource, Columns, Rows, diff --git a/packages/frontend-core/src/components/grid/stores/query.js b/packages/frontend-core/src/components/grid/stores/query.js new file mode 100644 index 0000000000..6ac0547d27 --- /dev/null +++ b/packages/frontend-core/src/components/grid/stores/query.js @@ -0,0 +1,108 @@ +import { get } from "svelte/store" + +export const createActions = context => { + const { API, columns, stickyColumn } = context + + const saveDefinition = async newDefinition => { + await API.saveQuery(newDefinition) + } + + const saveRow = async () => { + throw "Rows cannot be updated through queries" + } + + const deleteRows = async () => { + throw "Rows cannot be deleted through queries" + } + + const getRow = () => { + throw "Queries don't support fetching individual rows" + } + + const isDatasourceValid = datasource => { + return datasource?.type === "query" && datasource?._id + } + + const canUseColumn = name => { + const $columns = get(columns) + const $sticky = get(stickyColumn) + return $columns.some(col => col.name === name) || $sticky?.name === name + } + + return { + query: { + actions: { + saveDefinition, + addRow: saveRow, + updateRow: saveRow, + deleteRows, + getRow, + isDatasourceValid, + canUseColumn, + }, + }, + } +} + +export const initialise = context => { + const { + datasource, + sort, + filter, + query, + initialFilter, + initialSortColumn, + initialSortOrder, + fetch, + } = context + + // Keep a list of subscriptions so that we can clear them when the datasource + // config changes + let unsubscribers = [] + + // Observe datasource changes and apply logic for view V2 datasources + datasource.subscribe($datasource => { + // Clear previous subscriptions + unsubscribers?.forEach(unsubscribe => unsubscribe()) + unsubscribers = [] + if (!query.actions.isDatasourceValid($datasource)) { + return + } + + // Wipe state + filter.set(get(initialFilter)) + sort.set({ + column: get(initialSortColumn), + order: get(initialSortOrder) || "ascending", + }) + + // Update fetch when filter changes + unsubscribers.push( + filter.subscribe($filter => { + // Ensure we're updating the correct fetch + const $fetch = get(fetch) + if ($fetch?.options?.datasource?._id !== $datasource._id) { + return + } + $fetch.update({ + filter: $filter, + }) + }) + ) + + // Update fetch when sorting changes + unsubscribers.push( + sort.subscribe($sort => { + // Ensure we're updating the correct fetch + const $fetch = get(fetch) + if ($fetch?.options?.datasource?._id !== $datasource._id) { + return + } + $fetch.update({ + sortOrder: $sort.order || "ascending", + sortColumn: $sort.column, + }) + }) + ) + }) +} diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 392bf392e8..2e2832f209 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -1,7 +1,8 @@ import { writable, derived, get } from "svelte/store" -import { fetchData } from "../../../fetch/fetchData" +import { fetchData } from "../../../fetch" import { NewRowID, RowPageSize } from "../lib/constants" import { tick } from "svelte" +import { Helpers } from "@budibase/bbui" export const createStores = () => { const rows = writable([]) @@ -413,6 +414,13 @@ export const createActions = context => { let newRow for (let i = 0; i < newRows.length; i++) { newRow = newRows[i] + + // Ensure we have a unique _id. + // This means generating one for non DS+. + if (!newRow._id) { + newRow._id = Helpers.hashString(JSON.stringify(newRow)) + } + if (!rowCacheMap[newRow._id]) { rowCacheMap[newRow._id] = true rowsToAppend.push(newRow) diff --git a/packages/frontend-core/src/components/grid/stores/table.js b/packages/frontend-core/src/components/grid/stores/table.js index ed13609f45..9a4058ac8f 100644 --- a/packages/frontend-core/src/components/grid/stores/table.js +++ b/packages/frontend-core/src/components/grid/stores/table.js @@ -3,11 +3,7 @@ import { get } from "svelte/store" const SuppressErrors = true export const createActions = context => { - const { definition, API, datasource, columns, stickyColumn } = context - - const refreshDefinition = async () => { - definition.set(await API.fetchTableDefinition(get(datasource).tableId)) - } + const { API, datasource, columns, stickyColumn } = context const saveDefinition = async newDefinition => { await API.saveTable(newDefinition) @@ -52,7 +48,6 @@ export const createActions = context => { return { table: { actions: { - refreshDefinition, saveDefinition, addRow: saveRow, updateRow: saveRow, diff --git a/packages/frontend-core/src/components/grid/stores/viewV2.js b/packages/frontend-core/src/components/grid/stores/viewV2.js index b9a4bc099b..000727b262 100644 --- a/packages/frontend-core/src/components/grid/stores/viewV2.js +++ b/packages/frontend-core/src/components/grid/stores/viewV2.js @@ -3,20 +3,7 @@ import { get } from "svelte/store" const SuppressErrors = true export const createActions = context => { - const { definition, API, datasource, columns, stickyColumn } = context - - const refreshDefinition = async () => { - const $datasource = get(datasource) - if (!$datasource) { - definition.set(null) - return - } - const table = await API.fetchTableDefinition($datasource.tableId) - const view = Object.values(table?.views || {}).find( - view => view.id === $datasource.id - ) - definition.set(view) - } + const { API, datasource, columns, stickyColumn } = context const saveDefinition = async newDefinition => { await API.viewV2.update(newDefinition) @@ -61,7 +48,6 @@ export const createActions = context => { return { viewV2: { actions: { - refreshDefinition, saveDefinition, addRow: saveRow, updateRow: saveRow, diff --git a/packages/frontend-core/src/fetch/fetchData.js b/packages/frontend-core/src/fetch/index.js similarity index 72% rename from packages/frontend-core/src/fetch/fetchData.js rename to packages/frontend-core/src/fetch/index.js index 063dd02cbf..0edd07762b 100644 --- a/packages/frontend-core/src/fetch/fetchData.js +++ b/packages/frontend-core/src/fetch/index.js @@ -24,7 +24,18 @@ const DataFetchMap = { jsonarray: JSONArrayFetch, } +// Constructs a new fetch model for a certain datasource export const fetchData = ({ API, datasource, options }) => { const Fetch = DataFetchMap[datasource?.type] || TableFetch return new Fetch({ API, datasource, ...options }) } + +// Fetches the definition of any type of datasource +export const getDatasourceDefinition = async ({ API, datasource }) => { + const handler = DataFetchMap[datasource?.type] + if (!handler) { + return null + } + const instance = new handler({ API }) + return await instance.getDefinition(datasource) +} diff --git a/packages/frontend-core/src/index.js b/packages/frontend-core/src/index.js index b0afc0c25d..f51be616f8 100644 --- a/packages/frontend-core/src/index.js +++ b/packages/frontend-core/src/index.js @@ -1,5 +1,5 @@ export { createAPIClient } from "./api" -export { fetchData } from "./fetch/fetchData" +export { fetchData } from "./fetch" export { Utils } from "./utils" export * as Constants from "./constants" export * from "./stores" From c4a516ccb37e838038011ba39d198e65a1ca2c79 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 4 Oct 2023 09:25:35 +0100 Subject: [PATCH 02/95] Ensure grid schema structure is predictable and prevent copying IDs for query datasources --- .../components/grid/cells/HeaderCell.svelte | 7 +-- .../grid/overlays/MenuOverlay.svelte | 4 +- .../src/components/grid/stores/columns.js | 33 +++++++------ .../src/components/grid/stores/datasource.js | 48 +++++++++++-------- .../src/components/grid/stores/rows.js | 2 +- .../src/components/grid/stores/sort.js | 15 +++--- 6 files changed, 61 insertions(+), 48 deletions(-) diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index a58b4c8fe4..5ac70c93c8 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -22,6 +22,7 @@ columns, definition, datasource, + schema, } = getContext("grid") const bannedDisplayColumnTypes = [ @@ -126,16 +127,16 @@ // Generate new name let newName = `${column.name} copy` let attempts = 2 - while ($definition.schema[newName]) { + while ($schema[newName]) { newName = `${column.name} copy ${attempts++}` } // Save schema with new column - const existingColumnDefinition = $definition.schema[column.name] + const existingColumnDefinition = $schema[column.name] await datasource.actions.saveDefinition({ ...$definition, schema: { - ...$definition.schema, + ...$schema, [newName]: { ...existingColumnDefinition, name: newName, diff --git a/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte b/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte index cbf2c6ee4e..7fb2bb138d 100644 --- a/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte +++ b/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte @@ -75,7 +75,9 @@ copyToClipboard($focusedRow?._id)} on:click={menu.actions.close} > diff --git a/packages/frontend-core/src/components/grid/stores/columns.js b/packages/frontend-core/src/components/grid/stores/columns.js index 629d5dd893..c36afea7a1 100644 --- a/packages/frontend-core/src/components/grid/stores/columns.js +++ b/packages/frontend-core/src/components/grid/stores/columns.js @@ -69,7 +69,7 @@ export const deriveStores = context => { } export const createActions = context => { - const { columns, stickyColumn, datasource, definition } = context + const { columns, stickyColumn, datasource, definition, schema } = context // Updates the datasources primary display column const changePrimaryDisplay = async column => { @@ -101,7 +101,7 @@ export const createActions = context => { const $columns = get(columns) const $definition = get(definition) const $stickyColumn = get(stickyColumn) - const newSchema = cloneDeep($definition.schema) + let newSchema = cloneDeep(get(schema)) || {} // Build new updated datasource schema Object.keys(newSchema).forEach(column => { @@ -142,11 +142,11 @@ export const createActions = context => { } export const initialise = context => { - const { definition, columns, stickyColumn, schema } = context + const { definition, columns, stickyColumn, enrichedSchema } = context // Merge new schema fields with existing schema in order to preserve widths - schema.subscribe($schema => { - if (!$schema) { + enrichedSchema.subscribe($enrichedSchema => { + if (!$enrichedSchema) { columns.set([]) stickyColumn.set(null) return @@ -155,13 +155,16 @@ export const initialise = context => { // Find primary display let primaryDisplay - if ($definition.primaryDisplay && $schema[$definition.primaryDisplay]) { + if ( + $definition.primaryDisplay && + $enrichedSchema[$definition.primaryDisplay] + ) { primaryDisplay = $definition.primaryDisplay } // Get field list let fields = [] - Object.keys($schema).forEach(field => { + Object.keys($enrichedSchema).forEach(field => { if (field !== primaryDisplay) { fields.push(field) } @@ -172,11 +175,11 @@ export const initialise = context => { fields .map(field => ({ name: field, - label: $schema[field].displayName || field, - schema: $schema[field], - width: $schema[field].width || DefaultColumnWidth, - visible: $schema[field].visible ?? true, - order: $schema[field].order, + label: $enrichedSchema[field].displayName || field, + schema: $enrichedSchema[field], + width: $enrichedSchema[field].width || DefaultColumnWidth, + visible: $enrichedSchema[field].visible ?? true, + order: $enrichedSchema[field].order, })) .sort((a, b) => { // Sort by order first @@ -207,9 +210,9 @@ export const initialise = context => { } stickyColumn.set({ name: primaryDisplay, - label: $schema[primaryDisplay].displayName || primaryDisplay, - schema: $schema[primaryDisplay], - width: $schema[primaryDisplay].width || DefaultColumnWidth, + label: $enrichedSchema[primaryDisplay].displayName || primaryDisplay, + schema: $enrichedSchema[primaryDisplay], + width: $enrichedSchema[primaryDisplay].width || DefaultColumnWidth, visible: true, order: 0, left: GutterWidth, diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.js index e56b37a5f3..0d70424550 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -12,27 +12,36 @@ export const createStores = () => { export const deriveStores = context => { const { definition, schemaOverrides, columnWhitelist } = context - const schema = derived( - [definition, schemaOverrides, columnWhitelist], - ([$definition, $schemaOverrides, $columnWhitelist]) => { - if (!$definition?.schema) { + const schema = derived(definition, $definition => { + let schema = $definition?.schema + if (!schema) { + return null + } + + // Ensure schema is configured as objects. + // Certain datasources like queries use primitives. + Object.keys(schema || {}).forEach(key => { + if (typeof schema[key] !== "object") { + schema[key] = { type: schema[key] } + } + }) + + return schema + }) + + const enrichedSchema = derived( + [schema, schemaOverrides, columnWhitelist], + ([$schema, $schemaOverrides, $columnWhitelist]) => { + if (!$schema) { return null } - let newSchema = { ...$definition?.schema } - - // Ensure schema is configured as objects. - // Certain datasources like queries use primitives. - Object.keys(newSchema).forEach(key => { - if (typeof newSchema[key] !== "object") { - newSchema[key] = { type: newSchema[key] } - } - }) + let enrichedSchema = { ...$schema } // Apply schema overrides Object.keys($schemaOverrides || {}).forEach(field => { - if (newSchema[field]) { - newSchema[field] = { - ...newSchema[field], + if (enrichedSchema[field]) { + enrichedSchema[field] = { + ...enrichedSchema[field], ...$schemaOverrides[field], } } @@ -40,19 +49,20 @@ export const deriveStores = context => { // Apply whitelist if specified if ($columnWhitelist?.length) { - Object.keys(newSchema).forEach(key => { + Object.keys(enrichedSchema).forEach(key => { if (!$columnWhitelist.includes(key)) { - delete newSchema[key] + delete enrichedSchema[key] } }) } - return newSchema + return enrichedSchema } ) return { schema, + enrichedSchema, } } diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 2e2832f209..1eafb20756 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -418,7 +418,7 @@ export const createActions = context => { // Ensure we have a unique _id. // This means generating one for non DS+. if (!newRow._id) { - newRow._id = Helpers.hashString(JSON.stringify(newRow)) + newRow._id = `fake-${Helpers.hashString(JSON.stringify(newRow))}` } if (!rowCacheMap[newRow._id]) { diff --git a/packages/frontend-core/src/components/grid/stores/sort.js b/packages/frontend-core/src/components/grid/stores/sort.js index 734a876eed..336570d012 100644 --- a/packages/frontend-core/src/components/grid/stores/sort.js +++ b/packages/frontend-core/src/components/grid/stores/sort.js @@ -17,7 +17,7 @@ export const createStores = context => { } export const initialise = context => { - const { sort, initialSortColumn, initialSortOrder, definition } = context + const { sort, initialSortColumn, initialSortOrder, schema } = context // Reset sort when initial sort props change initialSortColumn.subscribe(newSortColumn => { @@ -28,15 +28,12 @@ export const initialise = context => { }) // Derive if the current sort column exists in the schema - const sortColumnExists = derived( - [sort, definition], - ([$sort, $definition]) => { - if (!$sort?.column || !$definition) { - return true - } - return $definition.schema?.[$sort.column] != null + const sortColumnExists = derived([sort, schema], ([$sort, $schema]) => { + if (!$sort?.column || !$schema) { + return true } - ) + return $schema[$sort.column] != null + }) // Clear sort state if our sort column does not exist sortColumnExists.subscribe(exists => { From 77f87af87f3f169b0b90b9fa39b4c102991bb956 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 4 Oct 2023 09:36:42 +0100 Subject: [PATCH 03/95] Improve grid handling of invalid datasources and fix potential error when encoutering invalid datasources --- .../src/components/grid/layout/Grid.svelte | 16 ++++++++-------- .../src/components/grid/stores/rows.js | 2 ++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/frontend-core/src/components/grid/layout/Grid.svelte b/packages/frontend-core/src/components/grid/layout/Grid.svelte index 44e0a972f1..e2ecd0f968 100644 --- a/packages/frontend-core/src/components/grid/layout/Grid.svelte +++ b/packages/frontend-core/src/components/grid/layout/Grid.svelte @@ -141,7 +141,14 @@ {/if} - {#if $loaded} + {#if $error} +
+
There was a problem loading your grid
+
+ {$error} +
+
+ {:else if $loaded}
@@ -171,13 +178,6 @@
- {:else if $error} -
-
There was a problem loading your grid
-
- {$error} -
-
{/if} {#if $loading && !$error}
diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 1eafb20756..e6251a5afa 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -94,12 +94,14 @@ export const createActions = context => { datasource.subscribe(async $datasource => { // Unsub from previous fetch if one exists unsubscribe?.() + unsubscribe = null fetch.set(null) instanceLoaded.set(false) loading.set(true) // Abandon if we don't have a valid datasource if (!datasource.actions.isDatasourceValid($datasource)) { + error.set("Datasource is invalid") return } From 88c4d0cd203c0b1f8788b2898810c381c5c1af1f Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 5 Oct 2023 08:23:01 +0100 Subject: [PATCH 04/95] Update grids to work with all datasources --- .../src/components/grid/stores/config.js | 2 + .../src/components/grid/stores/datasource.js | 12 +++--- .../{query.js => datasources/nonPlus.js} | 38 ++++++++++++------- .../grid/stores/{ => datasources}/table.js | 0 .../grid/stores/{ => datasources}/viewV2.js | 0 .../src/components/grid/stores/index.js | 8 ++-- 6 files changed, 38 insertions(+), 22 deletions(-) rename packages/frontend-core/src/components/grid/stores/{query.js => datasources/nonPlus.js} (64%) rename packages/frontend-core/src/components/grid/stores/{ => datasources}/table.js (100%) rename packages/frontend-core/src/components/grid/stores/{ => datasources}/viewV2.js (100%) diff --git a/packages/frontend-core/src/components/grid/stores/config.js b/packages/frontend-core/src/components/grid/stores/config.js index 0efdc2104e..6da6ebf11e 100644 --- a/packages/frontend-core/src/components/grid/stores/config.js +++ b/packages/frontend-core/src/components/grid/stores/config.js @@ -55,6 +55,8 @@ export const deriveStores = context => { config.canEditRows = false config.canDeleteRows = false config.canExpandRows = false + config.canSaveSchema = false + config.canEditColumns = false } return config diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.js index 0d70424550..6a10cb8b9b 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -75,21 +75,23 @@ export const createActions = context => { dispatch, table, viewV2, - query, + nonPlus, } = context // Gets the appropriate API for the configured datasource type const getAPI = () => { const $datasource = get(datasource) - switch ($datasource?.type) { + const type = $datasource?.type + if (!type) { + return null + } + switch (type) { case "table": return table case "viewV2": return viewV2 - case "query": - return query default: - return null + return nonPlus } } diff --git a/packages/frontend-core/src/components/grid/stores/query.js b/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js similarity index 64% rename from packages/frontend-core/src/components/grid/stores/query.js rename to packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js index 6ac0547d27..ea024643aa 100644 --- a/packages/frontend-core/src/components/grid/stores/query.js +++ b/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js @@ -1,26 +1,32 @@ import { get } from "svelte/store" export const createActions = context => { - const { API, columns, stickyColumn } = context + const { columns, stickyColumn, table, viewV2 } = context - const saveDefinition = async newDefinition => { - await API.saveQuery(newDefinition) + const saveDefinition = async () => { + throw "This datasource does not support updating the definition" } const saveRow = async () => { - throw "Rows cannot be updated through queries" + throw "This datasource does not support saving rows" } const deleteRows = async () => { - throw "Rows cannot be deleted through queries" + throw "This datasource does not support deleting rows" } const getRow = () => { - throw "Queries don't support fetching individual rows" + throw "This datasource does not support fetching individual rows" } const isDatasourceValid = datasource => { - return datasource?.type === "query" && datasource?._id + // There are many different types and shapes of datasource, so we only + // check that we aren't null + return ( + !table.actions.isDatasourceValid(datasource) && + !viewV2.actions.isDatasourceValid(datasource) && + datasource?.type != null + ) } const canUseColumn = name => { @@ -30,7 +36,7 @@ export const createActions = context => { } return { - query: { + nonPlus: { actions: { saveDefinition, addRow: saveRow, @@ -44,18 +50,22 @@ export const createActions = context => { } } +// Small util to compare datasource definitions +const isSameDatasource = (a, b) => { + return JSON.stringify(a) === JSON.stringify(b) +} + export const initialise = context => { const { datasource, sort, filter, - query, + nonPlus, initialFilter, initialSortColumn, initialSortOrder, fetch, } = context - // Keep a list of subscriptions so that we can clear them when the datasource // config changes let unsubscribers = [] @@ -65,7 +75,7 @@ export const initialise = context => { // Clear previous subscriptions unsubscribers?.forEach(unsubscribe => unsubscribe()) unsubscribers = [] - if (!query.actions.isDatasourceValid($datasource)) { + if (!nonPlus.actions.isDatasourceValid($datasource)) { return } @@ -81,7 +91,8 @@ export const initialise = context => { filter.subscribe($filter => { // Ensure we're updating the correct fetch const $fetch = get(fetch) - if ($fetch?.options?.datasource?._id !== $datasource._id) { + if (!isSameDatasource($fetch?.options?.datasource, $datasource)) { + console.log("skip, different ds") return } $fetch.update({ @@ -95,7 +106,8 @@ export const initialise = context => { sort.subscribe($sort => { // Ensure we're updating the correct fetch const $fetch = get(fetch) - if ($fetch?.options?.datasource?._id !== $datasource._id) { + if (!isSameDatasource($fetch?.options?.datasource, $datasource)) { + console.log("skip, different ds") return } $fetch.update({ diff --git a/packages/frontend-core/src/components/grid/stores/table.js b/packages/frontend-core/src/components/grid/stores/datasources/table.js similarity index 100% rename from packages/frontend-core/src/components/grid/stores/table.js rename to packages/frontend-core/src/components/grid/stores/datasources/table.js diff --git a/packages/frontend-core/src/components/grid/stores/viewV2.js b/packages/frontend-core/src/components/grid/stores/datasources/viewV2.js similarity index 100% rename from packages/frontend-core/src/components/grid/stores/viewV2.js rename to packages/frontend-core/src/components/grid/stores/datasources/viewV2.js diff --git a/packages/frontend-core/src/components/grid/stores/index.js b/packages/frontend-core/src/components/grid/stores/index.js index 5a46227b49..10fe932aab 100644 --- a/packages/frontend-core/src/components/grid/stores/index.js +++ b/packages/frontend-core/src/components/grid/stores/index.js @@ -15,10 +15,10 @@ import * as Config from "./config" import * as Sort from "./sort" import * as Filter from "./filter" import * as Notifications from "./notifications" -import * as Table from "./table" -import * as ViewV2 from "./viewV2" -import * as Query from "./query" import * as Datasource from "./datasource" +import * as Table from "./datasources/table" +import * as ViewV2 from "./datasources/viewV2" +import * as NonPlus from "./datasources/nonPlus" const DependencyOrderedStores = [ Sort, @@ -27,7 +27,7 @@ const DependencyOrderedStores = [ Scroll, Table, ViewV2, - Query, + NonPlus, Datasource, Columns, Rows, From b75c78dae54f74df60341615580449851297fe87 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 5 Oct 2023 08:24:30 +0100 Subject: [PATCH 05/95] Remove log --- .../src/components/grid/stores/datasources/nonPlus.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js b/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js index ea024643aa..e04d05ec59 100644 --- a/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js +++ b/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js @@ -92,7 +92,6 @@ export const initialise = context => { // Ensure we're updating the correct fetch const $fetch = get(fetch) if (!isSameDatasource($fetch?.options?.datasource, $datasource)) { - console.log("skip, different ds") return } $fetch.update({ @@ -107,7 +106,6 @@ export const initialise = context => { // Ensure we're updating the correct fetch const $fetch = get(fetch) if (!isSameDatasource($fetch?.options?.datasource, $datasource)) { - console.log("skip, different ds") return } $fetch.update({ From 43c30d877beb3b1e0b21b3da617d7ad205534206 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 5 Oct 2023 14:42:26 +0100 Subject: [PATCH 06/95] Add new custom datasource type --- .../settings/controls/DataSourceSelect.svelte | 81 ++++++++++++---- .../src/components/grid/lib/utils.js | 2 +- .../src/components/grid/stores/rows.js | 2 +- .../frontend-core/src/fetch/CustomFetch.js | 97 +++++++++++++++++++ packages/frontend-core/src/fetch/index.js | 2 + 5 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 packages/frontend-core/src/fetch/CustomFetch.js diff --git a/packages/builder/src/components/design/settings/controls/DataSourceSelect.svelte b/packages/builder/src/components/design/settings/controls/DataSourceSelect.svelte index d70929469a..e4b3f972ba 100644 --- a/packages/builder/src/components/design/settings/controls/DataSourceSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/DataSourceSelect.svelte @@ -9,6 +9,7 @@ Heading, Drawer, DrawerContent, + Icon, } from "@budibase/bbui" import { createEventDispatcher } from "svelte" import { store, currentAsset } from "builderStore" @@ -22,6 +23,7 @@ import BindingBuilder from "components/integration/QueryBindingBuilder.svelte" import IntegrationQueryEditor from "components/integration/index.svelte" import { makePropSafe as safe } from "@budibase/string-templates" + import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" export let value = {} export let otherSources @@ -31,9 +33,12 @@ const dispatch = createEventDispatcher() const arrayTypes = ["attachment", "array"] + let anchorRight, dropdownRight let drawer let tmpQueryParams + let tmpCustomData + let customDataValid = true $: text = value?.label ?? "Choose an option" $: tables = $tablesStore.list.map(m => ({ @@ -125,6 +130,10 @@ value: `{{ literal ${runtimeBinding} }}`, } }) + $: custom = { + type: "custom", + label: "Custom", + } const handleSelected = selected => { dispatch("change", selected) @@ -151,6 +160,11 @@ drawer.show() } + const openCustomDrawer = () => { + tmpCustomData = value.data || "" + drawer.show() + } + const getQueryValue = queries => { return queries.find(q => q._id === value._id) || value } @@ -162,6 +176,14 @@ }) drawer.hide() } + + const saveCustomData = () => { + handleSelected({ + ...value, + data: tmpCustomData, + }) + drawer.hide() + }
@@ -172,7 +194,9 @@ on:click={dropdownRight.show} /> {#if value?.type === "query"} - +
+ +
@@ -198,6 +222,31 @@ {/if} + {#if value?.type === "custom"} +
+ +
+ + +
+ Provide a JavaScript or JSON array to use as data +
+ (tmpCustomData = event.detail)} + {bindings} + allowJS + allowHelpers + /> +
+ {/if}
@@ -340,16 +390,7 @@ background-color: var(--spectrum-global-color-gray-200); } - i { - margin-left: 5px; - display: flex; - align-items: center; - transition: all 0.2s; - } - - i:hover { - transform: scale(1.1); - font-weight: 600; - cursor: pointer; + .icon { + margin-left: 8px; } diff --git a/packages/frontend-core/src/components/grid/lib/utils.js b/packages/frontend-core/src/components/grid/lib/utils.js index 9383f69f66..d3898a3b18 100644 --- a/packages/frontend-core/src/components/grid/lib/utils.js +++ b/packages/frontend-core/src/components/grid/lib/utils.js @@ -1,6 +1,6 @@ export const getColor = (idx, opacity = 0.3) => { if (idx == null || idx === -1) { - return null + idx = 0 } return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, ${opacity})` } diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index e6251a5afa..5b6f4fd7f9 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -420,7 +420,7 @@ export const createActions = context => { // Ensure we have a unique _id. // This means generating one for non DS+. if (!newRow._id) { - newRow._id = `fake-${Helpers.hashString(JSON.stringify(newRow))}` + newRow._id = `fake-${Math.random()}` } if (!rowCacheMap[newRow._id]) { diff --git a/packages/frontend-core/src/fetch/CustomFetch.js b/packages/frontend-core/src/fetch/CustomFetch.js new file mode 100644 index 0000000000..baa7ce3d4d --- /dev/null +++ b/packages/frontend-core/src/fetch/CustomFetch.js @@ -0,0 +1,97 @@ +import DataFetch from "./DataFetch.js" + +export default class CustomFetch extends DataFetch { + getType(value) { + if (value == null) { + return "string" + } + const type = typeof value + if (type === "object") { + if (Array.isArray(value)) { + return "array" + } + return "json" + } else { + return type + } + } + + // Parses the custom data into an array format + parseCustomData(data) { + if (!data) { + return [] + } + + // Happy path - already an array + if (Array.isArray(data)) { + return data + } + + // Handle string cases + if (typeof data === "string") { + // Try JSON parsing + try { + data = JSON.parse(data) + if (Array.isArray(data)) { + return data + } + } catch (error) { + // Ignore + } + + // Try a simple CSV + return data.split(",").map(x => x.trim()) + } + + // Other cases we just assume it's a single object and wrap it + return [data] + } + + // Enriches the custom data to ensure the structure and format is usable + enrichCustomData(data) { + if (!data?.length) { + return [] + } + + // Filter out any invalid values + data = data.filter(x => x != null && !Array.isArray(x)) + + // Ensure all values are packed into objects + return data.map(x => (typeof x === "object" ? x : { value: x })) + } + + getCustomData(datasource) { + return this.enrichCustomData(this.parseCustomData(datasource?.data)) + } + + async getDefinition(datasource) { + // Try and work out the schema from the array provided + let schema = {} + const data = this.getCustomData(datasource) + + // Go through every object and extract all valid keys + for (let datum of data) { + for (let key of Object.keys(datum)) { + if (key === "_id") { + continue + } + if (!schema[key]) { + schema[key] = { type: this.getType(datum[key]) } + } + } + } + + return { + schema, + } + } + + async getData() { + const { datasource } = this.options + return { + rows: this.getCustomData(datasource), + hasNextPage: false, + cursor: null, + } + } +} diff --git a/packages/frontend-core/src/fetch/index.js b/packages/frontend-core/src/fetch/index.js index 0edd07762b..d133942bb7 100644 --- a/packages/frontend-core/src/fetch/index.js +++ b/packages/frontend-core/src/fetch/index.js @@ -8,6 +8,7 @@ import FieldFetch from "./FieldFetch.js" import JSONArrayFetch from "./JSONArrayFetch.js" import UserFetch from "./UserFetch.js" import GroupUserFetch from "./GroupUserFetch.js" +import CustomFetch from "./CustomFetch.js" const DataFetchMap = { table: TableFetch, @@ -17,6 +18,7 @@ const DataFetchMap = { link: RelationshipFetch, user: UserFetch, groupUser: GroupUserFetch, + custom: CustomFetch, // Client specific datasource types provider: NestedProviderFetch, From 678033cc8b7276e5823536925cfebc577527f764 Mon Sep 17 00:00:00 2001 From: Mitch-Budibase Date: Thu, 5 Oct 2023 17:39:40 +0100 Subject: [PATCH 07/95] License Key - Activate & Manage Tests There are two test files, license.activate.spec.ts and license.manage.spec.ts These test files each contain a test: - Creates, activates, and deletes an online license for a self hosted account - license.activate.spec.ts - Retrieves plans, creates checkout session, and updates license - license.manage.spec.ts Updated and created API files - StripeAPI - LicenseAPI - internal-api LicenseAPI - index & AccountInternalAPI also updated to reflect API file changes --- .../src/account-api/api/AccountInternalAPI.ts | 4 +- .../src/account-api/api/apis/LicenseAPI.ts | 137 +++++++++++++----- qa-core/src/account-api/api/apis/StripeAPI.ts | 64 ++++++++ qa-core/src/account-api/api/apis/index.ts | 1 + .../tests/licensing/license.activate.spec.ts | 74 ++++++++++ .../tests/licensing/license.manage.spec.ts | 57 ++++++++ .../src/internal-api/api/apis/LicenseAPI.ts | 40 +++-- 7 files changed, 329 insertions(+), 48 deletions(-) create mode 100644 qa-core/src/account-api/api/apis/StripeAPI.ts create mode 100644 qa-core/src/account-api/tests/licensing/license.activate.spec.ts create mode 100644 qa-core/src/account-api/tests/licensing/license.manage.spec.ts diff --git a/qa-core/src/account-api/api/AccountInternalAPI.ts b/qa-core/src/account-api/api/AccountInternalAPI.ts index 3813ad2c9e..ef2c39d5a4 100644 --- a/qa-core/src/account-api/api/AccountInternalAPI.ts +++ b/qa-core/src/account-api/api/AccountInternalAPI.ts @@ -1,5 +1,5 @@ import AccountInternalAPIClient from "./AccountInternalAPIClient" -import { AccountAPI, LicenseAPI, AuthAPI } from "./apis" +import { AccountAPI, LicenseAPI, AuthAPI, StripeAPI } from "./apis" import { State } from "../../types" export default class AccountInternalAPI { @@ -8,11 +8,13 @@ export default class AccountInternalAPI { auth: AuthAPI accounts: AccountAPI licenses: LicenseAPI + stripe: StripeAPI constructor(state: State) { this.client = new AccountInternalAPIClient(state) this.auth = new AuthAPI(this.client) this.accounts = new AccountAPI(this.client) this.licenses = new LicenseAPI(this.client) + this.stripe = new StripeAPI((this.client)) } } diff --git a/qa-core/src/account-api/api/apis/LicenseAPI.ts b/qa-core/src/account-api/api/apis/LicenseAPI.ts index 44579f867b..9f06ec7198 100644 --- a/qa-core/src/account-api/api/apis/LicenseAPI.ts +++ b/qa-core/src/account-api/api/apis/LicenseAPI.ts @@ -2,25 +2,23 @@ import AccountInternalAPIClient from "../AccountInternalAPIClient" import { Account, CreateOfflineLicenseRequest, + GetLicenseKeyResponse, GetOfflineLicenseResponse, UpdateLicenseRequest, } from "@budibase/types" import { Response } from "node-fetch" import BaseAPI from "./BaseAPI" import { APIRequestOpts } from "../../../types" - export default class LicenseAPI extends BaseAPI { client: AccountInternalAPIClient - constructor(client: AccountInternalAPIClient) { super() this.client = client } - async updateLicense( - accountId: string, - body: UpdateLicenseRequest, - opts: APIRequestOpts = { status: 200 } + accountId: string, + body: UpdateLicenseRequest, + opts: APIRequestOpts = { status: 200 } ): Promise<[Response, Account]> { return this.doRequest(() => { return this.client.put(`/api/accounts/${accountId}/license`, { @@ -29,44 +27,111 @@ export default class LicenseAPI extends BaseAPI { }) }, opts) } - // TODO: Better approach for setting tenant id header - async createOfflineLicense( - accountId: string, - tenantId: string, - body: CreateOfflineLicenseRequest, - opts: { status?: number } = {} + accountId: string, + tenantId: string, + body: CreateOfflineLicenseRequest, + opts: { status?: number } = {} ): Promise { const [response, json] = await this.client.post( - `/api/internal/accounts/${accountId}/license/offline`, - { - body, - internal: true, - headers: { - "x-budibase-tenant-id": tenantId, + `/api/internal/accounts/${accountId}/license/offline`, + { + body, + internal: true, + headers: { + "x-budibase-tenant-id": tenantId, + }, + } + ) + expect(response.status).toBe(opts.status ? opts.status : 201) + return response + } + async getOfflineLicense( + accountId: string, + tenantId: string, + opts: { status?: number } = {} + ): Promise<[Response, GetOfflineLicenseResponse]> { + const [response, json] = await this.client.get( + `/api/internal/accounts/${accountId}/license/offline`, + { + internal: true, + headers: { + "x-budibase-tenant-id": tenantId, + }, + } + ) + expect(response.status).toBe(opts.status ? opts.status : 200) + return [response, json] + } + async getLicenseKey( + opts: { status?: number } = {} + ): Promise<[Response, GetLicenseKeyResponse]> { + const [response, json] = await this.client.get(`/api/license/key`) + expect(response.status).toBe(opts.status ? opts.status : 200) + return [response, json] + } + async activateLicense( + apiKey: string, + tenantId: string, + licenseKey: string, + opts: APIRequestOpts = { status: 200 } + ) { + return this.doRequest(() => { + return this.client.post(`/api/license/activate`, { + body: { + apiKey: apiKey, + tenantId: tenantId, + licenseKey: licenseKey, }, - } + }) + }, opts) + } + async regenerateLicenseKey(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.post(`/api/license/key/regenerate`, {}) + }, opts) + } + + async getPlans(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.get(`/api/plans`) + }, opts) + } + + async updatePlan(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.put(`/api/license/plan`) + }, opts) + } + + async refreshAccountLicense( + accountId: string, + opts: { status?: number } = {} + ): Promise { + const [response, json] = await this.client.post( + `/api/accounts/${accountId}/license/refresh`, + { + internal: true, + } ) expect(response.status).toBe(opts.status ? opts.status : 201) return response } - async getOfflineLicense( - accountId: string, - tenantId: string, - opts: { status?: number } = {} - ): Promise<[Response, GetOfflineLicenseResponse]> { - const [response, json] = await this.client.get( - `/api/internal/accounts/${accountId}/license/offline`, - { - internal: true, - headers: { - "x-budibase-tenant-id": tenantId, - }, - } - ) - expect(response.status).toBe(opts.status ? opts.status : 200) - return [response, json] + async getLicenseUsage(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.get(`/api/license/usage`) + }, opts) } -} + + async licenseUsageTriggered( + opts: { status?: number } = {} + ): Promise { + const [response, json] = await this.client.post( + `/api/license/usage/triggered` + ) + expect(response.status).toBe(opts.status ? opts.status : 201) + return response + } +} \ No newline at end of file diff --git a/qa-core/src/account-api/api/apis/StripeAPI.ts b/qa-core/src/account-api/api/apis/StripeAPI.ts new file mode 100644 index 0000000000..ffa96b3c2b --- /dev/null +++ b/qa-core/src/account-api/api/apis/StripeAPI.ts @@ -0,0 +1,64 @@ +import AccountInternalAPIClient from "../AccountInternalAPIClient" +import BaseAPI from "./BaseAPI" +import { APIRequestOpts } from "../../../types" + +export default class StripeAPI extends BaseAPI { + client: AccountInternalAPIClient + + constructor(client: AccountInternalAPIClient) { + super() + this.client = client + } + + async createCheckoutSession( + priceId: string, + opts: APIRequestOpts = { status: 200 } + ) { + return this.doRequest(() => { + return this.client.post(`/api/stripe/checkout-session`, { + body: { priceId }, + }) + }, opts) + } + + async checkoutSuccess(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.post(`/api/stripe/checkout-success`) + }, opts) + } + + async createPortalSession( + stripeCustomerId: string, + opts: APIRequestOpts = { status: 200 } + ) { + return this.doRequest(() => { + return this.client.post(`/api/stripe/portal-session`, { + body: { stripeCustomerId }, + }) + }, opts) + } + + async linkStripeCustomer(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.post(`/api/stripe/link`) + }, opts) + } + + async getInvoices(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.get(`/api/stripe/invoices`) + }, opts) + } + + async getUpcomingInvoice(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.get(`/api/stripe/upcoming-invoice`) + }, opts) + } + + async getStripeCustomers(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.get(`/api/stripe/customers`) + }, opts) + } +} diff --git a/qa-core/src/account-api/api/apis/index.ts b/qa-core/src/account-api/api/apis/index.ts index 1137ac3e36..5b0cf55110 100644 --- a/qa-core/src/account-api/api/apis/index.ts +++ b/qa-core/src/account-api/api/apis/index.ts @@ -1,3 +1,4 @@ export { default as AuthAPI } from "./AuthAPI" export { default as AccountAPI } from "./AccountAPI" export { default as LicenseAPI } from "./LicenseAPI" +export { default as StripeAPI } from "./StripeAPI" diff --git a/qa-core/src/account-api/tests/licensing/license.activate.spec.ts b/qa-core/src/account-api/tests/licensing/license.activate.spec.ts new file mode 100644 index 0000000000..709e2c33f0 --- /dev/null +++ b/qa-core/src/account-api/tests/licensing/license.activate.spec.ts @@ -0,0 +1,74 @@ +import TestConfiguration from "../../config/TestConfiguration" +import * as fixures from "../../fixtures" +import { Feature, Hosting } from "@budibase/types" + +describe("license activation", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + it("creates, activates and deletes online license - self host", async () => { + // Remove existing license key + await config.internalApi.license.deleteLicenseKey() + + // Verify license key not found + await config.internalApi.license.getLicenseKey({ status: 404 }) + + // Create self host account + const createAccountRequest = fixures.accounts.generateAccount({ + hosting: Hosting.SELF, + }) + const [createAccountRes, account] = + await config.accountsApi.accounts.create(createAccountRequest, { autoVerify: true }) + + let licenseKey: string = " " + await config.doInNewState(async () => { + await config.loginAsAccount(createAccountRequest) + // Retrieve license key + const [res, body] = + await config.accountsApi.licenses.getLicenseKey() + licenseKey = body.licenseKey + }) + + const accountId = account.accountId! + + // Update license to have paid feature + const [res, acc] = await config.accountsApi.licenses.updateLicense( + accountId, + { + overrides: { + features: [Feature.APP_BACKUPS], + }, + } + ) + + // Activate license key + await config.internalApi.license.activateLicenseKey({licenseKey}) + + // Verify license updated with new feature + await config.doInNewState(async () => { + await config.loginAsAccount(createAccountRequest) + const [selfRes, body] = await config.api.accounts.self() + expect(body.license.features[0]).toBe("appBackups") + }) + + // Remove license key + await config.internalApi.license.deleteLicenseKey() + + // Verify license key not found + await config.internalApi.license.getLicenseKey({ status: 404 }) + + // Verify user downgraded to free license + await config.doInNewState(async () => { + await config.loginAsAccount(createAccountRequest) + const [selfRes, body] = await config.api.accounts.self() + expect(body.license.plan.type).toBe("free") + }) + }) +}) diff --git a/qa-core/src/account-api/tests/licensing/license.manage.spec.ts b/qa-core/src/account-api/tests/licensing/license.manage.spec.ts new file mode 100644 index 0000000000..967252a0f9 --- /dev/null +++ b/qa-core/src/account-api/tests/licensing/license.manage.spec.ts @@ -0,0 +1,57 @@ +import TestConfiguration from "../../config/TestConfiguration" +import * as fixtures from "../../fixtures" +import { Hosting, PlanType } from "@budibase/types" + +describe("license management", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + it("retrieves plans, creates checkout session, and updates license", async () => { + // Create cloud account + const createAccountRequest = fixtures.accounts.generateAccount({ + hosting: Hosting.CLOUD, + }) + + // Self response has free license + const [selfRes, selfBody] = await config.api.accounts.self() + expect(selfBody.license.plan.type).toBe(PlanType.FREE) + + // Retrieve plans + const [plansRes, planBody] = await config.api.licenses.getPlans() + + // Select priceId from premium plan + let premiumPriceId = null + for (const plan of planBody) { + if (plan.type === PlanType.PREMIUM) { + premiumPriceId = plan.prices[0].priceId + break + } + } + + // Create checkout session for price + const checkoutSessionRes = await config.api.stripe.createCheckoutSession( + premiumPriceId + ) + const checkoutSessionUrl = checkoutSessionRes[1].url + expect(checkoutSessionUrl).toContain("checkout.stripe.com") + + // TODO: Mimic checkout success + // Create stripe customer + // Create subscription for premium plan + // Assert license updated from free to premium + + // Create portal session + //await config.api.stripe.createPortalSession() + + // Update from free to business license + + // License updated + }) +}) diff --git a/qa-core/src/internal-api/api/apis/LicenseAPI.ts b/qa-core/src/internal-api/api/apis/LicenseAPI.ts index 4c9d14c55e..268f8781c3 100644 --- a/qa-core/src/internal-api/api/apis/LicenseAPI.ts +++ b/qa-core/src/internal-api/api/apis/LicenseAPI.ts @@ -1,45 +1,63 @@ import { Response } from "node-fetch" import { + ActivateLicenseKeyRequest, ActivateOfflineLicenseTokenRequest, + GetLicenseKeyResponse, GetOfflineIdentifierResponse, GetOfflineLicenseTokenResponse, } from "@budibase/types" import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient" import BaseAPI from "./BaseAPI" +import { APIRequestOpts } from "../../../types" export default class LicenseAPI extends BaseAPI { constructor(client: BudibaseInternalAPIClient) { super(client) } - async getOfflineLicenseToken( - opts: { status?: number } = {} + opts: { status?: number } = {} ): Promise<[Response, GetOfflineLicenseTokenResponse]> { const [response, body] = await this.get( - `/global/license/offline`, - opts.status + `/global/license/offline`, + opts.status ) return [response, body] } - async deleteOfflineLicenseToken(): Promise<[Response]> { const [response] = await this.del(`/global/license/offline`, 204) return [response] } - async activateOfflineLicenseToken( - body: ActivateOfflineLicenseTokenRequest + body: ActivateOfflineLicenseTokenRequest ): Promise<[Response]> { const [response] = await this.post(`/global/license/offline`, body) return [response] } - async getOfflineIdentifier(): Promise< - [Response, GetOfflineIdentifierResponse] + [Response, GetOfflineIdentifierResponse] > { const [response, body] = await this.get( - `/global/license/offline/identifier` + `/global/license/offline/identifier` ) return [response, body] } -} + + async getLicenseKey( + opts: { status?: number } = {} + ): Promise<[Response, GetLicenseKeyResponse]> { + const [response, body] = await this.get(`/global/license/key`, opts.status) + return [response, body] + } + + async activateLicenseKey( + body: ActivateLicenseKeyRequest + ): Promise<[Response]> { + const [response] = await this.post(`/global/license/key`, body) + return [response] + } + + async deleteLicenseKey(): Promise<[Response]> { + const [response] = await this.del(`/global/license/key`, 204) + return [response] + } +} \ No newline at end of file From 5e16d0451936ed2b1c2c0c6d1928c146b45c65c1 Mon Sep 17 00:00:00 2001 From: Mitch-Budibase Date: Thu, 5 Oct 2023 17:43:25 +0100 Subject: [PATCH 08/95] lint --- .../src/account-api/api/AccountInternalAPI.ts | 2 +- .../src/account-api/api/apis/LicenseAPI.ts | 78 +++++------ qa-core/src/account-api/api/apis/StripeAPI.ts | 100 +++++++-------- .../tests/licensing/license.activate.spec.ts | 121 +++++++++--------- .../tests/licensing/license.manage.spec.ts | 82 ++++++------ .../src/internal-api/api/apis/LicenseAPI.ts | 18 +-- 6 files changed, 201 insertions(+), 200 deletions(-) diff --git a/qa-core/src/account-api/api/AccountInternalAPI.ts b/qa-core/src/account-api/api/AccountInternalAPI.ts index ef2c39d5a4..f89bf556f2 100644 --- a/qa-core/src/account-api/api/AccountInternalAPI.ts +++ b/qa-core/src/account-api/api/AccountInternalAPI.ts @@ -15,6 +15,6 @@ export default class AccountInternalAPI { this.auth = new AuthAPI(this.client) this.accounts = new AccountAPI(this.client) this.licenses = new LicenseAPI(this.client) - this.stripe = new StripeAPI((this.client)) + this.stripe = new StripeAPI(this.client) } } diff --git a/qa-core/src/account-api/api/apis/LicenseAPI.ts b/qa-core/src/account-api/api/apis/LicenseAPI.ts index 9f06ec7198..dba1a661d4 100644 --- a/qa-core/src/account-api/api/apis/LicenseAPI.ts +++ b/qa-core/src/account-api/api/apis/LicenseAPI.ts @@ -16,9 +16,9 @@ export default class LicenseAPI extends BaseAPI { this.client = client } async updateLicense( - accountId: string, - body: UpdateLicenseRequest, - opts: APIRequestOpts = { status: 200 } + accountId: string, + body: UpdateLicenseRequest, + opts: APIRequestOpts = { status: 200 } ): Promise<[Response, Account]> { return this.doRequest(() => { return this.client.put(`/api/accounts/${accountId}/license`, { @@ -29,53 +29,53 @@ export default class LicenseAPI extends BaseAPI { } // TODO: Better approach for setting tenant id header async createOfflineLicense( - accountId: string, - tenantId: string, - body: CreateOfflineLicenseRequest, - opts: { status?: number } = {} + accountId: string, + tenantId: string, + body: CreateOfflineLicenseRequest, + opts: { status?: number } = {} ): Promise { const [response, json] = await this.client.post( - `/api/internal/accounts/${accountId}/license/offline`, - { - body, - internal: true, - headers: { - "x-budibase-tenant-id": tenantId, - }, - } + `/api/internal/accounts/${accountId}/license/offline`, + { + body, + internal: true, + headers: { + "x-budibase-tenant-id": tenantId, + }, + } ) expect(response.status).toBe(opts.status ? opts.status : 201) return response } async getOfflineLicense( - accountId: string, - tenantId: string, - opts: { status?: number } = {} + accountId: string, + tenantId: string, + opts: { status?: number } = {} ): Promise<[Response, GetOfflineLicenseResponse]> { const [response, json] = await this.client.get( - `/api/internal/accounts/${accountId}/license/offline`, - { - internal: true, - headers: { - "x-budibase-tenant-id": tenantId, - }, - } + `/api/internal/accounts/${accountId}/license/offline`, + { + internal: true, + headers: { + "x-budibase-tenant-id": tenantId, + }, + } ) expect(response.status).toBe(opts.status ? opts.status : 200) return [response, json] } async getLicenseKey( - opts: { status?: number } = {} + opts: { status?: number } = {} ): Promise<[Response, GetLicenseKeyResponse]> { const [response, json] = await this.client.get(`/api/license/key`) expect(response.status).toBe(opts.status ? opts.status : 200) return [response, json] } async activateLicense( - apiKey: string, - tenantId: string, - licenseKey: string, - opts: APIRequestOpts = { status: 200 } + apiKey: string, + tenantId: string, + licenseKey: string, + opts: APIRequestOpts = { status: 200 } ) { return this.doRequest(() => { return this.client.post(`/api/license/activate`, { @@ -106,14 +106,14 @@ export default class LicenseAPI extends BaseAPI { } async refreshAccountLicense( - accountId: string, - opts: { status?: number } = {} + accountId: string, + opts: { status?: number } = {} ): Promise { const [response, json] = await this.client.post( - `/api/accounts/${accountId}/license/refresh`, - { - internal: true, - } + `/api/accounts/${accountId}/license/refresh`, + { + internal: true, + } ) expect(response.status).toBe(opts.status ? opts.status : 201) return response @@ -126,12 +126,12 @@ export default class LicenseAPI extends BaseAPI { } async licenseUsageTriggered( - opts: { status?: number } = {} + opts: { status?: number } = {} ): Promise { const [response, json] = await this.client.post( - `/api/license/usage/triggered` + `/api/license/usage/triggered` ) expect(response.status).toBe(opts.status ? opts.status : 201) return response } -} \ No newline at end of file +} diff --git a/qa-core/src/account-api/api/apis/StripeAPI.ts b/qa-core/src/account-api/api/apis/StripeAPI.ts index ffa96b3c2b..c9c776e89b 100644 --- a/qa-core/src/account-api/api/apis/StripeAPI.ts +++ b/qa-core/src/account-api/api/apis/StripeAPI.ts @@ -3,62 +3,62 @@ import BaseAPI from "./BaseAPI" import { APIRequestOpts } from "../../../types" export default class StripeAPI extends BaseAPI { - client: AccountInternalAPIClient + client: AccountInternalAPIClient - constructor(client: AccountInternalAPIClient) { - super() - this.client = client - } + constructor(client: AccountInternalAPIClient) { + super() + this.client = client + } - async createCheckoutSession( - priceId: string, - opts: APIRequestOpts = { status: 200 } - ) { - return this.doRequest(() => { - return this.client.post(`/api/stripe/checkout-session`, { - body: { priceId }, - }) - }, opts) - } + async createCheckoutSession( + priceId: string, + opts: APIRequestOpts = { status: 200 } + ) { + return this.doRequest(() => { + return this.client.post(`/api/stripe/checkout-session`, { + body: { priceId }, + }) + }, opts) + } - async checkoutSuccess(opts: APIRequestOpts = { status: 200 }) { - return this.doRequest(() => { - return this.client.post(`/api/stripe/checkout-success`) - }, opts) - } + async checkoutSuccess(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.post(`/api/stripe/checkout-success`) + }, opts) + } - async createPortalSession( - stripeCustomerId: string, - opts: APIRequestOpts = { status: 200 } - ) { - return this.doRequest(() => { - return this.client.post(`/api/stripe/portal-session`, { - body: { stripeCustomerId }, - }) - }, opts) - } + async createPortalSession( + stripeCustomerId: string, + opts: APIRequestOpts = { status: 200 } + ) { + return this.doRequest(() => { + return this.client.post(`/api/stripe/portal-session`, { + body: { stripeCustomerId }, + }) + }, opts) + } - async linkStripeCustomer(opts: APIRequestOpts = { status: 200 }) { - return this.doRequest(() => { - return this.client.post(`/api/stripe/link`) - }, opts) - } + async linkStripeCustomer(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.post(`/api/stripe/link`) + }, opts) + } - async getInvoices(opts: APIRequestOpts = { status: 200 }) { - return this.doRequest(() => { - return this.client.get(`/api/stripe/invoices`) - }, opts) - } + async getInvoices(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.get(`/api/stripe/invoices`) + }, opts) + } - async getUpcomingInvoice(opts: APIRequestOpts = { status: 200 }) { - return this.doRequest(() => { - return this.client.get(`/api/stripe/upcoming-invoice`) - }, opts) - } + async getUpcomingInvoice(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.get(`/api/stripe/upcoming-invoice`) + }, opts) + } - async getStripeCustomers(opts: APIRequestOpts = { status: 200 }) { - return this.doRequest(() => { - return this.client.get(`/api/stripe/customers`) - }, opts) - } + async getStripeCustomers(opts: APIRequestOpts = { status: 200 }) { + return this.doRequest(() => { + return this.client.get(`/api/stripe/customers`) + }, opts) + } } diff --git a/qa-core/src/account-api/tests/licensing/license.activate.spec.ts b/qa-core/src/account-api/tests/licensing/license.activate.spec.ts index 709e2c33f0..a494ceb354 100644 --- a/qa-core/src/account-api/tests/licensing/license.activate.spec.ts +++ b/qa-core/src/account-api/tests/licensing/license.activate.spec.ts @@ -3,72 +3,73 @@ import * as fixures from "../../fixtures" import { Feature, Hosting } from "@budibase/types" describe("license activation", () => { - const config = new TestConfiguration() + const config = new TestConfiguration() - beforeAll(async () => { - await config.beforeAll() + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + it("creates, activates and deletes online license - self host", async () => { + // Remove existing license key + await config.internalApi.license.deleteLicenseKey() + + // Verify license key not found + await config.internalApi.license.getLicenseKey({ status: 404 }) + + // Create self host account + const createAccountRequest = fixures.accounts.generateAccount({ + hosting: Hosting.SELF, + }) + const [createAccountRes, account] = + await config.accountsApi.accounts.create(createAccountRequest, { + autoVerify: true, + }) + + let licenseKey: string = " " + await config.doInNewState(async () => { + await config.loginAsAccount(createAccountRequest) + // Retrieve license key + const [res, body] = await config.accountsApi.licenses.getLicenseKey() + licenseKey = body.licenseKey }) - afterAll(async () => { - await config.afterAll() + const accountId = account.accountId! + + // Update license to have paid feature + const [res, acc] = await config.accountsApi.licenses.updateLicense( + accountId, + { + overrides: { + features: [Feature.APP_BACKUPS], + }, + } + ) + + // Activate license key + await config.internalApi.license.activateLicenseKey({ licenseKey }) + + // Verify license updated with new feature + await config.doInNewState(async () => { + await config.loginAsAccount(createAccountRequest) + const [selfRes, body] = await config.api.accounts.self() + expect(body.license.features[0]).toBe("appBackups") }) - it("creates, activates and deletes online license - self host", async () => { - // Remove existing license key - await config.internalApi.license.deleteLicenseKey() + // Remove license key + await config.internalApi.license.deleteLicenseKey() - // Verify license key not found - await config.internalApi.license.getLicenseKey({ status: 404 }) + // Verify license key not found + await config.internalApi.license.getLicenseKey({ status: 404 }) - // Create self host account - const createAccountRequest = fixures.accounts.generateAccount({ - hosting: Hosting.SELF, - }) - const [createAccountRes, account] = - await config.accountsApi.accounts.create(createAccountRequest, { autoVerify: true }) - - let licenseKey: string = " " - await config.doInNewState(async () => { - await config.loginAsAccount(createAccountRequest) - // Retrieve license key - const [res, body] = - await config.accountsApi.licenses.getLicenseKey() - licenseKey = body.licenseKey - }) - - const accountId = account.accountId! - - // Update license to have paid feature - const [res, acc] = await config.accountsApi.licenses.updateLicense( - accountId, - { - overrides: { - features: [Feature.APP_BACKUPS], - }, - } - ) - - // Activate license key - await config.internalApi.license.activateLicenseKey({licenseKey}) - - // Verify license updated with new feature - await config.doInNewState(async () => { - await config.loginAsAccount(createAccountRequest) - const [selfRes, body] = await config.api.accounts.self() - expect(body.license.features[0]).toBe("appBackups") - }) - - // Remove license key - await config.internalApi.license.deleteLicenseKey() - - // Verify license key not found - await config.internalApi.license.getLicenseKey({ status: 404 }) - - // Verify user downgraded to free license - await config.doInNewState(async () => { - await config.loginAsAccount(createAccountRequest) - const [selfRes, body] = await config.api.accounts.self() - expect(body.license.plan.type).toBe("free") - }) + // Verify user downgraded to free license + await config.doInNewState(async () => { + await config.loginAsAccount(createAccountRequest) + const [selfRes, body] = await config.api.accounts.self() + expect(body.license.plan.type).toBe("free") }) + }) }) diff --git a/qa-core/src/account-api/tests/licensing/license.manage.spec.ts b/qa-core/src/account-api/tests/licensing/license.manage.spec.ts index 967252a0f9..3f87838ee4 100644 --- a/qa-core/src/account-api/tests/licensing/license.manage.spec.ts +++ b/qa-core/src/account-api/tests/licensing/license.manage.spec.ts @@ -3,55 +3,55 @@ import * as fixtures from "../../fixtures" import { Hosting, PlanType } from "@budibase/types" describe("license management", () => { - const config = new TestConfiguration() + const config = new TestConfiguration() - beforeAll(async () => { - await config.beforeAll() + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + it("retrieves plans, creates checkout session, and updates license", async () => { + // Create cloud account + const createAccountRequest = fixtures.accounts.generateAccount({ + hosting: Hosting.CLOUD, }) - afterAll(async () => { - await config.afterAll() - }) + // Self response has free license + const [selfRes, selfBody] = await config.api.accounts.self() + expect(selfBody.license.plan.type).toBe(PlanType.FREE) - it("retrieves plans, creates checkout session, and updates license", async () => { - // Create cloud account - const createAccountRequest = fixtures.accounts.generateAccount({ - hosting: Hosting.CLOUD, - }) + // Retrieve plans + const [plansRes, planBody] = await config.api.licenses.getPlans() - // Self response has free license - const [selfRes, selfBody] = await config.api.accounts.self() - expect(selfBody.license.plan.type).toBe(PlanType.FREE) + // Select priceId from premium plan + let premiumPriceId = null + for (const plan of planBody) { + if (plan.type === PlanType.PREMIUM) { + premiumPriceId = plan.prices[0].priceId + break + } + } - // Retrieve plans - const [plansRes, planBody] = await config.api.licenses.getPlans() + // Create checkout session for price + const checkoutSessionRes = await config.api.stripe.createCheckoutSession( + premiumPriceId + ) + const checkoutSessionUrl = checkoutSessionRes[1].url + expect(checkoutSessionUrl).toContain("checkout.stripe.com") - // Select priceId from premium plan - let premiumPriceId = null - for (const plan of planBody) { - if (plan.type === PlanType.PREMIUM) { - premiumPriceId = plan.prices[0].priceId - break - } - } + // TODO: Mimic checkout success + // Create stripe customer + // Create subscription for premium plan + // Assert license updated from free to premium - // Create checkout session for price - const checkoutSessionRes = await config.api.stripe.createCheckoutSession( - premiumPriceId - ) - const checkoutSessionUrl = checkoutSessionRes[1].url - expect(checkoutSessionUrl).toContain("checkout.stripe.com") + // Create portal session + //await config.api.stripe.createPortalSession() - // TODO: Mimic checkout success - // Create stripe customer - // Create subscription for premium plan - // Assert license updated from free to premium + // Update from free to business license - // Create portal session - //await config.api.stripe.createPortalSession() - - // Update from free to business license - - // License updated - }) + // License updated + }) }) diff --git a/qa-core/src/internal-api/api/apis/LicenseAPI.ts b/qa-core/src/internal-api/api/apis/LicenseAPI.ts index 268f8781c3..ef322e069a 100644 --- a/qa-core/src/internal-api/api/apis/LicenseAPI.ts +++ b/qa-core/src/internal-api/api/apis/LicenseAPI.ts @@ -15,11 +15,11 @@ export default class LicenseAPI extends BaseAPI { super(client) } async getOfflineLicenseToken( - opts: { status?: number } = {} + opts: { status?: number } = {} ): Promise<[Response, GetOfflineLicenseTokenResponse]> { const [response, body] = await this.get( - `/global/license/offline`, - opts.status + `/global/license/offline`, + opts.status ) return [response, body] } @@ -28,29 +28,29 @@ export default class LicenseAPI extends BaseAPI { return [response] } async activateOfflineLicenseToken( - body: ActivateOfflineLicenseTokenRequest + body: ActivateOfflineLicenseTokenRequest ): Promise<[Response]> { const [response] = await this.post(`/global/license/offline`, body) return [response] } async getOfflineIdentifier(): Promise< - [Response, GetOfflineIdentifierResponse] + [Response, GetOfflineIdentifierResponse] > { const [response, body] = await this.get( - `/global/license/offline/identifier` + `/global/license/offline/identifier` ) return [response, body] } async getLicenseKey( - opts: { status?: number } = {} + opts: { status?: number } = {} ): Promise<[Response, GetLicenseKeyResponse]> { const [response, body] = await this.get(`/global/license/key`, opts.status) return [response, body] } async activateLicenseKey( - body: ActivateLicenseKeyRequest + body: ActivateLicenseKeyRequest ): Promise<[Response]> { const [response] = await this.post(`/global/license/key`, body) return [response] @@ -60,4 +60,4 @@ export default class LicenseAPI extends BaseAPI { const [response] = await this.del(`/global/license/key`, 204) return [response] } -} \ No newline at end of file +} From 9667c954ef79288a42838c0a72c8bea6490d9a63 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 5 Oct 2023 17:55:57 +0100 Subject: [PATCH 09/95] Enable uploading a CSV file as a datasource and fix edge cases --- .../settings/controls/DataSourceSelect.svelte | 44 ++++++++++++++++--- .../src/components/grid/stores/datasource.js | 6 +++ .../grid/stores/datasources/nonPlus.js | 9 ++++ .../grid/stores/datasources/table.js | 8 ++++ .../grid/stores/datasources/viewV2.js | 8 ++++ .../src/components/grid/stores/rows.js | 7 ++- .../frontend-core/src/fetch/CustomFetch.js | 6 +-- 7 files changed, 77 insertions(+), 11 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/DataSourceSelect.svelte b/packages/builder/src/components/design/settings/controls/DataSourceSelect.svelte index e4b3f972ba..de9a37dfae 100644 --- a/packages/builder/src/components/design/settings/controls/DataSourceSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/DataSourceSelect.svelte @@ -10,6 +10,10 @@ Drawer, DrawerContent, Icon, + Modal, + ModalContent, + CoreDropzone, + notifications, } from "@budibase/bbui" import { createEventDispatcher } from "svelte" import { store, currentAsset } from "builderStore" @@ -24,6 +28,7 @@ import IntegrationQueryEditor from "components/integration/index.svelte" import { makePropSafe as safe } from "@budibase/string-templates" import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" + import { API } from "api" export let value = {} export let otherSources @@ -39,6 +44,7 @@ let tmpQueryParams let tmpCustomData let customDataValid = true + let modal $: text = value?.label ?? "Choose an option" $: tables = $tablesStore.list.map(m => ({ @@ -184,6 +190,26 @@ }) drawer.hide() } + + const promptForCSV = () => { + drawer.hide() + modal.show() + } + + const handleCSV = async e => { + try { + const csv = await e.detail[0]?.text() + if (csv?.length) { + const js = await API.csvToJson(csv) + tmpCustomData = JSON.stringify(js) + } + } catch (error) { + console.log(error) + notifications.error("Failed to parse CSV") + } + modal.hide() + drawer.show() + }
@@ -227,12 +253,12 @@
- +
+ + +
Provide a JavaScript or JSON array to use as data
@@ -349,6 +375,12 @@
+ + + + + +