From 95faeea28638f234342c42ea87c39512c8509bf5 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 18 Jul 2023 18:32:31 +0200 Subject: [PATCH 001/245] datasourceType setup --- .../builder/src/components/backend/DataTable/DataTable.svelte | 1 + packages/client/src/components/app/GridBlock.svelte | 1 + packages/frontend-core/src/components/grid/layout/Grid.svelte | 2 ++ packages/frontend-core/src/components/grid/stores/rows.js | 3 ++- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index 33db9b60e3..3fcaefc5bd 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -44,6 +44,7 @@ { loading, sort, tableId, + datasourceType, API, scroll, validation, @@ -111,7 +112,7 @@ export const deriveStores = context => { const newFetch = fetchData({ API, datasource: { - type: "table", + type: datasourceType, tableId: $tableId, }, options: { From 9ccc54773dbdea33264677caf24d7a4f5c383715 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 18 Jul 2023 18:32:52 +0200 Subject: [PATCH 002/245] ViewV2 page --- .../TableNavigator/TableNavigator.svelte | 10 +++++++-- .../data/view/v2/[viewName].svelte | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewName].svelte diff --git a/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte b/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte index d9def682dc..ef8872c1ca 100644 --- a/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte +++ b/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte @@ -37,13 +37,19 @@ {/if} - {#each [...Object.keys(table.views || {})].sort() as viewName, idx (idx)} + {#each [...Object.entries(table.views || {})].sort() as [viewName, view], idx (idx)} $goto(`./view/${encodeURIComponent(viewName)}`)} + on:click={() => { + if (view.version === 2) { + $goto(`./view/v2/${encodeURIComponent(viewName)}`) + } else { + $goto(`./view/${encodeURIComponent(viewName)}`) + } + }} selectedBy={$userSelectedResourceMap[viewName]} > + import { views } from "stores/backend" + import { Grid } from "@budibase/frontend-core" + import { API } from "api" + + $: tableId = $views.selected?.id + + +
+ +
+ + From 82ea9a7cc1c35ed1a4db616b3b5924c425b74eed Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 19 Jul 2023 10:14:42 +0200 Subject: [PATCH 003/245] Setup datasource type --- .../data/view/v2/{[viewName].svelte => [id].svelte} | 10 ++++++++-- .../frontend-core/src/components/grid/stores/rows.js | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) rename packages/builder/src/pages/builder/app/[application]/data/view/v2/{[viewName].svelte => [id].svelte} (73%) diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewName].svelte b/packages/builder/src/pages/builder/app/[application]/data/view/v2/[id].svelte similarity index 73% rename from packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewName].svelte rename to packages/builder/src/pages/builder/app/[application]/data/view/v2/[id].svelte index 3df89bd777..266fd237f5 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewName].svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/view/v2/[id].svelte @@ -3,11 +3,17 @@ import { Grid } from "@budibase/frontend-core" import { API } from "api" - $: tableId = $views.selected?.id + export let id
- +
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte index f7b6f61a10..8f5a35ea1f 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte @@ -1,7 +1,14 @@ diff --git a/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte b/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte index e641bd99a5..caa9de222a 100644 --- a/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte +++ b/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte @@ -1,5 +1,5 @@ -
- -
- - + diff --git a/packages/builder/src/stores/backend/index.js b/packages/builder/src/stores/backend/index.js index 6fbc9f82c7..278e43c1ed 100644 --- a/packages/builder/src/stores/backend/index.js +++ b/packages/builder/src/stores/backend/index.js @@ -1,6 +1,7 @@ export { database } from "./database" export { tables } from "./tables" export { views } from "./views" +export { viewsV2 } from "./viewsV2" export { permissions } from "./permissions" export { roles } from "./roles" export { datasources, ImportTableError } from "./datasources" diff --git a/packages/builder/src/stores/backend/views.js b/packages/builder/src/stores/backend/views.js index 8df337a299..603b0830fc 100644 --- a/packages/builder/src/stores/backend/views.js +++ b/packages/builder/src/stores/backend/views.js @@ -9,7 +9,10 @@ export function createViewsStore() { const derivedStore = derived([store, tables], ([$store, $tables]) => { let list = [] $tables.list?.forEach(table => { - list = list.concat(Object.values(table?.views || {})) + const views = Object.values(table?.views || {}).filter(view => { + return view.version !== 2 + }) + list = list.concat(views) }) return { ...$store, @@ -26,11 +29,7 @@ export function createViewsStore() { } const deleteView = async view => { - if (view.version === 2) { - await API.viewV2.delete(view.id) - } else { - await API.deleteView(view.name) - } + await API.deleteView(view.name) // Update tables tables.update(state => { @@ -40,20 +39,6 @@ export function createViewsStore() { }) } - const create = async view => { - const savedViewResponse = await API.viewV2.create(view) - const savedView = savedViewResponse.data - - // Update tables - tables.update(state => { - const table = state.list.find(table => table._id === view.tableId) - table.views[view.name] = savedView - return { ...state } - }) - - return savedView - } - const save = async view => { const savedView = await API.saveView(view) @@ -74,7 +59,6 @@ export function createViewsStore() { subscribe: derivedStore.subscribe, select, delete: deleteView, - create, save, } } diff --git a/packages/builder/src/stores/backend/viewsV2.js b/packages/builder/src/stores/backend/viewsV2.js new file mode 100644 index 0000000000..8b7b1d876c --- /dev/null +++ b/packages/builder/src/stores/backend/viewsV2.js @@ -0,0 +1,85 @@ +import { writable, derived } from "svelte/store" +import { tables } from "./" +import { API } from "api" + +export function createViewsV2Store() { + const store = writable({ + selectedViewId: null, + }) + const derivedStore = derived([store, tables], ([$store, $tables]) => { + let list = [] + $tables.list?.forEach(table => { + const views = Object.values(table?.views || {}).filter(view => { + return view.version === 2 + }) + list = list.concat(views) + }) + return { + ...$store, + list, + selected: list.find(view => view.id === $store.selectedViewId), + } + }) + + const select = id => { + store.update(state => ({ + ...state, + selectedViewId: id, + })) + } + + const deleteView = async view => { + await API.viewV2.delete(view.id) + + // Update tables + tables.update(state => { + const table = state.list.find(table => table._id === view.tableId) + delete table.views[view.name] + return { ...state } + }) + } + + const create = async view => { + const savedViewResponse = await API.viewV2.create(view) + const savedView = savedViewResponse.data + + // Update tables + tables.update(state => { + const table = state.list.find(table => table._id === view.tableId) + table.views[view.name] = savedView + return { ...state } + }) + + return savedView + } + + const save = async view => { + // No dedicated save endpoint at this time + // const savedView = await API.saveView(view) + // + // // Update tables + // tables.update(state => { + // const table = state.list.find(table => table._id === view.tableId) + // if (table) { + // if (view.originalName) { + // delete table.views[view.originalName] + // } + // table.views[view.name] = savedView + // } + // return { ...state } + // }) + } + + const replace = (id, view) => {} + + return { + subscribe: derivedStore.subscribe, + select, + delete: deleteView, + create, + save, + replace, + } +} + +export const viewsV2 = createViewsV2Store() diff --git a/packages/frontend-core/src/components/grid/layout/Grid.svelte b/packages/frontend-core/src/components/grid/layout/Grid.svelte index d92851719f..556e6ca16e 100644 --- a/packages/frontend-core/src/components/grid/layout/Grid.svelte +++ b/packages/frontend-core/src/components/grid/layout/Grid.svelte @@ -28,8 +28,7 @@ } from "../lib/constants" export let API = null - export let tableId = null - export let datasourceType = null + export let datasource = null export let schemaOverrides = null export let columnWhitelist = null export let allowAddRows = true @@ -77,8 +76,7 @@ // Keep config store up to date with props $: config.set({ - tableId, - datasourceType, + datasource, schemaOverrides, columnWhitelist, allowAddRows, diff --git a/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte b/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte index f39e679da4..d964931682 100644 --- a/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte +++ b/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte @@ -12,7 +12,7 @@ width, config, hasNonAutoColumn, - tableId, + datasource, loading, } = getContext("grid") @@ -33,7 +33,7 @@ {#if $config.allowSchemaChanges} - {#key $tableId} + {#key $datasource} { - const { rows, tableId, users, focusedCellId, table, API } = context + const { rows, datasource, users, focusedCellId, table, API } = context const socket = createWebsocket("/socket/grid") - const connectToTable = tableId => { + const connectToDatasource = datasource => { if (!socket.connected) { return } // Identify which table we are editing const appId = API.getAppID() socket.emit( - GridSocketEvent.SelectTable, - { tableId, appId }, + GridSocketEvent.SelectDatasource, + { datasource, appId }, ({ users: gridUsers }) => { users.set(gridUsers) } @@ -23,7 +23,7 @@ export const createGridWebsocket = context => { // Built-in events socket.on("connect", () => { - connectToTable(get(tableId)) + connectToDatasource(get(datasource)) }) socket.on("connect_error", err => { console.log("Failed to connect to grid websocket:", err.message) @@ -57,7 +57,7 @@ export const createGridWebsocket = context => { }) // Change websocket connection when table changes - tableId.subscribe(connectToTable) + datasource.subscribe(connectToDatasource) // Notify selected cell changes focusedCellId.subscribe($focusedCellId => { diff --git a/packages/frontend-core/src/components/grid/stores/config.js b/packages/frontend-core/src/components/grid/stores/config.js index fc9decc1ef..8aa6b818e2 100644 --- a/packages/frontend-core/src/components/grid/stores/config.js +++ b/packages/frontend-core/src/components/grid/stores/config.js @@ -6,7 +6,7 @@ export const createStores = context => { const getProp = prop => derivedMemo(config, $config => $config[prop]) // Derive and memoize some props so that we can react to them in isolation - const tableId = getProp("tableId") + const datasource = getProp("datasource") const initialSortColumn = getProp("initialSortColumn") const initialSortOrder = getProp("initialSortOrder") const initialFilter = getProp("initialFilter") @@ -18,7 +18,7 @@ export const createStores = context => { return { config, - tableId, + datasource, initialSortColumn, initialSortOrder, initialFilter, diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index dccbc774a4..1346d7c8b2 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -59,7 +59,7 @@ export const deriveStores = context => { filter, loading, sort, - tableId, + datasource, API, scroll, validation, @@ -71,7 +71,7 @@ export const deriveStores = context => { hasNextPage, error, notifications, - props, + config, } = context const instanceLoaded = writable(false) const fetch = writable(null) @@ -95,7 +95,7 @@ export const deriveStores = context => { // Reset everything when table ID changes let unsubscribe = null let lastResetKey = null - tableId.subscribe(async $tableId => { + datasource.subscribe(async $datasource => { // Unsub from previous fetch if one exists unsubscribe?.() fetch.set(null) @@ -108,21 +108,6 @@ export const deriveStores = context => { const $filter = get(filter) const $sort = get(sort) - let datasource - if (props.datasourceType === "viewV2") { - const tableId = $tableId - datasource = { - type: props.datasourceType, - id: $tableId, - tableId: tableId.split("_").slice(0, -1).join("_"), - } - } else { - datasource = { - type: props.datasourceType, - tableId: $tableId, - } - } - // Create new fetch model const newFetch = fetchData({ API, From 352b7ebe1ccb2ac84b95294309ed4ea28d8a9354 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 26 Jul 2023 14:07:07 +0100 Subject: [PATCH 022/245] Add dedicated route for routes v1, improve view creation modal, fix selection state --- .../DataTable/modals/CreateViewModal.svelte | 19 ++++++----- .../TableNavigator/TableNavigator.svelte | 32 ++++++++++--------- .../view/{ => v1}/[viewName]/_layout.svelte | 2 +- .../view/{ => v1}/[viewName]/index.svelte | 0 .../[application]/data/view/v1/index.svelte | 5 +++ .../view/v2/{[id] => [viewId]}/_layout.svelte | 2 +- .../view/v2/{[id] => [viewId]}/index.svelte | 0 .../[application]/data/view/v2/index.svelte | 5 +++ .../src/components/grid/stores/rows.js | 2 +- 9 files changed, 41 insertions(+), 26 deletions(-) rename packages/builder/src/pages/builder/app/[application]/data/view/{ => v1}/[viewName]/_layout.svelte (95%) rename packages/builder/src/pages/builder/app/[application]/data/view/{ => v1}/[viewName]/index.svelte (100%) create mode 100644 packages/builder/src/pages/builder/app/[application]/data/view/v1/index.svelte rename packages/builder/src/pages/builder/app/[application]/data/view/v2/{[id] => [viewId]}/_layout.svelte (96%) rename packages/builder/src/pages/builder/app/[application]/data/view/v2/{[id] => [viewId]}/index.svelte (100%) create mode 100644 packages/builder/src/pages/builder/app/[application]/data/view/v2/index.svelte diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte index 6a7f5b96a1..ac5e522923 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte @@ -1,22 +1,19 @@ {#if $database?._id} @@ -37,28 +45,22 @@ {/if}
- {#each [...Object.entries(table.views || {})].sort() as [viewName, view], idx (idx)} - {@const viewSelected = - $isActive("./view") && $views.selected?.name === viewName} - {@const viewV2Selected = - $isActive("./view/v2") && $viewsV2.selected?.name === viewName} + {#each [...Object.entries(table.views || {})].sort() as [name, view], idx (idx)} { if (view.version === 2) { $goto(`./view/v2/${view.id}`) } else { - $goto(`./view/${encodeURIComponent(viewName)}`) + $goto(`./view/v1/${encodeURIComponent(name)}`) } }} - selectedBy={$userSelectedResourceMap[viewName]} + selectedBy={$userSelectedResourceMap[name]} > - + {/each} {/each} diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/[viewName]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/_layout.svelte similarity index 95% rename from packages/builder/src/pages/builder/app/[application]/data/view/[viewName]/_layout.svelte rename to packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/_layout.svelte index f3793317e8..7f4fc9c597 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/view/[viewName]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/_layout.svelte @@ -13,7 +13,7 @@ stateKey: "selectedViewName", validate: name => $views.list?.some(view => view.name === name), update: views.select, - fallbackUrl: "../", + fallbackUrl: "../../", store: views, routify, decode: decodeURIComponent, diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/[viewName]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/index.svelte similarity index 100% rename from packages/builder/src/pages/builder/app/[application]/data/view/[viewName]/index.svelte rename to packages/builder/src/pages/builder/app/[application]/data/view/v1/[viewName]/index.svelte diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v1/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/v1/index.svelte new file mode 100644 index 0000000000..c11ca87023 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/data/view/v1/index.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v2/[id]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/_layout.svelte similarity index 96% rename from packages/builder/src/pages/builder/app/[application]/data/view/v2/[id]/_layout.svelte rename to packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/_layout.svelte index 62844a54df..8ddd6adbd0 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/view/v2/[id]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/_layout.svelte @@ -9,7 +9,7 @@ $: store.actions.websocket.selectResource(id) const stopSyncing = syncURLToState({ - urlParam: "id", + urlParam: "viewId", stateKey: "selectedViewId", validate: id => $viewsV2.list?.some(view => view.id === id), update: viewsV2.select, diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v2/[id]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/index.svelte similarity index 100% rename from packages/builder/src/pages/builder/app/[application]/data/view/v2/[id]/index.svelte rename to packages/builder/src/pages/builder/app/[application]/data/view/v2/[viewId]/index.svelte diff --git a/packages/builder/src/pages/builder/app/[application]/data/view/v2/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/view/v2/index.svelte new file mode 100644 index 0000000000..c11ca87023 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/data/view/v2/index.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 1346d7c8b2..5d3cd20109 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -111,7 +111,7 @@ export const deriveStores = context => { // Create new fetch model const newFetch = fetchData({ API, - datasource, + datasource: $datasource, options: { filter: $filter, sortColumn: $sort.column, From 2d3da0dfcf6213e4b2412c625074535d27ffc8c3 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 26 Jul 2023 14:26:34 +0100 Subject: [PATCH 023/245] Fix null issues in view fetch, fix edit view popover, improve handling of nullish view params --- .../backend/DataTable/ViewV2DataTable.svelte | 3 +- .../TableNavigator/TableNavigator.svelte | 2 +- .../popovers/EditViewPopover.svelte | 33 ++++++++++--------- packages/builder/src/helpers/urlStateSync.js | 2 +- .../app/[application]/data/view/index.svelte | 15 +++++---- .../src/components/grid/stores/rows.js | 5 +++ .../frontend-core/src/fetch/ViewV2Fetch.js | 6 ++-- 7 files changed, 39 insertions(+), 27 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte index 6d549147fb..028030bb9a 100644 --- a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte @@ -1,5 +1,5 @@ - + diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 80e4e6039d..23eae75428 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -482,9 +482,14 @@ export const createActions = context => { } // Refreshes the schema of the data fetch subscription - const refreshTableDefinition = async () => { - const definition = await API.fetchTableDefinition(get(tableId)) - table.set(definition) + const refreshDatasourceDefinition = async () => { + const $datasource = get(datasource) + if ($datasource.type === "table") { + table.set(await API.fetchTableDefinition($datasource.tableId)) + } else if ($datasource.type === "viewV2") { + // const definition = await API.viewsV2.(get(tableId)) + // table.set(definition) + } } // Checks if we have a row with a certain ID @@ -520,7 +525,7 @@ export const createActions = context => { refreshRow, replaceRow, refreshData, - refreshTableDefinition, + refreshDatasourceDefinition, }, }, } From 9665ec34dd7de7dadc0cc49cf8fced0f88568950 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 27 Jul 2023 15:53:50 +0100 Subject: [PATCH 026/245] Adjust grid props and config --- .../src/components/grid/layout/Grid.svelte | 24 +++++++++---------- .../src/components/grid/stores/config.js | 16 +++---------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/packages/frontend-core/src/components/grid/layout/Grid.svelte b/packages/frontend-core/src/components/grid/layout/Grid.svelte index ea7cd73a4b..343419af58 100644 --- a/packages/frontend-core/src/components/grid/layout/Grid.svelte +++ b/packages/frontend-core/src/components/grid/layout/Grid.svelte @@ -32,12 +32,12 @@ export let datasource = null export let schemaOverrides = null export let columnWhitelist = null - export let allowAddRows = true - export let allowExpandRows = true - export let allowEditRows = true - export let allowDeleteRows = true - export let allowEditColumns = true - export let allowSchemaChanges = true + export let canAddRows = true + export let canExpandRows = true + export let canEditRows = true + export let canDeleteRows = true + export let canEditColumns = true + export let canSaveSchema = true export let stripeRows = false export let collaboration = true export let showAvatars = true @@ -83,12 +83,12 @@ datasource, schemaOverrides, columnWhitelist, - allowAddRows, - allowExpandRows, - allowEditRows, - allowDeleteRows, - allowEditColumns, - allowSchemaChanges, + canAddRows, + canExpandRows, + canEditRows, + canDeleteRows, + canEditColumns, + canSaveSchema, stripeRows, collaboration, showAvatars, diff --git a/packages/frontend-core/src/components/grid/stores/config.js b/packages/frontend-core/src/components/grid/stores/config.js index 82ec8e29f3..0ed04bf741 100644 --- a/packages/frontend-core/src/components/grid/stores/config.js +++ b/packages/frontend-core/src/components/grid/stores/config.js @@ -21,16 +21,10 @@ export const deriveStores = context => { [props, hasNonAutoColumn], ([$props, $hasNonAutoColumn]) => { let config = { - // Row features - canAddRows: $props.allowAddRows, - canExpandRows: $props.allowExpandRows, - canEditRows: $props.allowEditRows, - canDeleteRows: $props.allowDeleteRows, + ...$props, - // Column features - canEditColumns: $props.allowEditColumns, - canEditPrimaryDisplay: $props.allowEditColumns, - canSaveSchema: $props.allowSchemaChanges, + // Additional granular features which we don't expose as props + canEditPrimaryDisplay: $props.canEditColumns, } // Disable some features if we're editing a view @@ -42,15 +36,11 @@ export const deriveStores = context => { config.canAddRows = false } - console.log($hasNonAutoColumn) - // Disable adding rows if we don't have any valid columns if (!$hasNonAutoColumn) { config.canAddRows = false } - console.log(config) - return config } ) From d83820b583bbaf0c4c661a00f9848513c4b94f3c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 28 Jul 2023 15:57:35 +0100 Subject: [PATCH 027/245] Fix adding rows --- packages/frontend-core/src/api/rows.js | 2 +- .../src/components/grid/stores/index.js | 2 +- .../src/components/grid/stores/rows.js | 17 ++++++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/frontend-core/src/api/rows.js b/packages/frontend-core/src/api/rows.js index 8e8570ea2a..3878b1f08b 100644 --- a/packages/frontend-core/src/api/rows.js +++ b/packages/frontend-core/src/api/rows.js @@ -23,7 +23,7 @@ export const buildRowEndpoints = API => ({ return } return await API.post({ - url: `/api/${row.tableId}/rows`, + url: `/api/${row.viewId || row.tableId}/rows`, body: row, suppressErrors, }) diff --git a/packages/frontend-core/src/components/grid/stores/index.js b/packages/frontend-core/src/components/grid/stores/index.js index 985fad8c4a..d287025342 100644 --- a/packages/frontend-core/src/components/grid/stores/index.js +++ b/packages/frontend-core/src/components/grid/stores/index.js @@ -22,8 +22,8 @@ const DependencyOrderedStores = [ Filter, Bounds, Scroll, - Rows, Columns, + Rows, UI, Validation, Resize, diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 23eae75428..69dd7ae97a 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -90,7 +90,6 @@ export const createActions = context => { } = context const instanceLoaded = writable(false) const fetch = writable(null) - const tableId = writable(null) // Local cache of row IDs to speed up checking if a row exists let rowCacheMap = {} @@ -261,10 +260,18 @@ export const createActions = context => { const addRow = async (row, idx, bubble = false) => { try { // Create row - const newRow = await API.saveRow( - { ...row, tableId: get(tableId) }, - SuppressErrors - ) + const $datasource = get(datasource) + let newRow = { ...row } + if ($datasource.type === "table") { + newRow.tableId = $datasource.tableId + } else if ($datasource.type === "viewV2") { + newRow.tableId = $datasource.tableId + newRow._viewId = $datasource.id + } else { + return + } + console.log(newRow) + newRow = await API.saveRow(newRow, SuppressErrors) // Update state if (idx != null) { From c7c9bd656385d27a41127a52039f35d46638ddc9 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 28 Jul 2023 16:01:18 +0100 Subject: [PATCH 028/245] Fix row deletion and fetching for tables --- packages/frontend-core/src/components/grid/stores/rows.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 69dd7ae97a..af7c698014 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -312,7 +312,7 @@ export const createActions = context => { // Fetches a row by ID using the search endpoint const fetchRow = async id => { const res = await API.searchTable({ - tableId: get(tableId), + tableId: get(datasource).tableId, limit: 1, query: { equal: { @@ -442,7 +442,7 @@ export const createActions = context => { delete row.__idx }) await API.deleteRows({ - tableId: get(tableId), + tableId: get(datasource).tableId, rows: rowsToDelete, }) From 1aea6fce095b1db31485c3f6569eb70a8c23113a Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 28 Jul 2023 16:02:31 +0100 Subject: [PATCH 029/245] Fix grid import and export for tables --- .../backend/DataTable/buttons/grid/GridExportButton.svelte | 4 ++-- .../backend/DataTable/buttons/grid/GridImportButton.svelte | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridExportButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridExportButton.svelte index b5fe202d11..f1bbc04328 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridExportButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridExportButton.svelte @@ -2,7 +2,7 @@ import ExportButton from "../ExportButton.svelte" import { getContext } from "svelte" - const { rows, columns, tableId, sort, selectedRows, filter } = + const { rows, columns, datasource, sort, selectedRows, filter } = getContext("grid") $: disabled = !$rows.length || !$columns.length @@ -12,7 +12,7 @@ From 8204935dfa9948c17580c25b11a63a71805eaafe Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sun, 30 Jul 2023 12:49:07 +0100 Subject: [PATCH 030/245] Add ability to update views, create views with existing filters and sorting applied --- .../DataTable/modals/CreateViewModal.svelte | 15 +++++++++++++-- .../src/components/grid/lib/websocket.js | 5 ++++- .../src/components/grid/stores/columns.js | 14 +++++++++++--- packages/frontend-core/src/fetch/ViewV2Fetch.js | 4 +--- packages/server/src/sdk/app/views/index.ts | 1 + 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte index ac5e522923..b5c3fb6e31 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte @@ -1,11 +1,17 @@ @@ -35,7 +37,6 @@ align="right" offset={0} popoverTarget={document.getElementById(`add-column-button`)} - animate={false} customZindex={100} >
Date: Tue, 1 Aug 2023 10:54:01 +0100 Subject: [PATCH 033/245] Disable collab for views --- packages/frontend-core/src/components/grid/lib/websocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-core/src/components/grid/lib/websocket.js b/packages/frontend-core/src/components/grid/lib/websocket.js index 1c490f74ca..2f9ebf59ad 100644 --- a/packages/frontend-core/src/components/grid/lib/websocket.js +++ b/packages/frontend-core/src/components/grid/lib/websocket.js @@ -7,7 +7,7 @@ export const createGridWebsocket = context => { const socket = createWebsocket("/socket/grid") const connectToDatasource = datasource => { - if (!socket.connected) { + if (!socket.connected || datasource?.type !== "table") { return } // Identify which table we are editing From 9d2b31af54232c03cca73c060c63642b53ba975d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Aug 2023 11:16:10 +0100 Subject: [PATCH 034/245] Enable creating and updating rows through views --- packages/frontend-core/src/api/viewsV2.js | 43 +++++++++++++++++++ .../grid/overlays/KeyboardManager.svelte | 6 +-- .../src/components/grid/stores/config.js | 2 - .../src/components/grid/stores/rows.js | 43 +++++++++++-------- 4 files changed, 71 insertions(+), 23 deletions(-) diff --git a/packages/frontend-core/src/api/viewsV2.js b/packages/frontend-core/src/api/viewsV2.js index 37bc78ae16..81fcfcf455 100644 --- a/packages/frontend-core/src/api/viewsV2.js +++ b/packages/frontend-core/src/api/viewsV2.js @@ -33,4 +33,47 @@ export const buildViewV2Endpoints = API => ({ delete: async viewId => { return await API.delete({ url: `/api/v2/views/${viewId}` }) }, + /** + * Creates a row from a view + * @param row the row to create + * @param suppressErrors whether or not to suppress error notifications + */ + createRow: async (row, suppressErrors = false) => { + if (!row?._viewId || !row?.tableId) { + return + } + return await API.post({ + url: `/api/v2/views/${row._viewId}/rows`, + body: row, + suppressErrors, + }) + }, + /** + * Updates an existing row through a view + * @param row the row to update + * @param suppressErrors whether or not to suppress error notifications + */ + updateRow: async (row, suppressErrors = false) => { + if (!row?._viewId || !row?.tableId || !row?._id) { + return + } + return await API.patch({ + url: `/api/v2/views/${row._viewId}/rows/${row._id}`, + body: row, + suppressErrors, + }) + }, + /** + * Deletes multiple rows from a table through a view + * @param viewId the table ID to delete the rows from + * @param rows the array of rows to delete + */ + deleteRows: async ({ viewId, rows }) => { + return await API.delete({ + url: `/api/v2/views/${viewId}/rows`, + body: { + rows, + }, + }) + }, }) diff --git a/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte b/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte index 721babd913..4f30c3c7db 100644 --- a/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte +++ b/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte @@ -4,7 +4,7 @@ import { NewRowID } from "../lib/constants" const { - enrichedRows, + rows, focusedCellId, visibleColumns, focusedRow, @@ -142,7 +142,7 @@ // Focuses the first cell in the grid const focusFirstCell = () => { - const firstRow = $enrichedRows[0] + const firstRow = $rows[0] if (!firstRow) { return } @@ -183,7 +183,7 @@ if (!$focusedRow) { return } - const newRow = $enrichedRows[$focusedRow.__idx + delta] + const newRow = $rows[$focusedRow.__idx + delta] if (newRow) { const split = $focusedCellId.split("-") $focusedCellId = `${newRow._id}-${split[1]}` diff --git a/packages/frontend-core/src/components/grid/stores/config.js b/packages/frontend-core/src/components/grid/stores/config.js index 0ed04bf741..9a72e781f7 100644 --- a/packages/frontend-core/src/components/grid/stores/config.js +++ b/packages/frontend-core/src/components/grid/stores/config.js @@ -31,9 +31,7 @@ export const deriveStores = context => { if ($props.datasource?.type === "viewV2") { config.canEditPrimaryDisplay = false config.canEditColumns = false - config.canEditRows = false config.canDeleteRows = false - config.canAddRows = false } // Disable adding rows if we don't have any valid columns diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 35bacb786a..ac50672e07 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -16,17 +16,13 @@ export const createStores = () => { const error = writable(null) // Generate a lookup map to quick find a row by ID - const rowLookupMap = derived( - rows, - $rows => { - let map = {} - for (let i = 0; i < $rows.length; i++) { - map[$rows[i]._id] = i - } - return map - }, - {} - ) + const rowLookupMap = derived(rows, $rows => { + let map = {} + for (let i = 0; i < $rows.length; i++) { + map[$rows[i]._id] = i + } + return map + }) // Mark loaded as true if we've ever stopped loading let hasStartedLoading = false @@ -47,8 +43,7 @@ export const createStores = () => { ...$rowChangeCache[row._id], __idx: idx, })) - }, - [] + } ) return { @@ -262,13 +257,14 @@ export const createActions = context => { let newRow = { ...row } if ($datasource.type === "table") { newRow.tableId = $datasource.tableId + newRow = await API.saveRow(newRow, SuppressErrors) } else if ($datasource.type === "viewV2") { newRow.tableId = $datasource.tableId newRow._viewId = $datasource.id + newRow = await API.viewV2.createRow(newRow) } else { return } - newRow = await API.saveRow(newRow, SuppressErrors) // Update state if (idx != null) { @@ -362,6 +358,7 @@ export const createActions = context => { const updateRow = async (rowId, changes) => { const $rows = get(rows) const $rowLookupMap = get(rowLookupMap) + const $datasource = get(datasource) const index = $rowLookupMap[rowId] const row = $rows[index] if (index == null || !Object.keys(changes || {}).length) { @@ -395,10 +392,20 @@ export const createActions = context => { ...state, [rowId]: true, })) - const saved = await API.saveRow( - { ...row, ...get(rowChangeCache)[rowId] }, - SuppressErrors - ) + + let saved + if ($datasource.type === "table") { + saved = await API.saveRow( + { ...row, ...get(rowChangeCache)[rowId] }, + SuppressErrors + ) + } else if ($datasource.type === "viewV2") { + saved = await API.viewV2.updateRow( + { ...row, ...get(rowChangeCache)[rowId] }, + SuppressErrors + ) + saved._viewId = $datasource.id + } // Update state after a successful change if (saved?._id) { From 1d21b4260abd6ffe4ce1fc30bf5a3d1364b24f92 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Aug 2023 11:21:22 +0100 Subject: [PATCH 035/245] Enable deleting rows through views --- packages/frontend-core/src/api/viewsV2.js | 4 ++++ .../src/components/grid/stores/config.js | 1 - .../src/components/grid/stores/rows.js | 16 ++++++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/frontend-core/src/api/viewsV2.js b/packages/frontend-core/src/api/viewsV2.js index 81fcfcf455..f2848b55f3 100644 --- a/packages/frontend-core/src/api/viewsV2.js +++ b/packages/frontend-core/src/api/viewsV2.js @@ -69,6 +69,10 @@ export const buildViewV2Endpoints = API => ({ * @param rows the array of rows to delete */ deleteRows: async ({ viewId, rows }) => { + // Ensure we delete _viewId from rows as otherwise this throws a 500 + rows?.forEach(row => { + delete row?._viewId + }) return await API.delete({ url: `/api/v2/views/${viewId}/rows`, body: { diff --git a/packages/frontend-core/src/components/grid/stores/config.js b/packages/frontend-core/src/components/grid/stores/config.js index 9a72e781f7..a0b1be19da 100644 --- a/packages/frontend-core/src/components/grid/stores/config.js +++ b/packages/frontend-core/src/components/grid/stores/config.js @@ -31,7 +31,6 @@ export const deriveStores = context => { if ($props.datasource?.type === "viewV2") { config.canEditPrimaryDisplay = false config.canEditColumns = false - config.canDeleteRows = false } // Disable adding rows if we don't have any valid columns diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index ac50672e07..ed9f99a3f6 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -440,15 +440,23 @@ export const createActions = context => { if (!rowsToDelete?.length) { return } + const $datasource = get(datasource) // Actually delete rows rowsToDelete.forEach(row => { delete row.__idx }) - await API.deleteRows({ - tableId: get(datasource).tableId, - rows: rowsToDelete, - }) + if ($datasource.type === "table") { + await API.deleteRows({ + tableId: $datasource.tableId, + rows: rowsToDelete, + }) + } else if ($datasource.type === "viewV2") { + await API.viewV2.deleteRows({ + viewId: $datasource.id, + rows: rowsToDelete, + }) + } // Update state handleRemoveRows(rowsToDelete) From cb01768e5a9b3154c0cea6f5f1aa4c3237098014 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Aug 2023 14:18:12 +0100 Subject: [PATCH 036/245] Fix crash when schemaNonUI does not exist --- packages/server/src/api/controllers/view/viewsV2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/view/viewsV2.ts b/packages/server/src/api/controllers/view/viewsV2.ts index cd28a1b55f..e2ac4c83fa 100644 --- a/packages/server/src/api/controllers/view/viewsV2.ts +++ b/packages/server/src/api/controllers/view/viewsV2.ts @@ -18,7 +18,7 @@ async function parseSchemaUI(ctx: Ctx, view: CreateViewRequest) { newObj: Record, existingObj: Record ) { - const result = Object.entries(newObj).some(([key, value]) => { + const result = Object.entries(newObj || {}).some(([key, value]) => { const isObject = typeof value === "object" const existing = existingObj[key] if (isObject && hasOverrides(value, existing || {})) { From bb810e14d73b5a2c1357bf8948935af119070c7e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Aug 2023 14:24:06 +0100 Subject: [PATCH 037/245] Fix issue with order 0 being ignored --- packages/server/src/sdk/app/views/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index 7fa5165220..e9f678371c 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -77,13 +77,13 @@ export function enrichSchema(view: View | ViewV2, tableSchema: TableSchema) { let schema = { ...tableSchema } if (view.schemaUI) { const viewOverridesEntries = Object.entries(view.schemaUI) - const viewSetsOrder = viewOverridesEntries.some(([_, v]) => v.order) + const viewSetsOrder = viewOverridesEntries.some(([_, v]) => v.order != null) for (const [fieldName, schemaUI] of viewOverridesEntries) { schema[fieldName] = { ...schema[fieldName], ...schemaUI, order: viewSetsOrder - ? schemaUI.order || undefined + ? schemaUI.order ?? undefined : schema[fieldName].order, } } From d825c32fdfbe5933a71c412066a2596e6c92f409 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Aug 2023 14:25:16 +0100 Subject: [PATCH 038/245] Hide access button for now --- .../src/components/backend/DataTable/ViewV2DataTable.svelte | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte index 028030bb9a..21b3e1c291 100644 --- a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte @@ -4,7 +4,6 @@ import { API } from "api" import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte" import GridFilterButton from "components/backend/DataTable/buttons/grid/GridFilterButton.svelte" - import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte" $: id = $viewsV2.selected?.id $: datasource = { @@ -31,7 +30,6 @@ - From 3482ec3d9e238f0bf13851f51dc7bda2b54ffbe9 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 1 Aug 2023 15:34:02 +0100 Subject: [PATCH 039/245] Add feature flags to view V2 fetch --- .../src/components/grid/stores/rows.js | 33 +++++++++++++++---- .../frontend-core/src/fetch/ViewV2Fetch.js | 8 +++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index ed9f99a3f6..3c33d92d25 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -169,6 +169,8 @@ export const createActions = context => { scroll.update(state => ({ ...state, top: 0 })) } + // For views we always update the filter to match the definition + // Process new rows handleNewRows($fetch.rows, resetRows) @@ -182,15 +184,19 @@ export const createActions = context => { // Update fetch when filter or sort config changes filter.subscribe($filter => { - get(fetch)?.update({ - filter: $filter, - }) + if (get(datasource)?.type === "table") { + get(fetch)?.update({ + filter: $filter, + }) + } }) sort.subscribe($sort => { - get(fetch)?.update({ - sortOrder: $sort.order, - sortColumn: $sort.column, - }) + if (get(datasource)?.type === "table") { + get(fetch)?.update({ + sortOrder: $sort.order, + sortColumn: $sort.column, + }) + } }) // Gets a row by ID @@ -549,3 +555,16 @@ export const createActions = context => { }, } } + +export const initialise = context => { + const { table, filter, datasource } = context + + // For views, always keep the UI for filter and sorting up to date with the + // latest view definition + table.subscribe($definition => { + if (!$definition || get(datasource)?.type !== "viewV2") { + return + } + filter.set($definition.query) + }) +} diff --git a/packages/frontend-core/src/fetch/ViewV2Fetch.js b/packages/frontend-core/src/fetch/ViewV2Fetch.js index 3713aab877..9bc61f0383 100644 --- a/packages/frontend-core/src/fetch/ViewV2Fetch.js +++ b/packages/frontend-core/src/fetch/ViewV2Fetch.js @@ -1,6 +1,14 @@ import DataFetch from "./DataFetch.js" export default class ViewV2Fetch extends DataFetch { + determineFeatureFlags() { + return { + supportsSearch: true, + supportsSort: true, + supportsPagination: true, + } + } + async getSchema(datasource, definition) { return definition?.schema } From ab47e49dd946c5e85ea4e7fc050ad8b6b40dffdf Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 2 Aug 2023 15:27:34 +0100 Subject: [PATCH 040/245] Update create view modal to only depend on grid context --- .../backend/DataTable/modals/CreateViewModal.svelte | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte index b5c3fb6e31..afb9efa53f 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte @@ -3,17 +3,16 @@ import { Input, notifications, ModalContent } from "@budibase/bbui" import { goto } from "@roxi/routify" import { viewsV2 } from "stores/backend" - import { tables } from "stores/backend" import { LuceneUtils } from "@budibase/frontend-core" const { filter, sort, table } = getContext("grid") $: query = LuceneUtils.buildLuceneQuery($filter) - $: console.log($table.schema) let name - $: views = Object.keys($tables.selected?.views || {}) + $: console.log($table) + $: views = Object.keys($table?.views || {}) $: nameExists = views.includes(name?.trim()) const saveView = async () => { @@ -21,17 +20,19 @@ try { const newView = await viewsV2.create({ name, - tableId: $tables.selected._id, + tableId: $table._id, query, sort: { field: $sort.column, order: $sort.order, }, - columns: $table.schema, + schema: $table.schema, + primaryDisplay: $table.primaryDisplay, }) notifications.success(`View ${name} created`) $goto(`../../view/v2/${newView.id}`) } catch (error) { + console.log(error) notifications.error("Error creating view") } } From cb2a19620bea0c00cc649506c390c59dac55abb6 Mon Sep 17 00:00:00 2001 From: Dean Date: Thu, 3 Aug 2023 09:29:12 +0100 Subject: [PATCH 041/245] Initial commit --- .../src/DetailSummary/DetailSummary.svelte | 4 +- .../src/builderStore/store/frontend.js | 89 ++++++---- .../EditFieldPopover.svelte | 161 ++++++++++++++++++ .../FieldConfiguration.svelte | 155 +++++++++++++---- .../settings/controls/SettingsList.svelte | 133 +++++++++++++++ .../settings/ComponentSettingsSection.svelte | 36 ++-- packages/client/manifest.json | 110 ++++++------ .../app/blocks/form/FormBlock.svelte | 30 +++- .../app/blocks/form/InnerFormBlock.svelte | 34 ++-- 9 files changed, 591 insertions(+), 161 deletions(-) create mode 100644 packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte create mode 100644 packages/builder/src/components/design/settings/controls/SettingsList.svelte diff --git a/packages/bbui/src/DetailSummary/DetailSummary.svelte b/packages/bbui/src/DetailSummary/DetailSummary.svelte index f7e2611792..daa9f3f5ca 100644 --- a/packages/bbui/src/DetailSummary/DetailSummary.svelte +++ b/packages/bbui/src/DetailSummary/DetailSummary.svelte @@ -44,7 +44,9 @@ align-items: stretch; border-bottom: var(--border-light); } - + .property-group-container:last-child { + border-bottom: 0px; + } .property-group-name { cursor: pointer; display: flex; diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index f312a58e97..4df26182e6 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -93,6 +93,40 @@ const INITIAL_FRONTEND_STATE = { tourNodes: null, } +export const updateComponentSetting = (name, value) => { + return component => { + if (!name || !component) { + return false + } + // Skip update if the value is the same + if (component[name] === value) { + return false + } + + const settings = getComponentSettings(component._component) + const updatedSetting = settings.find(setting => setting.key === name) + + if ( + updatedSetting?.type === "dataSource" || + updatedSetting?.type === "table" + ) { + const { schema } = getSchemaForDatasource(null, value) + const columnNames = Object.keys(schema || {}) + const multifieldKeysToSelectAll = settings + .filter(setting => { + return setting.type === "multifield" && setting.selectAllFields + }) + .map(setting => setting.key) + + multifieldKeysToSelectAll.forEach(key => { + component[key] = columnNames + }) + } + + component[name] = value + } +} + export const getFrontendStore = () => { const store = writable({ ...INITIAL_FRONTEND_STATE }) let websocket @@ -111,13 +145,18 @@ export const getFrontendStore = () => { } let clone = cloneDeep(screen) const result = patchFn(clone) + console.log("sequentialScreenPatch ", result) if (result === false) { return } - return await store.actions.screens.save(clone) + return + //return await store.actions.screens.save(clone) }) store.actions = { + tester: (name, value) => { + return updateComponentSetting(name, value) + }, reset: () => { store.set({ ...INITIAL_FRONTEND_STATE }) websocket?.disconnect() @@ -825,6 +864,7 @@ export const getFrontendStore = () => { }, patch: async (patchFn, componentId, screenId) => { // Use selected component by default + console.log("front end patch") if (!componentId || !screenId) { const state = get(store) componentId = componentId || state.selectedComponentId @@ -834,6 +874,7 @@ export const getFrontendStore = () => { return } const patchScreen = screen => { + // findComponent looks in the tree not comp.settings[0] let component = findComponent(screen.props, componentId) if (!component) { return false @@ -842,6 +883,18 @@ export const getFrontendStore = () => { } await store.actions.screens.patch(patchScreen, screenId) }, + // Temporary + customPatch: async (patchFn, componentId, screenId) => { + console.log("patchUpdate :") + if (!componentId || !screenId) { + const state = get(store) + componentId = componentId || state.selectedComponentId + screenId = screenId || state.selectedScreenId + } + if (!componentId || !screenId || !patchFn) { + return + } + }, delete: async component => { if (!component) { return @@ -1207,37 +1260,9 @@ export const getFrontendStore = () => { }) }, updateSetting: async (name, value) => { - await store.actions.components.patch(component => { - if (!name || !component) { - return false - } - // Skip update if the value is the same - if (component[name] === value) { - return false - } - - const settings = getComponentSettings(component._component) - const updatedSetting = settings.find(setting => setting.key === name) - - if ( - updatedSetting?.type === "dataSource" || - updatedSetting?.type === "table" - ) { - const { schema } = getSchemaForDatasource(null, value) - const columnNames = Object.keys(schema || {}) - const multifieldKeysToSelectAll = settings - .filter(setting => { - return setting.type === "multifield" && setting.selectAllFields - }) - .map(setting => setting.key) - - multifieldKeysToSelectAll.forEach(key => { - component[key] = columnNames - }) - } - - component[name] = value - }) + await store.actions.components.patch( + updateComponentSetting(name, value) + ) }, requestEjectBlock: componentId => { store.actions.preview.sendEvent("eject-block", componentId) diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte b/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte new file mode 100644 index 0000000000..859f019a26 --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte @@ -0,0 +1,161 @@ + + + + { + popover.show() + }} +/> + + + + + + + + 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 f9dccf586c..922b994c5a 100644 --- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte +++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte @@ -1,45 +1,125 @@
- {text} +
- - - Configure the fields in your form. - - - - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte index 78eecea76e..72383fcf3c 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte @@ -13,11 +13,16 @@ export let bindings export let componentBindings export let isScreen = false + export let onUpdateSetting $: sections = getSections(componentInstance, componentDefinition, isScreen) const getSections = (instance, definition, isScreen) => { const settings = definition?.settings ?? [] + console.log( + "ComponentSettingsSection::definition?.settings", + definition?.settings + ) const generalSettings = settings.filter(setting => !setting.section) const customSections = settings.filter(setting => setting.section) let sections = [ @@ -46,19 +51,23 @@ } const updateSetting = async (setting, value) => { - try { - await store.actions.components.updateSetting(setting.key, value) + if (typeof onUpdateSetting === "function") { + onUpdateSetting(setting, value) + } else { + try { + await store.actions.components.updateSetting(setting.key, value) - // Send event if required - if (setting.sendEvents) { - analytics.captureEvent(Events.COMPONENT_UPDATED, { - name: componentInstance._component, - setting: setting.key, - value, - }) + // Send event if required + if (setting.sendEvents) { + analytics.captureEvent(Events.COMPONENT_UPDATED, { + name: componentInstance._component, + setting: setting.key, + value, + }) + } + } catch (error) { + notifications.error("Error updating component prop") } - } catch (error) { - notifications.error("Error updating component prop") } } @@ -129,10 +138,13 @@ {/if} {#each section.settings as setting (setting.key)} {#if setting.visible} + { - if (typeof fields?.[0] === "string") { - return fields.map(field => ({ name: field, displayName: field })) - } - - return fields + return typeof fields?.[0] === "string" + ? fields.map(field => ({ + name: field, + displayName: field, + active: true, + })) + : fields } + //All settings need to derive from the block config now + + // Parse the fields here too. Not present means false. const getDefaultFields = (fields, schema) => { + let formFields if (schema && (!fields || fields.length === 0)) { const defaultFields = [] @@ -41,13 +47,20 @@ defaultFields.push({ name: field.name, displayName: field.name, + active: true, }) }) - - return defaultFields + formFields = [...defaultFields] + } else { + formFields = (fields || []).map(field => { + return { + ...field, + active: typeof field?.active != "boolean" ? true : field?.active, + } + }) } - return fields + return formFields.filter(field => field.active) } let schema @@ -56,7 +69,6 @@ $: formattedFields = convertOldFieldFormat(fields) $: fieldsOrDefault = getDefaultFields(formattedFields, schema) - $: fetchSchema(dataSource) $: dataProvider = `{{ literal ${safe(providerId)} }}` $: filter = [ diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte index 5e4a156949..b2870b02bf 100644 --- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte +++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte @@ -197,22 +197,24 @@ {/if} {/if} - - {#each fields as field, idx} - {#if getComponentForField(field.name)} - - {/if} - {/each} - + {#key fields} + + {#each fields as field, idx} + {#if getComponentForField(field.name)} + + {/if} + {/each} + + {/key} {:else} From e3cf0667be65faf53ba72bcce1b5f288d47b27a5 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 3 Aug 2023 11:18:19 +0100 Subject: [PATCH 042/245] Refactor grid to split up stores and provide better separation of datasource-specific logic --- .../backend/DataTable/TableDataTable.svelte | 2 +- .../buttons/grid/GridFilterButton.svelte | 10 +- .../buttons/grid/GridImportButton.svelte | 6 +- .../grid/GridManageAccessButton.svelte | 4 +- .../grid/GridRelationshipButton.svelte | 6 +- .../modals/grid/GridCreateColumnModal.svelte | 4 +- .../grid/controls/SizeButton.svelte | 14 ++- .../src/components/grid/index.js | 1 + .../src/components/grid/lib/constants.js | 4 + .../src/components/grid/stores/columns.js | 96 +++------------- .../src/components/grid/stores/config.js | 4 +- .../src/components/grid/stores/datsource.js | 107 ++++++++++++++++++ .../src/components/grid/stores/index.js | 9 ++ .../src/components/grid/stores/rows.js | 73 +++--------- .../src/components/grid/stores/sort.js | 3 +- .../src/components/grid/stores/table.js | 24 ++++ .../src/components/grid/stores/ui.js | 8 +- .../src/components/grid/stores/viewV2.js | 43 +++++++ packages/frontend-core/src/fetch/DataFetch.js | 10 +- 19 files changed, 262 insertions(+), 166 deletions(-) create mode 100644 packages/frontend-core/src/components/grid/stores/datsource.js create mode 100644 packages/frontend-core/src/components/grid/stores/table.js create mode 100644 packages/frontend-core/src/components/grid/stores/viewV2.js diff --git a/packages/builder/src/components/backend/DataTable/TableDataTable.svelte b/packages/builder/src/components/backend/DataTable/TableDataTable.svelte index 33175b336d..109c965271 100644 --- a/packages/builder/src/components/backend/DataTable/TableDataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/TableDataTable.svelte @@ -61,7 +61,7 @@ allowDeleteRows={!isUsersTable} schemaOverrides={isUsersTable ? userSchemaOverrides : null} showAvatars={false} - on:updatetable={handleGridTableUpdate} + on:updatedatasource={handleGridTableUpdate} > diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridFilterButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridFilterButton.svelte index 45e5d4e2b0..ae4d39483e 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridFilterButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridFilterButton.svelte @@ -2,22 +2,22 @@ import TableFilterButton from "../TableFilterButton.svelte" import { getContext } from "svelte" - const { columns, tableId, filter, table } = getContext("grid") + const { columns, datasource, filter, definition } = getContext("grid") // Wipe filter whenever table ID changes to avoid using stale filters - $: $tableId, filter.set([]) + $: $datasource, filter.set([]) const onFilter = e => { filter.set(e.detail || []) } -{#key $tableId} +{#key $datasource} {/key} diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridImportButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridImportButton.svelte index 667cf5e89a..71d971891c 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridImportButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridImportButton.svelte @@ -4,12 +4,12 @@ export let disabled = false - const { rows, datasource, table } = getContext("grid") + const { rows, datasource, definition } = getContext("grid") diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte index 154007950a..cc15ae564e 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte @@ -2,7 +2,7 @@ import ManageAccessButton from "../ManageAccessButton.svelte" import { getContext } from "svelte" - const { tableId } = getContext("grid") + const { datasource } = getContext("grid") - + diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridRelationshipButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridRelationshipButton.svelte index 460391366f..baa7dbed14 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridRelationshipButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridRelationshipButton.svelte @@ -2,12 +2,12 @@ import ExistingRelationshipButton from "../ExistingRelationshipButton.svelte" import { getContext } from "svelte" - const { table, rows } = getContext("grid") + const { definition, rows } = getContext("grid") -{#if $table} +{#if $definition} rows.actions.refreshData()} /> {/if} diff --git a/packages/builder/src/components/backend/DataTable/modals/grid/GridCreateColumnModal.svelte b/packages/builder/src/components/backend/DataTable/modals/grid/GridCreateColumnModal.svelte index 56ce7ebc2b..2040f66706 100644 --- a/packages/builder/src/components/backend/DataTable/modals/grid/GridCreateColumnModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/grid/GridCreateColumnModal.svelte @@ -2,7 +2,7 @@ import { getContext } from "svelte" import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte" - const { rows } = getContext("grid") + const { datasource } = getContext("grid") - + diff --git a/packages/frontend-core/src/components/grid/controls/SizeButton.svelte b/packages/frontend-core/src/components/grid/controls/SizeButton.svelte index 22e0c6c2e9..c2797ce537 100644 --- a/packages/frontend-core/src/components/grid/controls/SizeButton.svelte +++ b/packages/frontend-core/src/components/grid/controls/SizeButton.svelte @@ -8,8 +8,14 @@ SmallRowHeight, } from "../lib/constants" - const { stickyColumn, columns, rowHeight, table, fixedRowHeight } = - getContext("grid") + const { + stickyColumn, + columns, + rowHeight, + definition, + fixedRowHeight, + datasource, + } = getContext("grid") // Some constants for column width options const smallColSize = 120 @@ -60,8 +66,8 @@ ] const changeRowHeight = height => { - columns.actions.saveTable({ - ...$table, + datasource.actions.saveDefinition({ + ...$definition, rowHeight: height, }) } diff --git a/packages/frontend-core/src/components/grid/index.js b/packages/frontend-core/src/components/grid/index.js index 25747ec142..453e12967f 100644 --- a/packages/frontend-core/src/components/grid/index.js +++ b/packages/frontend-core/src/components/grid/index.js @@ -1 +1,2 @@ export { default as Grid } from "./layout/Grid.svelte" +export { DatasourceType } from "./lib/constants" diff --git a/packages/frontend-core/src/components/grid/lib/constants.js b/packages/frontend-core/src/components/grid/lib/constants.js index a6e6723463..de9fa6591c 100644 --- a/packages/frontend-core/src/components/grid/lib/constants.js +++ b/packages/frontend-core/src/components/grid/lib/constants.js @@ -1,3 +1,7 @@ +export const DatasourceType = { + Table: "table", + ViewV2: "viewV2", +} export const Padding = 246 export const MaxCellRenderHeight = 222 export const ScrollBarSize = 8 diff --git a/packages/frontend-core/src/components/grid/stores/columns.js b/packages/frontend-core/src/components/grid/stores/columns.js index 6573bc3937..8059bd98d7 100644 --- a/packages/frontend-core/src/components/grid/stores/columns.js +++ b/packages/frontend-core/src/components/grid/stores/columns.js @@ -69,8 +69,7 @@ export const deriveStores = context => { } export const createActions = context => { - const { table, columns, stickyColumn, API, dispatch, config, datasource } = - context + const { columns, stickyColumn, config, datasource, definition } = context // Checks if we have a certain column by name const hasColumn = column => { @@ -79,13 +78,13 @@ export const createActions = context => { return $columns.some(col => col.name === column) || $sticky?.name === column } - // Updates the tables primary display column + // Updates the datasources primary display column const changePrimaryDisplay = async column => { if (!get(config).canEditPrimaryDisplay) { return } - return await saveTable({ - ...get(table), + return await datasource.actions.saveDefinition({ + ...get(definition), primaryDisplay: column, }) } @@ -107,14 +106,14 @@ export const createActions = context => { await saveChanges() } - // Persists column changes by saving metadata against table schema + // Persists column changes by saving metadata against datasource schema const saveChanges = async () => { const $columns = get(columns) - const $table = get(table) + const $definition = get(definition) const $stickyColumn = get(stickyColumn) - const newSchema = cloneDeep($table.schema) + const newSchema = cloneDeep($definition.schema) - // Build new updated table schema + // Build new updated datasource schema Object.keys(newSchema).forEach(column => { // Respect order specified by columns const index = $columns.findIndex(x => x.name === column) @@ -134,28 +133,10 @@ export const createActions = context => { } }) - await saveTable({ ...$table, schema: newSchema }) - } - - const saveTable = async newTable => { - const $config = get(config) - const $datasource = get(datasource) - - // Update local state - table.set(newTable) - - // Update server - if ($config.canSaveSchema) { - if ($datasource.type === "table") { - await API.saveTable(newTable) - } else if ($datasource.type === "viewV2") { - await API.viewV2.update({ ...newTable }) - } - } - - // Broadcast change to external state can be updated, as this change - // will not be received by the builder websocket because we caused it ourselves - dispatch("updatetable", newTable) + await datasource.actions.saveDefinition({ + ...$definition, + schema: newSchema, + }) } return { @@ -164,7 +145,6 @@ export const createActions = context => { actions: { hasColumn, saveChanges, - saveTable, changePrimaryDisplay, changeAllColumnWidths, }, @@ -173,51 +153,7 @@ export const createActions = context => { } export const initialise = context => { - const { table, columns, stickyColumn, schemaOverrides, columnWhitelist } = - context - - const schema = derived( - [table, schemaOverrides, columnWhitelist], - ([$table, $schemaOverrides, $columnWhitelist]) => { - if (!$table?.schema) { - return null - } - let newSchema = { ...$table?.schema } - - // Edge case to temporarily allow deletion of duplicated user - // fields that were saved with the "disabled" flag set. - // By overriding the saved schema we ensure only overrides can - // set the disabled flag. - // TODO: remove in future - Object.keys(newSchema).forEach(field => { - newSchema[field] = { - ...newSchema[field], - disabled: false, - } - }) - - // Apply schema overrides - Object.keys($schemaOverrides || {}).forEach(field => { - if (newSchema[field]) { - newSchema[field] = { - ...newSchema[field], - ...$schemaOverrides[field], - } - } - }) - - // Apply whitelist if specified - if ($columnWhitelist?.length) { - Object.keys(newSchema).forEach(key => { - if (!$columnWhitelist.includes(key)) { - delete newSchema[key] - } - }) - } - - return newSchema - } - ) + const { definition, columns, stickyColumn, schema } = context // Merge new schema fields with existing schema in order to preserve widths schema.subscribe($schema => { @@ -226,12 +162,12 @@ export const initialise = context => { stickyColumn.set(null) return } - const $table = get(table) + const $definition = get(definition) // Find primary display let primaryDisplay - if ($table.primaryDisplay && $schema[$table.primaryDisplay]) { - primaryDisplay = $table.primaryDisplay + if ($definition.primaryDisplay && $schema[$definition.primaryDisplay]) { + primaryDisplay = $definition.primaryDisplay } // Get field list diff --git a/packages/frontend-core/src/components/grid/stores/config.js b/packages/frontend-core/src/components/grid/stores/config.js index a0b1be19da..de81a32ff5 100644 --- a/packages/frontend-core/src/components/grid/stores/config.js +++ b/packages/frontend-core/src/components/grid/stores/config.js @@ -1,5 +1,6 @@ import { derivedMemo } from "../../../utils" import { derived } from "svelte/store" +import { DatasourceType } from "../lib/constants" export const deriveStores = context => { const { props, hasNonAutoColumn } = context @@ -28,8 +29,7 @@ export const deriveStores = context => { } // Disable some features if we're editing a view - if ($props.datasource?.type === "viewV2") { - config.canEditPrimaryDisplay = false + if ($props.datasource?.type === DatasourceType.ViewV2) { config.canEditColumns = false } diff --git a/packages/frontend-core/src/components/grid/stores/datsource.js b/packages/frontend-core/src/components/grid/stores/datsource.js new file mode 100644 index 0000000000..a0f6e36811 --- /dev/null +++ b/packages/frontend-core/src/components/grid/stores/datsource.js @@ -0,0 +1,107 @@ +import { derived, get, writable } from "svelte/store" + +export const createStores = () => { + const definition = writable(null) + + return { + definition, + } +} + +export const deriveStores = context => { + const { definition, schemaOverrides, columnWhitelist } = context + + const schema = derived( + [definition, schemaOverrides, columnWhitelist], + ([$definition, $schemaOverrides, $columnWhitelist]) => { + if (!$definition?.schema) { + return null + } + let newSchema = { ...$definition?.schema } + + // Edge case to temporarily allow deletion of duplicated user + // fields that were saved with the "disabled" flag set. + // By overriding the saved schema we ensure only overrides can + // set the disabled flag. + // TODO: remove in future + Object.keys(newSchema).forEach(field => { + newSchema[field] = { + ...newSchema[field], + disabled: false, + } + }) + + // Apply schema overrides + Object.keys($schemaOverrides || {}).forEach(field => { + if (newSchema[field]) { + newSchema[field] = { + ...newSchema[field], + ...$schemaOverrides[field], + } + } + }) + + // Apply whitelist if specified + if ($columnWhitelist?.length) { + Object.keys(newSchema).forEach(key => { + if (!$columnWhitelist.includes(key)) { + delete newSchema[key] + } + }) + } + + return newSchema + } + ) + + return { + schema, + } +} + +export const createActions = context => { + const { datasource, definition, API, config, dispatch } = context + + // Refreshes the datasource definition + const refreshDefinition = async () => { + const $datasource = get(datasource) + if ($datasource.type === "table") { + definition.set(await API.fetchTableDefinition($datasource.tableId)) + } else if ($datasource.type === "viewV2") { + // const definition = await API.viewsV2.(get(tableId)) + // table.set(definition) + } + } + + // Saves the datasource definition + const saveDefinition = async newDefinition => { + const $config = get(config) + const $datasource = get(datasource) + + // Update local state + definition.set(newDefinition) + + // Update server + if ($config.canSaveSchema) { + if ($datasource.type === "table") { + await API.saveTable(newDefinition) + } else if ($datasource.type === "viewV2") { + await API.viewV2.update({ ...newDefinition }) + } + } + + // Broadcast change to external state can be updated, as this change + // will not be received by the builder websocket because we caused it ourselves + dispatch("updatedefinition", newDefinition) + } + + return { + datasource: { + ...datasource, + actions: { + refreshDefinition, + saveDefinition, + }, + }, + } +} diff --git a/packages/frontend-core/src/components/grid/stores/index.js b/packages/frontend-core/src/components/grid/stores/index.js index d287025342..70a9471991 100644 --- a/packages/frontend-core/src/components/grid/stores/index.js +++ b/packages/frontend-core/src/components/grid/stores/index.js @@ -15,13 +15,18 @@ 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 Datasource from "./datsource" const DependencyOrderedStores = [ + // Common stores Notifications, Sort, Filter, Bounds, Scroll, + Datasource, Columns, Rows, UI, @@ -34,6 +39,10 @@ const DependencyOrderedStores = [ Pagination, Clipboard, Config, + + // Datasource specific stores + Table, + ViewV2, ] export const attachStores = context => { diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 3c33d92d25..dce4fca64b 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -7,13 +7,13 @@ const SuppressErrors = true export const createStores = () => { const rows = writable([]) - const table = writable(null) const loading = writable(false) const loaded = writable(false) const rowChangeCache = writable({}) const inProgressChanges = writable({}) const hasNextPage = writable(false) const error = writable(null) + const fetch = writable(null) // Generate a lookup map to quick find a row by ID const rowLookupMap = derived(rows, $rows => { @@ -51,8 +51,8 @@ export const createStores = () => { ...rows, subscribe: enrichedRows.subscribe, }, + fetch, rowLookupMap, - table, loaded, loading, rowChangeCache, @@ -66,7 +66,7 @@ export const createActions = context => { const { rows, rowLookupMap, - table, + definition, filter, loading, sort, @@ -82,14 +82,14 @@ export const createActions = context => { hasNextPage, error, notifications, + fetch, } = context const instanceLoaded = writable(false) - const fetch = writable(null) // Local cache of row IDs to speed up checking if a row exists let rowCacheMap = {} - // Reset everything when table ID changes + // Reset everything when datasource changes let unsubscribe = null let lastResetKey = null datasource.subscribe(async $datasource => { @@ -100,11 +100,11 @@ export const createActions = context => { loading.set(true) // Abandon if we don't have a valid datasource - if (!$datasource?.tableId) { + if (!$datasource) { return } - // Tick to allow other reactive logic to update stores when table ID changes + // Tick to allow other reactive logic to update stores when datasource changes // before proceeding. This allows us to wipe filters etc if needed. await tick() const $filter = get(filter) @@ -142,7 +142,7 @@ export const createActions = context => { const previousResetKey = lastResetKey lastResetKey = $fetch.resetKey - // If resetting rows due to a table change, wipe data and wait for + // If resetting rows due to a datasource change, wipe data and wait for // derived stores to compute. This prevents stale data being passed // to cells when we save the new schema. if (!$instanceLoaded && previousResetKey) { @@ -152,16 +152,17 @@ export const createActions = context => { // Reset state properties when dataset changes if (!$instanceLoaded || resetRows) { - table.set($fetch.definition) - sort.set({ - column: $fetch.sortColumn, - order: $fetch.sortOrder, - }) + definition.set($fetch.definition) + + // sort.set({ + // column: $fetch.sortColumn, + // order: $fetch.sortOrder, + // }) } // Reset scroll state when data changes if (!$instanceLoaded) { - // Reset both top and left for a new table ID + // Reset both top and left for a new datasource ID instanceLoaded.set(true) scroll.set({ top: 0, left: 0 }) } else if (resetRows) { @@ -169,8 +170,6 @@ export const createActions = context => { scroll.update(state => ({ ...state, top: 0 })) } - // For views we always update the filter to match the definition - // Process new rows handleNewRows($fetch.rows, resetRows) @@ -182,23 +181,6 @@ export const createActions = context => { fetch.set(newFetch) }) - // Update fetch when filter or sort config changes - filter.subscribe($filter => { - if (get(datasource)?.type === "table") { - get(fetch)?.update({ - filter: $filter, - }) - } - }) - sort.subscribe($sort => { - if (get(datasource)?.type === "table") { - get(fetch)?.update({ - sortOrder: $sort.order, - sortColumn: $sort.column, - }) - } - }) - // Gets a row by ID const getRow = id => { const index = get(rowLookupMap)[id] @@ -506,17 +488,6 @@ export const createActions = context => { get(fetch)?.nextPage() } - // Refreshes the schema of the data fetch subscription - const refreshDatasourceDefinition = async () => { - const $datasource = get(datasource) - if ($datasource.type === "table") { - table.set(await API.fetchTableDefinition($datasource.tableId)) - } else if ($datasource.type === "viewV2") { - // const definition = await API.viewsV2.(get(tableId)) - // table.set(definition) - } - } - // Checks if we have a row with a certain ID const hasRow = id => { if (id === NewRowID) { @@ -550,21 +521,7 @@ export const createActions = context => { refreshRow, replaceRow, refreshData, - refreshDatasourceDefinition, }, }, } } - -export const initialise = context => { - const { table, filter, datasource } = context - - // For views, always keep the UI for filter and sorting up to date with the - // latest view definition - table.subscribe($definition => { - if (!$definition || get(datasource)?.type !== "viewV2") { - return - } - filter.set($definition.query) - }) -} diff --git a/packages/frontend-core/src/components/grid/stores/sort.js b/packages/frontend-core/src/components/grid/stores/sort.js index 3beecd2c2a..098e126930 100644 --- a/packages/frontend-core/src/components/grid/stores/sort.js +++ b/packages/frontend-core/src/components/grid/stores/sort.js @@ -16,7 +16,8 @@ export const createStores = context => { } export const initialise = context => { - const { sort, initialSortColumn, initialSortOrder } = context + const { sort, initialSortColumn, initialSortOrder, table, datasource } = + context // Reset sort when initial sort props change initialSortColumn.subscribe(newSortColumn => { diff --git a/packages/frontend-core/src/components/grid/stores/table.js b/packages/frontend-core/src/components/grid/stores/table.js new file mode 100644 index 0000000000..bcac6df48f --- /dev/null +++ b/packages/frontend-core/src/components/grid/stores/table.js @@ -0,0 +1,24 @@ +import { get } from "svelte/store" + +export const initialise = context => { + const { datasource, fetch, filter, sort } = context + + // Update fetch when filter changes + filter.subscribe($filter => { + if (get(datasource)?.type === "table") { + get(fetch)?.update({ + filter: $filter, + }) + } + }) + + // Update fetch when sorting changes + sort.subscribe($sort => { + if (get(datasource)?.type === "table") { + get(fetch)?.update({ + sortOrder: $sort.order, + sortColumn: $sort.column, + }) + } + }) +} diff --git a/packages/frontend-core/src/components/grid/stores/ui.js b/packages/frontend-core/src/components/grid/stores/ui.js index e0c6fd4c9c..e10e96a722 100644 --- a/packages/frontend-core/src/components/grid/stores/ui.js +++ b/packages/frontend-core/src/components/grid/stores/ui.js @@ -131,7 +131,7 @@ export const initialise = context => { focusedCellId, selectedRows, hoveredRowId, - table, + definition, rowHeight, fixedRowHeight, } = context @@ -187,9 +187,9 @@ export const initialise = context => { }) // Pull row height from table as long as we don't have a fixed height - table.subscribe($table => { + definition.subscribe($definition => { if (!get(fixedRowHeight)) { - rowHeight.set($table?.rowHeight || DefaultRowHeight) + rowHeight.set($definition?.rowHeight || DefaultRowHeight) } }) @@ -198,7 +198,7 @@ export const initialise = context => { if (height) { rowHeight.set(height) } else { - rowHeight.set(get(table)?.rowHeight || DefaultRowHeight) + rowHeight.set(get(definition)?.rowHeight || DefaultRowHeight) } }) } diff --git a/packages/frontend-core/src/components/grid/stores/viewV2.js b/packages/frontend-core/src/components/grid/stores/viewV2.js new file mode 100644 index 0000000000..bf47513908 --- /dev/null +++ b/packages/frontend-core/src/components/grid/stores/viewV2.js @@ -0,0 +1,43 @@ +import { get } from "svelte/store" + +export const initialise = context => { + const { definition, datasource, sort, rows } = context + + // For views, keep sort state in line with the view definition + definition.subscribe($definition => { + if (!$definition || get(datasource)?.type !== "viewV2") { + return + } + const $sort = get(sort) + if ( + $definition.sort?.field !== $sort?.column || + $definition.sort?.order !== $sort?.order + ) { + sort.set({ + column: $definition.sort?.field, + order: $definition.sort?.order, + }) + } + }) + + // When sorting changes, ensure view definition is kept up to date + sort.subscribe(async $sort => { + const $view = get(definition) + if (!$view || get(datasource)?.type !== "viewV2") { + return + } + if ( + $sort?.column !== $view.sort?.field || + $sort?.order !== $view.sort?.order + ) { + await datasource.actions.saveDefinition({ + ...$view, + sort: { + field: $sort.column, + order: $sort.order, + }, + }) + await rows.actions.refreshData() + } + }) +} diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js index ea1cfdde77..a9803747ae 100644 --- a/packages/frontend-core/src/fetch/DataFetch.js +++ b/packages/frontend-core/src/fetch/DataFetch.js @@ -116,8 +116,16 @@ export default class DataFetch { async getInitialData() { const { datasource, filter, paginate } = this.options - // Fetch datasource definition and determine feature flags + // Fetch datasource definition and extract filter and sort if configured const definition = await this.getDefinition(datasource) + if (definition?.sort?.field) { + this.options.sortColumn = definition.sort.field + } + if (definition?.sort?.order) { + this.options.sortOrder = definition.sort.order + } + + // Determine feature flags const features = this.determineFeatureFlags(definition) this.features = { supportsSearch: !!features?.supportsSearch, From 3e97e299bf4702bcbd63918cdd13d28308582aaf Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 3 Aug 2023 11:27:33 +0100 Subject: [PATCH 043/245] Fix sorting for tables --- .../src/components/grid/stores/{datsource.js => datasource.js} | 2 +- packages/frontend-core/src/components/grid/stores/index.js | 2 +- packages/frontend-core/src/components/grid/stores/table.js | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) rename packages/frontend-core/src/components/grid/stores/{datsource.js => datasource.js} (98%) diff --git a/packages/frontend-core/src/components/grid/stores/datsource.js b/packages/frontend-core/src/components/grid/stores/datasource.js similarity index 98% rename from packages/frontend-core/src/components/grid/stores/datsource.js rename to packages/frontend-core/src/components/grid/stores/datasource.js index a0f6e36811..fb033cd1f1 100644 --- a/packages/frontend-core/src/components/grid/stores/datsource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -86,7 +86,7 @@ export const createActions = context => { if ($datasource.type === "table") { await API.saveTable(newDefinition) } else if ($datasource.type === "viewV2") { - await API.viewV2.update({ ...newDefinition }) + await API.viewV2.update(newDefinition) } } diff --git a/packages/frontend-core/src/components/grid/stores/index.js b/packages/frontend-core/src/components/grid/stores/index.js index 70a9471991..8104319f38 100644 --- a/packages/frontend-core/src/components/grid/stores/index.js +++ b/packages/frontend-core/src/components/grid/stores/index.js @@ -17,7 +17,7 @@ import * as Filter from "./filter" import * as Notifications from "./notifications" import * as Table from "./table" import * as ViewV2 from "./viewV2" -import * as Datasource from "./datsource" +import * as Datasource from "./datasource" const DependencyOrderedStores = [ // Common stores diff --git a/packages/frontend-core/src/components/grid/stores/table.js b/packages/frontend-core/src/components/grid/stores/table.js index bcac6df48f..59d351950a 100644 --- a/packages/frontend-core/src/components/grid/stores/table.js +++ b/packages/frontend-core/src/components/grid/stores/table.js @@ -15,6 +15,8 @@ export const initialise = context => { // Update fetch when sorting changes sort.subscribe($sort => { if (get(datasource)?.type === "table") { + console.log("update", $sort) + console.log(get(fetch)) get(fetch)?.update({ sortOrder: $sort.order, sortColumn: $sort.column, From b00f3d24188b6798c18ec4340b98f25f41ef5064 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 3 Aug 2023 11:28:02 +0100 Subject: [PATCH 044/245] Remove log --- packages/frontend-core/src/components/grid/stores/table.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/table.js b/packages/frontend-core/src/components/grid/stores/table.js index 59d351950a..bcac6df48f 100644 --- a/packages/frontend-core/src/components/grid/stores/table.js +++ b/packages/frontend-core/src/components/grid/stores/table.js @@ -15,8 +15,6 @@ export const initialise = context => { // Update fetch when sorting changes sort.subscribe($sort => { if (get(datasource)?.type === "table") { - console.log("update", $sort) - console.log(get(fetch)) get(fetch)?.update({ sortOrder: $sort.order, sortColumn: $sort.column, From d443bf3616f97e0bc9f326ad40c74eeb31a24765 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 3 Aug 2023 13:18:58 +0100 Subject: [PATCH 045/245] Split out more datasource specific logic --- .../src/components/grid/index.js | 1 - .../src/components/grid/lib/constants.js | 4 -- .../src/components/grid/stores/config.js | 3 +- .../src/components/grid/stores/datasource.js | 64 ++++++++++--------- .../src/components/grid/stores/index.js | 7 +- .../src/components/grid/stores/rows.js | 45 +++---------- .../src/components/grid/stores/table.js | 38 +++++++++++ .../src/components/grid/stores/viewV2.js | 58 +++++++++++++++++ 8 files changed, 142 insertions(+), 78 deletions(-) diff --git a/packages/frontend-core/src/components/grid/index.js b/packages/frontend-core/src/components/grid/index.js index 453e12967f..25747ec142 100644 --- a/packages/frontend-core/src/components/grid/index.js +++ b/packages/frontend-core/src/components/grid/index.js @@ -1,2 +1 @@ export { default as Grid } from "./layout/Grid.svelte" -export { DatasourceType } from "./lib/constants" diff --git a/packages/frontend-core/src/components/grid/lib/constants.js b/packages/frontend-core/src/components/grid/lib/constants.js index de9fa6591c..a6e6723463 100644 --- a/packages/frontend-core/src/components/grid/lib/constants.js +++ b/packages/frontend-core/src/components/grid/lib/constants.js @@ -1,7 +1,3 @@ -export const DatasourceType = { - Table: "table", - ViewV2: "viewV2", -} export const Padding = 246 export const MaxCellRenderHeight = 222 export const ScrollBarSize = 8 diff --git a/packages/frontend-core/src/components/grid/stores/config.js b/packages/frontend-core/src/components/grid/stores/config.js index de81a32ff5..46bff299d4 100644 --- a/packages/frontend-core/src/components/grid/stores/config.js +++ b/packages/frontend-core/src/components/grid/stores/config.js @@ -1,6 +1,5 @@ import { derivedMemo } from "../../../utils" import { derived } from "svelte/store" -import { DatasourceType } from "../lib/constants" export const deriveStores = context => { const { props, hasNonAutoColumn } = context @@ -29,7 +28,7 @@ export const deriveStores = context => { } // Disable some features if we're editing a view - if ($props.datasource?.type === DatasourceType.ViewV2) { + if ($props.datasource?.type === "viewV2") { config.canEditColumns = false } diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.js index fb033cd1f1..4e6fbedfd6 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -19,18 +19,6 @@ export const deriveStores = context => { } let newSchema = { ...$definition?.schema } - // Edge case to temporarily allow deletion of duplicated user - // fields that were saved with the "disabled" flag set. - // By overriding the saved schema we ensure only overrides can - // set the disabled flag. - // TODO: remove in future - Object.keys(newSchema).forEach(field => { - newSchema[field] = { - ...newSchema[field], - disabled: false, - } - }) - // Apply schema overrides Object.keys($schemaOverrides || {}).forEach(field => { if (newSchema[field]) { @@ -60,34 +48,34 @@ export const deriveStores = context => { } export const createActions = context => { - const { datasource, definition, API, config, dispatch } = context + const { datasource, definition, config, dispatch, table, viewV2 } = context + + // Gets the appropriate API for the configured datasource type + const getAPI = () => { + const $datasource = get(datasource) + switch ($datasource?.type) { + case "table": + return table + case "viewV2": + return viewV2 + default: + return null + } + } // Refreshes the datasource definition const refreshDefinition = async () => { - const $datasource = get(datasource) - if ($datasource.type === "table") { - definition.set(await API.fetchTableDefinition($datasource.tableId)) - } else if ($datasource.type === "viewV2") { - // const definition = await API.viewsV2.(get(tableId)) - // table.set(definition) - } + return await getAPI()?.actions.refreshDefinition() } // Saves the datasource definition const saveDefinition = async newDefinition => { - const $config = get(config) - const $datasource = get(datasource) - // Update local state definition.set(newDefinition) // Update server - if ($config.canSaveSchema) { - if ($datasource.type === "table") { - await API.saveTable(newDefinition) - } else if ($datasource.type === "viewV2") { - await API.viewV2.update(newDefinition) - } + if (get(config).canSaveSchema) { + await getAPI()?.actions.saveDefinition(newDefinition) } // Broadcast change to external state can be updated, as this change @@ -95,12 +83,30 @@ export const createActions = context => { dispatch("updatedefinition", newDefinition) } + // Adds a row to the datasource + const addRow = async row => { + return await getAPI()?.actions.addRow(row) + } + + // Updates an existing row in the datasource + const updateRow = async row => { + return await getAPI()?.actions.updateRow(row) + } + + // Deletes rows from the datasource + const deleteRows = async rows => { + return await getAPI()?.actions.deleteRows(rows) + } + return { datasource: { ...datasource, actions: { refreshDefinition, saveDefinition, + addRow, + updateRow, + deleteRows, }, }, } diff --git a/packages/frontend-core/src/components/grid/stores/index.js b/packages/frontend-core/src/components/grid/stores/index.js index 8104319f38..cb858b7293 100644 --- a/packages/frontend-core/src/components/grid/stores/index.js +++ b/packages/frontend-core/src/components/grid/stores/index.js @@ -20,12 +20,13 @@ import * as ViewV2 from "./viewV2" import * as Datasource from "./datasource" const DependencyOrderedStores = [ - // Common stores Notifications, Sort, Filter, Bounds, Scroll, + Table, + ViewV2, Datasource, Columns, Rows, @@ -39,10 +40,6 @@ const DependencyOrderedStores = [ Pagination, Clipboard, Config, - - // Datasource specific stores - Table, - ViewV2, ] export const attachStores = context => { diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index dce4fca64b..d28707406b 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -240,19 +240,9 @@ export const createActions = context => { // Adds a new row const addRow = async (row, idx, bubble = false) => { try { - // Create row - const $datasource = get(datasource) + // Create row. Spread row so we can mutate and enrich safely. let newRow = { ...row } - if ($datasource.type === "table") { - newRow.tableId = $datasource.tableId - newRow = await API.saveRow(newRow, SuppressErrors) - } else if ($datasource.type === "viewV2") { - newRow.tableId = $datasource.tableId - newRow._viewId = $datasource.id - newRow = await API.viewV2.createRow(newRow) - } else { - return - } + newRow = await datasource.actions.addRow(newRow) // Update state if (idx != null) { @@ -381,19 +371,11 @@ export const createActions = context => { [rowId]: true, })) - let saved - if ($datasource.type === "table") { - saved = await API.saveRow( - { ...row, ...get(rowChangeCache)[rowId] }, - SuppressErrors - ) - } else if ($datasource.type === "viewV2") { - saved = await API.viewV2.updateRow( - { ...row, ...get(rowChangeCache)[rowId] }, - SuppressErrors - ) - saved._viewId = $datasource.id - } + // Update row + const saved = await datasource.actions.updateRow({ + ...row, + ...get(rowChangeCache)[rowId], + }) // Update state after a successful change if (saved?._id) { @@ -428,23 +410,12 @@ export const createActions = context => { if (!rowsToDelete?.length) { return } - const $datasource = get(datasource) // Actually delete rows rowsToDelete.forEach(row => { delete row.__idx }) - if ($datasource.type === "table") { - await API.deleteRows({ - tableId: $datasource.tableId, - rows: rowsToDelete, - }) - } else if ($datasource.type === "viewV2") { - await API.viewV2.deleteRows({ - viewId: $datasource.id, - rows: rowsToDelete, - }) - } + await datasource.actions.deleteRows(rowsToDelete) // Update state handleRemoveRows(rowsToDelete) diff --git a/packages/frontend-core/src/components/grid/stores/table.js b/packages/frontend-core/src/components/grid/stores/table.js index bcac6df48f..87a53343f4 100644 --- a/packages/frontend-core/src/components/grid/stores/table.js +++ b/packages/frontend-core/src/components/grid/stores/table.js @@ -1,5 +1,43 @@ import { get } from "svelte/store" +const SuppressErrors = true + +export const createActions = context => { + const { definition, API, datasource } = context + + const refreshDefinition = async () => { + definition.set(await API.fetchTableDefinition(get(datasource).tableId)) + } + + const saveDefinition = async newDefinition => { + await API.saveTable(newDefinition) + } + + const saveRow = async row => { + row.tableId = get(datasource)?.tableId + return await API.saveRow(row, SuppressErrors) + } + + const deleteRows = async rows => { + await API.deleteRows({ + tableId: get(datasource).tableId, + rows, + }) + } + + return { + table: { + actions: { + refreshDefinition, + saveDefinition, + addRow: saveRow, + updateRow: saveRow, + deleteRows, + }, + }, + } +} + export const initialise = context => { const { datasource, fetch, filter, sort } = context diff --git a/packages/frontend-core/src/components/grid/stores/viewV2.js b/packages/frontend-core/src/components/grid/stores/viewV2.js index bf47513908..44a73405a5 100644 --- a/packages/frontend-core/src/components/grid/stores/viewV2.js +++ b/packages/frontend-core/src/components/grid/stores/viewV2.js @@ -1,5 +1,63 @@ import { get } from "svelte/store" +const SuppressErrors = true + +export const createActions = context => { + const { definition, API, datasource } = 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 saveDefinition = async newDefinition => { + await API.viewV2.update(newDefinition) + } + + const addRow = async row => { + const $datasource = get(datasource) + row.tableId = $datasource?.tableId + row._viewId = $datasource?.id + return await API.viewV2.createRow(row, SuppressErrors) + } + + const updateRow = async row => { + const $datasource = get(datasource) + const savedRow = await API.viewV2.updateRow(row, SuppressErrors) + return { + ...savedRow, + _viewId: $datasource.id, + } + } + + const deleteRows = async rows => { + await API.viewV2.deleteRows({ + viewId: get(datasource).id, + rows, + }) + } + + return { + viewV2: { + actions: { + refreshDefinition, + saveDefinition, + addRow, + updateRow, + deleteRows, + }, + }, + } +} + export const initialise = context => { const { definition, datasource, sort, rows } = context From 3eeb945934c58f5be69c5c11f6e1a13bfac72b30 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 3 Aug 2023 13:22:49 +0100 Subject: [PATCH 046/245] Split out logic for getting rows from datasources --- .../src/components/grid/stores/datasource.js | 5 +++++ .../src/components/grid/stores/rows.js | 20 +------------------ .../src/components/grid/stores/table.js | 15 ++++++++++++++ .../src/components/grid/stores/viewV2.js | 16 +++++++++++++++ 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.js index 4e6fbedfd6..5ace5ceef8 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -98,6 +98,10 @@ export const createActions = context => { return await getAPI()?.actions.deleteRows(rows) } + const getRow = async id => { + return await getAPI()?.actions.getRow(id) + } + return { datasource: { ...datasource, @@ -107,6 +111,7 @@ export const createActions = context => { addRow, updateRow, deleteRows, + getRow, }, }, } diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index d28707406b..96194c6fc5 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -3,8 +3,6 @@ import { fetchData } from "../../../fetch/fetchData" import { NewRowID, RowPageSize } from "../lib/constants" import { tick } from "svelte" -const SuppressErrors = true - export const createStores = () => { const rows = writable([]) const loading = writable(false) @@ -280,21 +278,6 @@ export const createActions = context => { } } - // Fetches a row by ID using the search endpoint - const fetchRow = async id => { - const res = await API.searchTable({ - tableId: get(datasource).tableId, - limit: 1, - query: { - equal: { - _id: id, - }, - }, - paginate: false, - }) - return res?.rows?.[0] - } - // Replaces a row in state with the newly defined row, handling updates, // addition and deletion const replaceRow = (id, row) => { @@ -323,7 +306,7 @@ export const createActions = context => { // Refreshes a specific row const refreshRow = async id => { - const row = await fetchRow(id) + const row = await datasource.actions.getRow(id) replaceRow(id, row) } @@ -336,7 +319,6 @@ export const createActions = context => { const updateRow = async (rowId, changes) => { const $rows = get(rows) const $rowLookupMap = get(rowLookupMap) - const $datasource = get(datasource) const index = $rowLookupMap[rowId] const row = $rows[index] if (index == null || !Object.keys(changes || {}).length) { diff --git a/packages/frontend-core/src/components/grid/stores/table.js b/packages/frontend-core/src/components/grid/stores/table.js index 87a53343f4..d79696c263 100644 --- a/packages/frontend-core/src/components/grid/stores/table.js +++ b/packages/frontend-core/src/components/grid/stores/table.js @@ -25,6 +25,20 @@ export const createActions = context => { }) } + const getRow = async id => { + const res = await API.searchTable({ + tableId: get(datasource).tableId, + limit: 1, + query: { + equal: { + _id: id, + }, + }, + paginate: false, + }) + return res?.rows?.[0] + } + return { table: { actions: { @@ -33,6 +47,7 @@ export const createActions = context => { addRow: saveRow, updateRow: saveRow, deleteRows, + getRow, }, }, } diff --git a/packages/frontend-core/src/components/grid/stores/viewV2.js b/packages/frontend-core/src/components/grid/stores/viewV2.js index 44a73405a5..35e447d8fa 100644 --- a/packages/frontend-core/src/components/grid/stores/viewV2.js +++ b/packages/frontend-core/src/components/grid/stores/viewV2.js @@ -45,6 +45,21 @@ export const createActions = context => { }) } + // TODO: update in future. We can't depend on having table read access. + const getRow = async id => { + const res = await API.searchTable({ + tableId: get(datasource).tableId, + limit: 1, + query: { + equal: { + _id: id, + }, + }, + paginate: false, + }) + return res?.rows?.[0] + } + return { viewV2: { actions: { @@ -53,6 +68,7 @@ export const createActions = context => { addRow, updateRow, deleteRows, + getRow, }, }, } From f5e5a883cf82b9f386167027e5d551477ebb8bb8 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 3 Aug 2023 13:31:44 +0100 Subject: [PATCH 047/245] Enable renaming views and lint --- .../builder/src/stores/backend/viewsV2.js | 30 ++++++++----------- .../src/components/grid/stores/sort.js | 3 +- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/builder/src/stores/backend/viewsV2.js b/packages/builder/src/stores/backend/viewsV2.js index 8b7b1d876c..511545adcb 100644 --- a/packages/builder/src/stores/backend/viewsV2.js +++ b/packages/builder/src/stores/backend/viewsV2.js @@ -54,23 +54,20 @@ export function createViewsV2Store() { } const save = async view => { - // No dedicated save endpoint at this time - // const savedView = await API.saveView(view) - // - // // Update tables - // tables.update(state => { - // const table = state.list.find(table => table._id === view.tableId) - // if (table) { - // if (view.originalName) { - // delete table.views[view.originalName] - // } - // table.views[view.name] = savedView - // } - // return { ...state } - // }) - } + const savedView = await API.viewV2.update(view) - const replace = (id, view) => {} + // Update tables + tables.update(state => { + const table = state.list.find(table => table._id === view.tableId) + if (table) { + if (view.originalName) { + delete table.views[view.originalName] + } + table.views[view.name] = savedView + } + return { ...state } + }) + } return { subscribe: derivedStore.subscribe, @@ -78,7 +75,6 @@ export function createViewsV2Store() { delete: deleteView, create, save, - replace, } } diff --git a/packages/frontend-core/src/components/grid/stores/sort.js b/packages/frontend-core/src/components/grid/stores/sort.js index 098e126930..3beecd2c2a 100644 --- a/packages/frontend-core/src/components/grid/stores/sort.js +++ b/packages/frontend-core/src/components/grid/stores/sort.js @@ -16,8 +16,7 @@ export const createStores = context => { } export const initialise = context => { - const { sort, initialSortColumn, initialSortOrder, table, datasource } = - context + const { sort, initialSortColumn, initialSortOrder } = context // Reset sort when initial sort props change initialSortColumn.subscribe(newSortColumn => { From 19ca7e4a0a70241a70966de49171ae3e066c27e1 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 3 Aug 2023 13:40:36 +0100 Subject: [PATCH 048/245] Fix issue with viewV2 renaming --- packages/builder/src/stores/backend/viewsV2.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/stores/backend/viewsV2.js b/packages/builder/src/stores/backend/viewsV2.js index 511545adcb..c8f38ff5e5 100644 --- a/packages/builder/src/stores/backend/viewsV2.js +++ b/packages/builder/src/stores/backend/viewsV2.js @@ -54,7 +54,8 @@ export function createViewsV2Store() { } const save = async view => { - const savedView = await API.viewV2.update(view) + const res = await API.viewV2.update(view) + const savedView = res?.data // Update tables tables.update(state => { From 46f16764dbcbb578a77cddcf9ad8a186351a831e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 4 Aug 2023 11:47:06 +0100 Subject: [PATCH 049/245] Fix initial sorting state for tables and memoize sorting store to avoid loops --- .../src/components/grid/stores/rows.js | 5 ----- .../src/components/grid/stores/sort.js | 5 +++-- .../src/components/grid/stores/table.js | 18 +++++++++++++++++- .../src/components/grid/stores/viewV2.js | 14 ++++---------- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 96194c6fc5..d876b5f8df 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -151,11 +151,6 @@ export const createActions = context => { // Reset state properties when dataset changes if (!$instanceLoaded || resetRows) { definition.set($fetch.definition) - - // sort.set({ - // column: $fetch.sortColumn, - // order: $fetch.sortOrder, - // }) } // Reset scroll state when data changes diff --git a/packages/frontend-core/src/components/grid/stores/sort.js b/packages/frontend-core/src/components/grid/stores/sort.js index 3beecd2c2a..689b278874 100644 --- a/packages/frontend-core/src/components/grid/stores/sort.js +++ b/packages/frontend-core/src/components/grid/stores/sort.js @@ -1,11 +1,12 @@ -import { writable, get } from "svelte/store" +import { get } from "svelte/store" +import { memo } from "../../../utils" export const createStores = context => { const { props } = context const $props = get(props) // Initialise to default props - const sort = writable({ + const sort = memo({ column: $props.initialSortColumn, order: $props.initialSortOrder || "ascending", }) diff --git a/packages/frontend-core/src/components/grid/stores/table.js b/packages/frontend-core/src/components/grid/stores/table.js index d79696c263..5c3d2c9ef2 100644 --- a/packages/frontend-core/src/components/grid/stores/table.js +++ b/packages/frontend-core/src/components/grid/stores/table.js @@ -54,7 +54,7 @@ export const createActions = context => { } export const initialise = context => { - const { datasource, fetch, filter, sort } = context + const { datasource, fetch, filter, sort, definition } = context // Update fetch when filter changes filter.subscribe($filter => { @@ -74,4 +74,20 @@ export const initialise = context => { }) } }) + + // Ensure sorting UI reflects the fetch state whenever we reset the fetch, + // which triggers a new definition + definition.subscribe(() => { + if (get(datasource)?.type === "table") { + const $fetch = get(fetch) + if (!$fetch) { + return + } + const { sortColumn, sortOrder } = get($fetch) + sort.set({ + column: sortColumn, + order: sortOrder, + }) + } + }) } diff --git a/packages/frontend-core/src/components/grid/stores/viewV2.js b/packages/frontend-core/src/components/grid/stores/viewV2.js index 35e447d8fa..b2d80acd1a 100644 --- a/packages/frontend-core/src/components/grid/stores/viewV2.js +++ b/packages/frontend-core/src/components/grid/stores/viewV2.js @@ -82,16 +82,10 @@ export const initialise = context => { if (!$definition || get(datasource)?.type !== "viewV2") { return } - const $sort = get(sort) - if ( - $definition.sort?.field !== $sort?.column || - $definition.sort?.order !== $sort?.order - ) { - sort.set({ - column: $definition.sort?.field, - order: $definition.sort?.order, - }) - } + sort.set({ + column: $definition.sort?.field, + order: $definition.sort?.order, + }) }) // When sorting changes, ensure view definition is kept up to date From cd2231630f7d248dd249d99a8797989b76423266 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 4 Aug 2023 14:54:45 +0100 Subject: [PATCH 050/245] Improve updating of viewV2 state and restore grid<>builder sync for datasource definitions --- .../backend/DataTable/ViewV2DataTable.svelte | 3 +- .../builder/src/stores/backend/viewsV2.js | 73 ++++++++++++------- .../src/components/grid/stores/datasource.js | 2 +- 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte index 21b3e1c291..859ce11280 100644 --- a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte @@ -13,7 +13,8 @@ } const handleGridViewUpdate = async e => { - viewsV2.replace(id, e.detail) + console.log("update") + viewsV2.replaceView(id, e.detail) } diff --git a/packages/builder/src/stores/backend/viewsV2.js b/packages/builder/src/stores/backend/viewsV2.js index c8f38ff5e5..6fa3b52bb9 100644 --- a/packages/builder/src/stores/backend/viewsV2.js +++ b/packages/builder/src/stores/backend/viewsV2.js @@ -1,4 +1,4 @@ -import { writable, derived } from "svelte/store" +import { writable, derived, get } from "svelte/store" import { tables } from "./" import { API } from "api" @@ -30,44 +30,64 @@ export function createViewsV2Store() { const deleteView = async view => { await API.viewV2.delete(view.id) - - // Update tables - tables.update(state => { - const table = state.list.find(table => table._id === view.tableId) - delete table.views[view.name] - return { ...state } - }) + replaceView(view.id, null) } const create = async view => { const savedViewResponse = await API.viewV2.create(view) const savedView = savedViewResponse.data - - // Update tables - tables.update(state => { - const table = state.list.find(table => table._id === view.tableId) - table.views[view.name] = savedView - return { ...state } - }) - + replaceView(savedView.id, savedView) return savedView } const save = async view => { const res = await API.viewV2.update(view) const savedView = res?.data + replaceView(view.id, savedView) + } - // Update tables - tables.update(state => { - const table = state.list.find(table => table._id === view.tableId) - if (table) { - if (view.originalName) { - delete table.views[view.originalName] - } - table.views[view.name] = savedView - } - return { ...state } + // Handles external updates of tables + const replaceView = (viewId, view) => { + console.log("replace", viewId, view) + if (!viewId) { + return + } + const existingView = get(derivedStore).list.find(view => view.id === viewId) + const tableIndex = get(tables).list.findIndex(table => { + return table._id === view?.tableId || table._id === existingView?.tableId }) + if (tableIndex === -1) { + return + } + + // Handle deletion + if (!view) { + tables.update(state => { + delete state.list[tableIndex].views[existingView.name] + return state + }) + return + } + + // Add new view + if (!existingView) { + tables.update(state => { + state.list[tableIndex].views[view.name] = view + return state + }) + } + + // Update existing view + else { + tables.update(state => { + // Remove old view + delete state.list[tableIndex].views[existingView.name] + + // Add new view + state.list[tableIndex].views[view.name] = view + return state + }) + } } return { @@ -76,6 +96,7 @@ export function createViewsV2Store() { delete: deleteView, create, save, + replaceView, } } diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.js index 5ace5ceef8..d5017aa8d1 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -80,7 +80,7 @@ export const createActions = context => { // Broadcast change to external state can be updated, as this change // will not be received by the builder websocket because we caused it ourselves - dispatch("updatedefinition", newDefinition) + dispatch("updatedatasource", newDefinition) } // Adds a row to the datasource From 0a87e3502e2ad720d49facf102f04f062cd1d74d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 4 Aug 2023 14:58:12 +0100 Subject: [PATCH 051/245] Fix other user selection state for viewV2s --- .../components/backend/TableNavigator/TableNavigator.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte b/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte index f1965117ae..056a36c4a7 100644 --- a/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte +++ b/packages/builder/src/components/backend/TableNavigator/TableNavigator.svelte @@ -58,7 +58,8 @@ $goto(`./view/v1/${encodeURIComponent(name)}`) } }} - selectedBy={$userSelectedResourceMap[name]} + selectedBy={$userSelectedResourceMap[name] || + $userSelectedResourceMap[view.id]} > From 938a5a445f87b32cc0672ba3feab58c3eb917eba Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 4 Aug 2023 15:04:58 +0100 Subject: [PATCH 052/245] Add multi dev collab for viewV2 definitions --- packages/server/src/api/controllers/view/viewsV2.ts | 12 +++++++++++- packages/server/src/sdk/app/views/index.ts | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/server/src/api/controllers/view/viewsV2.ts b/packages/server/src/api/controllers/view/viewsV2.ts index 226b7201b8..ba97fff8b6 100644 --- a/packages/server/src/api/controllers/view/viewsV2.ts +++ b/packages/server/src/api/controllers/view/viewsV2.ts @@ -8,6 +8,7 @@ import { ViewV2, RequiredKeys, } from "@budibase/types" +import { builderSocket } from "../../../websockets" async function parseSchemaUI(ctx: Ctx, view: CreateViewRequest) { if (!view.schema) { @@ -86,6 +87,9 @@ export async function create(ctx: Ctx) { ctx.body = { data: result, } + + const table = await sdk.tables.getTable(tableId) + builderSocket?.emitTableUpdate(ctx, table) } export async function update(ctx: Ctx) { @@ -118,11 +122,17 @@ export async function update(ctx: Ctx) { ctx.body = { data: result, } + + const table = await sdk.tables.getTable(tableId) + builderSocket?.emitTableUpdate(ctx, table) } export async function remove(ctx: Ctx) { const { viewId } = ctx.params - await sdk.views.remove(viewId) + const view = await sdk.views.remove(viewId) ctx.status = 204 + + const table = await sdk.tables.getTable(view.tableId) + builderSocket?.emitTableUpdate(ctx, table) } diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index d87715f49b..aafaab3a36 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -56,7 +56,7 @@ export function isV2(view: View | ViewV2): view is ViewV2 { return (view as ViewV2).version === 2 } -export async function remove(viewId: string): Promise { +export async function remove(viewId: string): Promise { const db = context.getAppDB() const view = await get(viewId) @@ -67,6 +67,7 @@ export async function remove(viewId: string): Promise { delete table.views![view?.name] await db.put(table) + return view } export function enrichSchema(view: View | ViewV2, tableSchema: TableSchema) { From ffe82e18e993b7a402cfccdd6e9b319898bced77 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 4 Aug 2023 15:07:44 +0100 Subject: [PATCH 053/245] Remove log --- packages/builder/src/stores/backend/viewsV2.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/builder/src/stores/backend/viewsV2.js b/packages/builder/src/stores/backend/viewsV2.js index 6fa3b52bb9..dcc2c27152 100644 --- a/packages/builder/src/stores/backend/viewsV2.js +++ b/packages/builder/src/stores/backend/viewsV2.js @@ -48,7 +48,6 @@ export function createViewsV2Store() { // Handles external updates of tables const replaceView = (viewId, view) => { - console.log("replace", viewId, view) if (!viewId) { return } From 8a9db6d8deef8d27ec8909e3ef038e781a809951 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 4 Aug 2023 15:15:43 +0100 Subject: [PATCH 054/245] Remove log --- .../src/components/backend/DataTable/ViewV2DataTable.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte index 859ce11280..e4e1adda36 100644 --- a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte @@ -13,7 +13,6 @@ } const handleGridViewUpdate = async e => { - console.log("update") viewsV2.replaceView(id, e.detail) } From ef4ff87d8a3aacd73d23ef61f43b5a175c47a2b7 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 4 Aug 2023 15:17:30 +0100 Subject: [PATCH 055/245] Remove filter button on views for now --- .../src/components/backend/DataTable/ViewV2DataTable.svelte | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte index e4e1adda36..789f7d730a 100644 --- a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte @@ -26,9 +26,7 @@ showAvatars={false} on:updatedatasource={handleGridViewUpdate} > - - - + From dd4b3047369d7b0d8fdaf5d0ab03eaa1303ac854 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 4 Aug 2023 15:20:30 +0100 Subject: [PATCH 056/245] Fix creating views --- .../buttons/grid/GridCreateViewButton.svelte | 4 ++-- .../GridCreateViewModal.svelte} | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) rename packages/builder/src/components/backend/DataTable/modals/{CreateViewModal.svelte => grid/GridCreateViewModal.svelte} (82%) diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridCreateViewButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridCreateViewButton.svelte index 33c416d7ef..3244ce3277 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridCreateViewButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridCreateViewButton.svelte @@ -1,7 +1,7 @@ - + {/if}
diff --git a/packages/frontend-core/src/components/grid/stores/pagination.js b/packages/frontend-core/src/components/grid/stores/pagination.js index c6a856e229..1dbea6e0d2 100644 --- a/packages/frontend-core/src/components/grid/stores/pagination.js +++ b/packages/frontend-core/src/components/grid/stores/pagination.js @@ -1,4 +1,4 @@ -import { derived } from "svelte/store" +import { derived, get } from "svelte/store" export const initialise = context => { const { scrolledRowCount, rows, visualRowCapacity } = context @@ -11,13 +11,12 @@ export const initialise = context => { [scrolledRowCount, rowCount, visualRowCapacity], ([$scrolledRowCount, $rowCount, $visualRowCapacity]) => { return Math.max(0, $rowCount - $scrolledRowCount - $visualRowCapacity) - }, - 100 + } ) // Fetch next page when fewer than 25 remaining rows to scroll remainingRows.subscribe(remaining => { - if (remaining < 25) { + if (remaining < 25 && get(rowCount)) { rows.actions.loadNextPage() } }) diff --git a/packages/frontend-core/src/components/grid/stores/sort.js b/packages/frontend-core/src/components/grid/stores/sort.js index 689b278874..9b2dbfb8df 100644 --- a/packages/frontend-core/src/components/grid/stores/sort.js +++ b/packages/frontend-core/src/components/grid/stores/sort.js @@ -1,4 +1,4 @@ -import { get } from "svelte/store" +import { derived, get } from "svelte/store" import { memo } from "../../../utils" export const createStores = context => { @@ -17,13 +17,34 @@ export const createStores = context => { } export const initialise = context => { - const { sort, initialSortColumn, initialSortOrder } = context + const { sort, initialSortColumn, initialSortOrder, definition } = context // Reset sort when initial sort props change initialSortColumn.subscribe(newSortColumn => { sort.update(state => ({ ...state, column: newSortColumn })) }) initialSortOrder.subscribe(newSortOrder => { - sort.update(state => ({ ...state, order: newSortOrder })) + sort.update(state => ({ ...state, order: newSortOrder || "ascending" })) + }) + + // Derive if the current sort column exists in the schema + const sortColumnExists = derived( + [sort, definition], + ([$sort, $definition]) => { + if (!$sort?.column) { + return true + } + return $definition?.schema?.[$sort.column] != null + } + ) + + // Clear sort state if our sort column does not exist + sortColumnExists.subscribe(exists => { + if (!exists) { + sort.set({ + column: null, + order: "ascending", + }) + } }) } diff --git a/packages/frontend-core/src/components/grid/stores/table.js b/packages/frontend-core/src/components/grid/stores/table.js index 5c33e8171d..6c877e353a 100644 --- a/packages/frontend-core/src/components/grid/stores/table.js +++ b/packages/frontend-core/src/components/grid/stores/table.js @@ -59,51 +59,55 @@ export const createActions = context => { } export const initialise = context => { - const { datasource, fetch, filter, sort, definition } = context + const { datasource, fetch, filter, sort, table } = context - // Wipe filter whenever table ID changes to avoid using stale filters + // 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 table datasources datasource.subscribe($datasource => { - if ($datasource?.type !== "table") { + // Clear previous subscriptions + unsubscribers?.forEach(unsubscribe => unsubscribe()) + unsubscribers = [] + if (!table.actions.isDatasourceValid($datasource)) { return } + + // Wipe state filter.set([]) - }) - - // Update fetch when filter changes - filter.subscribe($filter => { - if (get(datasource)?.type !== "table") { - return - } - get(fetch)?.update({ - filter: $filter, - }) - }) - - // Update fetch when sorting changes - sort.subscribe($sort => { - if (get(datasource)?.type !== "table") { - return - } - get(fetch)?.update({ - sortOrder: $sort.order, - sortColumn: $sort.column, - }) - }) - - // Ensure sorting UI reflects the fetch state whenever we reset the fetch, - // which triggers a new definition - definition.subscribe(() => { - if (get(datasource)?.type !== "table") { - return - } - const $fetch = get(fetch) - if (!$fetch) { - return - } - const { sortColumn, sortOrder } = get($fetch) sort.set({ - column: sortColumn, - order: sortOrder, + column: null, + order: "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?.tableId !== $datasource.tableId) { + 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?.tableId !== $datasource.tableId) { + return + } + $fetch.update({ + sortOrder: $sort.order || "ascending", + sortColumn: $sort.column, + }) + }) + ) }) } diff --git a/packages/frontend-core/src/components/grid/stores/viewV2.js b/packages/frontend-core/src/components/grid/stores/viewV2.js index b24bf56c98..f93dc57b69 100644 --- a/packages/frontend-core/src/components/grid/stores/viewV2.js +++ b/packages/frontend-core/src/components/grid/stores/viewV2.js @@ -69,62 +69,91 @@ export const createActions = context => { } export const initialise = context => { - const { definition, datasource, sort, rows, filter, subscribe } = context + const { definition, datasource, sort, rows, filter, subscribe, viewV2 } = + context - // Keep sort and filter state in line with the view definition - definition.subscribe($definition => { - if (!$definition || get(datasource)?.type !== "viewV2") { + // 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 (!viewV2.actions.isDatasourceValid($datasource)) { return } + + // Reset state for new view + filter.set([]) sort.set({ - column: $definition.sort?.field, - order: $definition.sort?.order, + column: null, + order: "ascending", }) - filter.set($definition.query || []) - }) - // When sorting changes, ensure view definition is kept up to date - sort.subscribe(async $sort => { - const $view = get(definition) - if (!$view || get(datasource)?.type !== "viewV2") { - return - } - if ( - $sort?.column !== $view.sort?.field || - $sort?.order !== $view.sort?.order - ) { - await datasource.actions.saveDefinition({ - ...$view, - sort: { - field: $sort.column, - order: $sort.order, - }, + // Keep sort and filter state in line with the view definition + unsubscribers.push( + definition.subscribe($definition => { + if ($definition?.id !== $datasource.id) { + return + } + sort.set({ + column: $definition.sort?.field, + order: $definition.sort?.order || "ascending", + }) + filter.set($definition.query || []) }) - await rows.actions.refreshData() - } - }) + ) - // When filters change, ensure view definition is kept up to date - filter.subscribe(async $filter => { - const $view = get(definition) - if (!$view || get(datasource)?.type !== "viewV2") { - return - } - if (JSON.stringify($filter) !== JSON.stringify($view.query)) { - await datasource.actions.saveDefinition({ - ...$view, - query: $filter, + // When sorting changes, ensure view definition is kept up to date + unsubscribers.push( + sort.subscribe(async $sort => { + // Ensure we're updating the correct view + const $view = get(definition) + if ($view?.id !== $datasource.id) { + return + } + if ( + $sort?.column !== $view.sort?.field || + $sort?.order !== $view.sort?.order + ) { + await datasource.actions.saveDefinition({ + ...$view, + sort: { + field: $sort.column, + order: $sort.order || "ascending", + }, + }) + await rows.actions.refreshData() + } }) - await rows.actions.refreshData() - } - }) + ) - // When hidden we show columns, we need to refresh data in order to fetch - // values for those columns - subscribe("show-column", async () => { - if (get(datasource)?.type !== "viewV2") { - return - } - await rows.actions.refreshData() + // When filters change, ensure view definition is kept up to date + unsubscribers?.push( + filter.subscribe(async $filter => { + // Ensure we're updating the correct view + const $view = get(definition) + if ($view?.id !== $datasource.id) { + return + } + if (JSON.stringify($filter) !== JSON.stringify($view.query)) { + await datasource.actions.saveDefinition({ + ...$view, + query: $filter, + }) + await rows.actions.refreshData() + } + }) + ) + + // When hidden we show columns, we need to refresh data in order to fetch + // values for those columns + unsubscribers.push( + subscribe("show-column", async () => { + await rows.actions.refreshData() + }) + ) }) } diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js index 28fa68afe9..cd12535ddc 100644 --- a/packages/frontend-core/src/fetch/DataFetch.js +++ b/packages/frontend-core/src/fetch/DataFetch.js @@ -110,6 +110,17 @@ export default class DataFetch { return this.derivedStore.subscribe } + /** + * Gets the default sort column for this datasource + */ + getDefaultSortColumn(definition, schema) { + if (definition?.primaryDisplay && schema[definition.primaryDisplay]) { + return definition.primaryDisplay + } else { + return Object.keys(schema)[0] + } + } + /** * Fetches a fresh set of data from the server, resetting pagination */ @@ -118,12 +129,6 @@ export default class DataFetch { // Fetch datasource definition and extract sort properties if configured const definition = await this.getDefinition(datasource) - if (definition?.sort?.field) { - this.options.sortColumn = definition.sort.field - } - if (definition?.sort?.order) { - this.options.sortOrder = definition.sort.order - } // Determine feature flags const features = this.determineFeatureFlags(definition) @@ -140,32 +145,32 @@ export default class DataFetch { return } - // If no sort order, default to descending - if (!this.options.sortOrder) { + // If an invalid sort column is specified, delete it + if (this.options.sortColumn && !schema[this.options.sortColumn]) { + this.options.sortColumn = null + } + + // If no sort column, get the default column for this datasource + if (!this.options.sortColumn) { + this.options.sortColumn = this.getDefaultSortColumn(definition, schema) + } + + // If we don't have a sort column specified then just ensure we don't set + // any sorting params + if (!this.options.sortColumn) { this.options.sortOrder = "ascending" - } + this.options.sortType = null + } else { + // Otherwise determine what sort type to use base on sort column + const type = schema?.[this.options.sortColumn]?.type + this.options.sortType = + type === "number" || type === "bigint" ? "number" : "string" - // If no sort column, or an invalid sort column is provided, use the primary - // display and fallback to first column - const sortValid = this.options.sortColumn && schema[this.options.sortColumn] - if (!sortValid) { - let newSortColumn - if (definition?.primaryDisplay && schema[definition.primaryDisplay]) { - newSortColumn = definition.primaryDisplay - } else { - newSortColumn = Object.keys(schema)[0] + // If no sort order, default to ascending + if (!this.options.sortOrder) { + this.options.sortOrder = "ascending" } - this.options.sortColumn = newSortColumn } - const { sortOrder, sortColumn } = this.options - - // Determine what sort type to use - let sortType = "string" - if (sortColumn) { - const type = schema?.[sortColumn]?.type - sortType = type === "number" || type === "bigint" ? "number" : "string" - } - this.options.sortType = sortType // Build the lucene query let query = this.options.query @@ -182,8 +187,6 @@ export default class DataFetch { loading: true, cursors: [], cursor: null, - sortOrder, - sortColumn, })) // Actually fetch data @@ -351,6 +354,14 @@ export default class DataFetch { const entries = Object.entries(newOptions || {}) for (let [key, value] of entries) { if (JSON.stringify(value) !== JSON.stringify(this.options[key])) { + console.log( + key, + "is different", + "new", + value, + "vs old", + this.options[key] + ) refresh = true break } diff --git a/packages/frontend-core/src/fetch/QueryFetch.js b/packages/frontend-core/src/fetch/QueryFetch.js index 456abaec79..6420893515 100644 --- a/packages/frontend-core/src/fetch/QueryFetch.js +++ b/packages/frontend-core/src/fetch/QueryFetch.js @@ -29,6 +29,10 @@ export default class QueryFetch extends DataFetch { } } + getDefaultSortColumn() { + return null + } + async getData() { const { datasource, limit, paginate } = this.options const { supportsPagination } = this.features diff --git a/packages/frontend-core/src/fetch/ViewV2Fetch.js b/packages/frontend-core/src/fetch/ViewV2Fetch.js index d8b96df7ae..b9eaf4bdf7 100644 --- a/packages/frontend-core/src/fetch/ViewV2Fetch.js +++ b/packages/frontend-core/src/fetch/ViewV2Fetch.js @@ -4,9 +4,6 @@ import { get } from "svelte/store" export default class ViewV2Fetch extends DataFetch { determineFeatureFlags() { return { - // The API does not actually support dynamic filtering, but since views - // have filters built in we don't want to perform client side filtering - // which would happen if we marked this as false supportsSearch: true, supportsSort: true, supportsPagination: true, @@ -33,18 +30,23 @@ export default class ViewV2Fetch extends DataFetch { } } + getDefaultSortColumn() { + return null + } + async getData() { const { datasource, limit, sortColumn, sortOrder, sortType, paginate } = this.options - const { cursor } = get(this.store) + const { cursor, query } = get(this.store) try { const res = await this.API.viewV2.fetch({ viewId: datasource.id, + query, paginate, limit, bookmark: cursor, sort: sortColumn, - sortOrder, + sortOrder: sortOrder?.toLowerCase(), sortType, }) return { diff --git a/packages/server/src/api/controllers/row/views.ts b/packages/server/src/api/controllers/row/views.ts index 455a4c0aa2..578c775a55 100644 --- a/packages/server/src/api/controllers/row/views.ts +++ b/packages/server/src/api/controllers/row/views.ts @@ -6,9 +6,11 @@ import { SearchViewRowRequest, RequiredKeys, SearchParams, + SearchFilters, } from "@budibase/types" import { dataFilters } from "@budibase/shared-core" import sdk from "../../../sdk" +import { db } from "@budibase/backend-core" export async function searchView( ctx: UserCtx @@ -19,15 +21,37 @@ export async function searchView( if (!view) { ctx.throw(404, `View ${viewId} not found`) } - if (view.version !== 2) { ctx.throw(400, `This method only supports viewsV2`) } const viewFields = Object.keys(view.schema || {}) - const { body } = ctx.request - const query = dataFilters.buildLuceneQuery(view.query || []) + + // Enrich saved query with ephemeral query params. + // We prevent searching on any fields that are saved as part of the query, as + // that could let users find rows they should not be allowed to access. + let query = dataFilters.buildLuceneQuery(view.query || []) + if (body.query) { + // Extract existing fields + const existingFields = + view.query + ?.filter(filter => filter.field) + .map(filter => db.removeKeyNumbering(filter.field)) || [] + + // Prevent using an "OR" search + delete body.query.allOr + + // Carry over filters for unused fields + Object.keys(body.query).forEach(key => { + const operator = key as keyof Omit + Object.keys(body.query[operator] || {}).forEach(field => { + if (!existingFields.includes(db.removeKeyNumbering(field))) { + query[operator]![field] = body.query[operator]![field] + } + }) + }) + } const searchOptions: RequiredKeys & RequiredKeys> = { diff --git a/packages/types/src/api/web/app/rows.ts b/packages/types/src/api/web/app/rows.ts index 2b51c7b203..a99ef0e837 100644 --- a/packages/types/src/api/web/app/rows.ts +++ b/packages/types/src/api/web/app/rows.ts @@ -16,7 +16,13 @@ export interface SearchRowRequest extends Omit {} export interface SearchViewRowRequest extends Pick< SearchRowRequest, - "sort" | "sortOrder" | "sortType" | "limit" | "bookmark" | "paginate" + | "sort" + | "sortOrder" + | "sortType" + | "limit" + | "bookmark" + | "paginate" + | "query" > {} export interface SearchRowResponse { From 729b93532b0fd9a65af497a914e350788f743dac Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 21 Aug 2023 12:01:17 +0100 Subject: [PATCH 098/245] Prevent overriding onEmptyFilter behaviour for views --- packages/server/src/api/controllers/row/views.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/server/src/api/controllers/row/views.ts b/packages/server/src/api/controllers/row/views.ts index 578c775a55..36a0b588b6 100644 --- a/packages/server/src/api/controllers/row/views.ts +++ b/packages/server/src/api/controllers/row/views.ts @@ -39,12 +39,16 @@ export async function searchView( ?.filter(filter => filter.field) .map(filter => db.removeKeyNumbering(filter.field)) || [] - // Prevent using an "OR" search + // Delete extraneous search params that cannot be overridden delete body.query.allOr + delete body.query.onEmptyFilter // Carry over filters for unused fields Object.keys(body.query).forEach(key => { - const operator = key as keyof Omit + const operator = key as keyof Omit< + SearchFilters, + "allOr" | "onEmptyFilter" + > Object.keys(body.query[operator] || {}).forEach(field => { if (!existingFields.includes(db.removeKeyNumbering(field))) { query[operator]![field] = body.query[operator]![field] From cad624a9a41f7b2b96821bf70aa21755bbd0bded Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 21 Aug 2023 12:50:16 +0100 Subject: [PATCH 099/245] Merge Commit --- packages/bbui/src/Drawer/Drawer.svelte | 7 +++++++ packages/bbui/src/Form/Core/Combobox.svelte | 15 ++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/bbui/src/Drawer/Drawer.svelte b/packages/bbui/src/Drawer/Drawer.svelte index 4ff4df854b..421d12615f 100644 --- a/packages/bbui/src/Drawer/Drawer.svelte +++ b/packages/bbui/src/Drawer/Drawer.svelte @@ -4,6 +4,8 @@ import Body from "../Typography/Body.svelte" import Heading from "../Typography/Heading.svelte" import { setContext } from "svelte" + import { createEventDispatcher } from "svelte" + import { generate } from "shortid" export let title export let fillWidth @@ -11,13 +13,17 @@ export let width = "calc(100% - 626px)" export let headless = false + const dispatch = createEventDispatcher() + let visible = false + let drawerId = generate() export function show() { if (visible) { return } visible = true + dispatch("drawerShow", drawerId) } export function hide() { @@ -25,6 +31,7 @@ return } visible = false + dispatch("drawerHide", drawerId) } setContext("drawer-actions", { diff --git a/packages/bbui/src/Form/Core/Combobox.svelte b/packages/bbui/src/Form/Core/Combobox.svelte index b68a24d8db..b1b264a9b7 100644 --- a/packages/bbui/src/Form/Core/Combobox.svelte +++ b/packages/bbui/src/Form/Core/Combobox.svelte @@ -2,8 +2,8 @@ import "@spectrum-css/inputgroup/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/menu/dist/index-vars.css" - import { fly } from "svelte/transition" import { createEventDispatcher } from "svelte" + import clickOutside from "../../Actions/click_outside" export let value = null export let id = null @@ -80,10 +80,11 @@ {#if open} -
(open = false)} />
{ + open = false + }} >
    {#if options && Array.isArray(options)} @@ -125,14 +126,6 @@ .spectrum-Textfield-input { width: 0; } - .overlay { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - z-index: 999; - } .spectrum-Popover { max-height: 240px; width: 100%; From 5abab4cb625a11b7b04fd00997e1dc4ed1ccf1cb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 21 Aug 2023 16:11:37 +0100 Subject: [PATCH 100/245] Update grids to allow filtering and sorting in client lib with V2 views --- .../src/components/grid/stores/table.js | 17 ++- .../src/components/grid/stores/viewV2.js | 115 ++++++++++++------ 2 files changed, 94 insertions(+), 38 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/table.js b/packages/frontend-core/src/components/grid/stores/table.js index 6c877e353a..a881f45b5f 100644 --- a/packages/frontend-core/src/components/grid/stores/table.js +++ b/packages/frontend-core/src/components/grid/stores/table.js @@ -59,7 +59,16 @@ export const createActions = context => { } export const initialise = context => { - const { datasource, fetch, filter, sort, table } = context + const { + datasource, + fetch, + filter, + sort, + table, + initialFilter, + initialSortColumn, + initialSortOrder, + } = context // Keep a list of subscriptions so that we can clear them when the datasource // config changes @@ -75,10 +84,10 @@ export const initialise = context => { } // Wipe state - filter.set([]) + filter.set(get(initialFilter)) sort.set({ - column: null, - order: "ascending", + column: get(initialSortColumn), + order: get(initialSortOrder) || "ascending", }) // Update fetch when filter changes diff --git a/packages/frontend-core/src/components/grid/stores/viewV2.js b/packages/frontend-core/src/components/grid/stores/viewV2.js index f93dc57b69..59bf747330 100644 --- a/packages/frontend-core/src/components/grid/stores/viewV2.js +++ b/packages/frontend-core/src/components/grid/stores/viewV2.js @@ -69,8 +69,20 @@ export const createActions = context => { } export const initialise = context => { - const { definition, datasource, sort, rows, filter, subscribe, viewV2 } = - context + const { + definition, + datasource, + sort, + rows, + filter, + subscribe, + viewV2, + initialFilter, + initialSortColumn, + initialSortOrder, + config, + fetch, + } = context // Keep a list of subscriptions so that we can clear them when the datasource // config changes @@ -86,10 +98,10 @@ export const initialise = context => { } // Reset state for new view - filter.set([]) + filter.set(get(initialFilter)) sort.set({ - column: null, - order: "ascending", + column: get(initialSortColumn), + order: get(initialSortOrder) || "ascending", }) // Keep sort and filter state in line with the view definition @@ -98,34 +110,55 @@ export const initialise = context => { if ($definition?.id !== $datasource.id) { return } - sort.set({ - column: $definition.sort?.field, - order: $definition.sort?.order || "ascending", - }) - filter.set($definition.query || []) + // Only override sorting if we don't have an initial sort column + if (!get(initialSortColumn)) { + sort.set({ + column: $definition.sort?.field, + order: $definition.sort?.order || "ascending", + }) + } + // Only override filter state if we don't have an initial filter + if (!get(initialFilter)) { + filter.set($definition.query) + } }) ) // When sorting changes, ensure view definition is kept up to date unsubscribers.push( sort.subscribe(async $sort => { - // Ensure we're updating the correct view - const $view = get(definition) - if ($view?.id !== $datasource.id) { - return + // If we can mutate schema then update the view definition + if (get(config).canSaveSchema) { + // Ensure we're updating the correct view + const $view = get(definition) + if ($view?.id !== $datasource.id) { + return + } + if ( + $sort?.column !== $view.sort?.field || + $sort?.order !== $view.sort?.order + ) { + await datasource.actions.saveDefinition({ + ...$view, + sort: { + field: $sort.column, + order: $sort.order || "ascending", + }, + }) + await rows.actions.refreshData() + } } - if ( - $sort?.column !== $view.sort?.field || - $sort?.order !== $view.sort?.order - ) { - await datasource.actions.saveDefinition({ - ...$view, - sort: { - field: $sort.column, - order: $sort.order || "ascending", - }, + // Otherwise just update the fetch + else { + // Ensure we're updating the correct fetch + const $fetch = get(fetch) + if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) { + return + } + $fetch.update({ + sortOrder: $sort.order || "ascending", + sortColumn: $sort.column, }) - await rows.actions.refreshData() } }) ) @@ -133,17 +166,31 @@ export const initialise = context => { // When filters change, ensure view definition is kept up to date unsubscribers?.push( filter.subscribe(async $filter => { - // Ensure we're updating the correct view - const $view = get(definition) - if ($view?.id !== $datasource.id) { - return + // If we can mutate schema then update the view definition + if (get(config).canSaveSchema) { + // Ensure we're updating the correct view + const $view = get(definition) + if ($view?.id !== $datasource.id) { + return + } + if (JSON.stringify($filter) !== JSON.stringify($view.query)) { + await datasource.actions.saveDefinition({ + ...$view, + query: $filter, + }) + await rows.actions.refreshData() + } } - if (JSON.stringify($filter) !== JSON.stringify($view.query)) { - await datasource.actions.saveDefinition({ - ...$view, - query: $filter, + // Otherwise just update the fetch + else { + // Ensure we're updating the correct fetch + const $fetch = get(fetch) + if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) { + return + } + $fetch.update({ + filter: $filter, }) - await rows.actions.refreshData() } }) ) From 59559ee93c4e2b98cd18f1db891f785af9e43bfe Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 21 Aug 2023 16:53:09 +0100 Subject: [PATCH 101/245] Remove log and account for onEmptyFilter in grid button --- .../backend/DataTable/buttons/TableFilterButton.svelte | 9 ++------- packages/frontend-core/src/fetch/DataFetch.js | 8 -------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte index b96738ab1a..23f6d1dea1 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/TableFilterButton.svelte @@ -15,6 +15,7 @@ $: tempValue = filters || [] $: schemaFields = Object.values(schema || {}) $: text = getText(filters) + $: selected = tempValue.filter(x => !x.onEmptyFilter)?.length > 0 const getText = filters => { const count = filters?.filter(filter => filter.field)?.length @@ -22,13 +23,7 @@ } - 0} -> + {text} diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js index cd12535ddc..857072601e 100644 --- a/packages/frontend-core/src/fetch/DataFetch.js +++ b/packages/frontend-core/src/fetch/DataFetch.js @@ -354,14 +354,6 @@ export default class DataFetch { const entries = Object.entries(newOptions || {}) for (let [key, value] of entries) { if (JSON.stringify(value) !== JSON.stringify(this.options[key])) { - console.log( - key, - "is different", - "new", - value, - "vs old", - this.options[key] - ) refresh = true break } From 10cbf4f08a887d17a570715673b09a9c456fb100 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 21 Aug 2023 17:49:21 +0100 Subject: [PATCH 102/245] Update grid sorting settings to make order conditional on column --- packages/client/manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 7be23cda77..cc3100acdf 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -5583,7 +5583,8 @@ "label": "Sort order", "key": "initialSortOrder", "options": ["Ascending", "Descending"], - "defaultValue": "Ascending" + "defaultValue": "Ascending", + "dependsOn": "initialSortColumn" }, { "type": "select", From c7d1010ce316695de8a37d0aaaf92641fe2d8dad Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 21 Aug 2023 17:49:54 +0100 Subject: [PATCH 103/245] Fix resetting sort column when sort column doesn't exist in schema whenever schema is yet to be loaded --- packages/frontend-core/src/components/grid/stores/sort.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/sort.js b/packages/frontend-core/src/components/grid/stores/sort.js index 9b2dbfb8df..734a876eed 100644 --- a/packages/frontend-core/src/components/grid/stores/sort.js +++ b/packages/frontend-core/src/components/grid/stores/sort.js @@ -31,10 +31,10 @@ export const initialise = context => { const sortColumnExists = derived( [sort, definition], ([$sort, $definition]) => { - if (!$sort?.column) { + if (!$sort?.column || !$definition) { return true } - return $definition?.schema?.[$sort.column] != null + return $definition.schema?.[$sort.column] != null } ) From c936304410e44adad38091379a63a62b7540f4cb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 22 Aug 2023 11:31:25 +0100 Subject: [PATCH 104/245] Fix grid config store so that schema overrides work --- .../backend/DataTable/TableDataTable.svelte | 4 +-- .../components/grid/cells/HeaderCell.svelte | 1 - .../src/components/grid/stores/columns.js | 3 -- .../src/components/grid/stores/config.js | 36 ++++++++++--------- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/TableDataTable.svelte b/packages/builder/src/components/backend/DataTable/TableDataTable.svelte index 109c965271..58e0a0e691 100644 --- a/packages/builder/src/components/backend/DataTable/TableDataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/TableDataTable.svelte @@ -57,8 +57,8 @@ Use as display column diff --git a/packages/frontend-core/src/components/grid/stores/columns.js b/packages/frontend-core/src/components/grid/stores/columns.js index 8059bd98d7..09e25586fd 100644 --- a/packages/frontend-core/src/components/grid/stores/columns.js +++ b/packages/frontend-core/src/components/grid/stores/columns.js @@ -80,9 +80,6 @@ export const createActions = context => { // Updates the datasources primary display column const changePrimaryDisplay = async column => { - if (!get(config).canEditPrimaryDisplay) { - return - } return await datasource.actions.saveDefinition({ ...get(definition), primaryDisplay: column, diff --git a/packages/frontend-core/src/components/grid/stores/config.js b/packages/frontend-core/src/components/grid/stores/config.js index 46bff299d4..ae995b4ac7 100644 --- a/packages/frontend-core/src/components/grid/stores/config.js +++ b/packages/frontend-core/src/components/grid/stores/config.js @@ -1,8 +1,8 @@ import { derivedMemo } from "../../../utils" import { derived } from "svelte/store" -export const deriveStores = context => { - const { props, hasNonAutoColumn } = context +export const createStores = context => { + const { props } = context const getProp = prop => derivedMemo(props, $props => $props[prop]) // Derive and memoize some props so that we can react to them in isolation @@ -16,16 +16,27 @@ export const deriveStores = context => { const notifySuccess = getProp("notifySuccess") const notifyError = getProp("notifyError") + return { + datasource, + initialSortColumn, + initialSortOrder, + initialFilter, + fixedRowHeight, + schemaOverrides, + columnWhitelist, + notifySuccess, + notifyError, + } +} + +export const deriveStores = context => { + const { props, hasNonAutoColumn } = context + // Derive features const config = derived( [props, hasNonAutoColumn], ([$props, $hasNonAutoColumn]) => { - let config = { - ...$props, - - // Additional granular features which we don't expose as props - canEditPrimaryDisplay: $props.canEditColumns, - } + let config = { ...$props } // Disable some features if we're editing a view if ($props.datasource?.type === "viewV2") { @@ -43,14 +54,5 @@ export const deriveStores = context => { return { config, - datasource, - initialSortColumn, - initialSortOrder, - initialFilter, - fixedRowHeight, - schemaOverrides, - columnWhitelist, - notifySuccess, - notifyError, } } From eb7c12ba09b3817ca3c5fd63220d1533cb4ae701 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 22 Aug 2023 11:31:38 +0100 Subject: [PATCH 105/245] Prevent editing columns in grids in client apps --- packages/client/src/components/app/GridBlock.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index a7c3b8eef2..9bdea52124 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -50,6 +50,7 @@ canAddRows={allowAddRows} canEditRows={allowEditRows} canDeleteRows={allowDeleteRows} + canEditColumns={false} canExpandRows={false} canSaveSchema={false} showControls={false} From 51f6574942a8bc20dc50d4c7807fd3f8ef910344 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 22 Aug 2023 11:32:57 +0100 Subject: [PATCH 106/245] Fix issue with tab styles --- packages/bbui/src/Tabs/Tabs.svelte | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/bbui/src/Tabs/Tabs.svelte b/packages/bbui/src/Tabs/Tabs.svelte index 9c3d25a807..c94b396398 100644 --- a/packages/bbui/src/Tabs/Tabs.svelte +++ b/packages/bbui/src/Tabs/Tabs.svelte @@ -57,10 +57,8 @@ function calculateIndicatorLength() { if (!vertical) { width = $tab.info?.width + "px" - height = $tab.info?.height } else { height = $tab.info?.height + 4 + "px" - width = $tab.info?.width } } From e77644ce11ba0a4a9f8193b0989f4e651820b05f Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 22 Aug 2023 11:48:06 +0100 Subject: [PATCH 107/245] Lint --- .../components/backend/DataTable/ViewV2DataTable.svelte | 2 +- .../DataTable/buttons/grid/GridManageAccessButton.svelte | 8 ++++---- .../frontend-core/src/components/grid/stores/columns.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte index 5cb65e340f..0c6a0cca9a 100644 --- a/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/ViewV2DataTable.svelte @@ -4,7 +4,7 @@ import { API } from "api" import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte" import GridFilterButton from "components/backend/DataTable/buttons/grid/GridFilterButton.svelte" - import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte"; + import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte" $: id = $viewsV2.selected?.id $: datasource = { diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte index 8c7eb49923..0cd008bab1 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte @@ -7,10 +7,10 @@ $: resourceId = getResourceID($datasource) const getResourceID = datasource => { - if (!datasource) { - return null - } - return datasource.type === "table" ? datasource.tableId : datasource.id + if (!datasource) { + return null + } + return datasource.type === "table" ? datasource.tableId : datasource.id } diff --git a/packages/frontend-core/src/components/grid/stores/columns.js b/packages/frontend-core/src/components/grid/stores/columns.js index 09e25586fd..a5d90adcfe 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, config, datasource, definition } = context + const { columns, stickyColumn, datasource, definition } = context // Checks if we have a certain column by name const hasColumn = column => { From df676bbe9e55c8f1a42d61878f13fbe55c605308 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 22 Aug 2023 13:39:58 +0100 Subject: [PATCH 108/245] Update table block to work with views --- packages/client/src/components/app/blocks/TableBlock.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/app/blocks/TableBlock.svelte b/packages/client/src/components/app/blocks/TableBlock.svelte index efc83fd5ac..c2a13ddef2 100644 --- a/packages/client/src/components/app/blocks/TableBlock.svelte +++ b/packages/client/src/components/app/blocks/TableBlock.svelte @@ -53,7 +53,8 @@ $: editTitle = getEditTitle(detailsFormBlockId, primaryDisplay) $: normalFields = getNormalFields(schema) $: rowClickActions = - clickBehaviour === "actions" || dataSource?.type !== "table" + clickBehaviour === "actions" || + (dataSource?.type !== "table" && dataSource?.type !== "viewV2") ? onClick : [ { From 4192618bdf1df82da3239d86377d2b34343c12b5 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 23 Aug 2023 18:56:04 +0100 Subject: [PATCH 109/245] Handle missing required columns in views by showing errors --- .../src/components/grid/stores/columns.js | 8 -------- .../src/components/grid/stores/datasource.js | 6 ++++++ .../frontend-core/src/components/grid/stores/rows.js | 2 +- .../src/components/grid/stores/table.js | 9 ++++++++- .../src/components/grid/stores/viewV2.js | 12 +++++++++++- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/columns.js b/packages/frontend-core/src/components/grid/stores/columns.js index a5d90adcfe..629d5dd893 100644 --- a/packages/frontend-core/src/components/grid/stores/columns.js +++ b/packages/frontend-core/src/components/grid/stores/columns.js @@ -71,13 +71,6 @@ export const deriveStores = context => { export const createActions = context => { const { columns, stickyColumn, datasource, definition } = context - // Checks if we have a certain column by name - const hasColumn = column => { - const $columns = get(columns) - const $sticky = get(stickyColumn) - return $columns.some(col => col.name === column) || $sticky?.name === column - } - // Updates the datasources primary display column const changePrimaryDisplay = async column => { return await datasource.actions.saveDefinition({ @@ -140,7 +133,6 @@ export const createActions = context => { columns: { ...columns, actions: { - hasColumn, saveChanges, changePrimaryDisplay, changeAllColumnWidths, diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.js index 61caa79734..3f4347953e 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -108,6 +108,11 @@ export const createActions = context => { return getAPI()?.actions.isDatasourceValid(datasource) } + // Checks if this datasource can use a specific column by name + const canUseColumn = name => { + return getAPI()?.actions.canUseColumn(name) + } + return { datasource: { ...datasource, @@ -119,6 +124,7 @@ export const createActions = context => { deleteRows, getRow, isDatasourceValid, + canUseColumn, }, }, } diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index f595276e73..392bf392e8 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -192,7 +192,7 @@ export const createActions = context => { let erroredColumns = [] let missingColumns = [] for (let column of keys) { - if (columns.actions.hasColumn(column)) { + if (datasource.actions.canUseColumn(column)) { erroredColumns.push(column) } else { missingColumns.push(column) diff --git a/packages/frontend-core/src/components/grid/stores/table.js b/packages/frontend-core/src/components/grid/stores/table.js index a881f45b5f..ed13609f45 100644 --- a/packages/frontend-core/src/components/grid/stores/table.js +++ b/packages/frontend-core/src/components/grid/stores/table.js @@ -3,7 +3,7 @@ import { get } from "svelte/store" const SuppressErrors = true export const createActions = context => { - const { definition, API, datasource } = context + const { definition, API, datasource, columns, stickyColumn } = context const refreshDefinition = async () => { definition.set(await API.fetchTableDefinition(get(datasource).tableId)) @@ -43,6 +43,12 @@ export const createActions = context => { return res?.rows?.[0] } + const canUseColumn = name => { + const $columns = get(columns) + const $sticky = get(stickyColumn) + return $columns.some(col => col.name === name) || $sticky?.name === name + } + return { table: { actions: { @@ -53,6 +59,7 @@ export const createActions = context => { deleteRows, getRow, isDatasourceValid, + canUseColumn, }, }, } diff --git a/packages/frontend-core/src/components/grid/stores/viewV2.js b/packages/frontend-core/src/components/grid/stores/viewV2.js index 59bf747330..8f0e07c0de 100644 --- a/packages/frontend-core/src/components/grid/stores/viewV2.js +++ b/packages/frontend-core/src/components/grid/stores/viewV2.js @@ -3,7 +3,7 @@ import { get } from "svelte/store" const SuppressErrors = true export const createActions = context => { - const { definition, API, datasource } = context + const { definition, API, datasource, columns, stickyColumn } = context const refreshDefinition = async () => { const $datasource = get(datasource) @@ -53,6 +53,15 @@ export const createActions = context => { ) } + const canUseColumn = name => { + const $columns = get(columns) + const $sticky = get(stickyColumn) + return ( + $columns.some(col => col.name === name && col.visible) || + $sticky?.name === name + ) + } + return { viewV2: { actions: { @@ -63,6 +72,7 @@ export const createActions = context => { deleteRows, getRow, isDatasourceValid, + canUseColumn, }, }, } From 9a15277fa1474dde4f435f189acc992000e3643c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 24 Aug 2023 09:11:54 +0200 Subject: [PATCH 110/245] Split authorized middleware to handle resource id fetch --- packages/server/src/api/routes/row.ts | 6 ++++-- packages/server/src/middleware/authorized.ts | 14 +++++++++++++- packages/server/src/middleware/resourceId.ts | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/routes/row.ts b/packages/server/src/api/routes/row.ts index a4ac8aa3ee..60bc4e0735 100644 --- a/packages/server/src/api/routes/row.ts +++ b/packages/server/src/api/routes/row.ts @@ -1,10 +1,11 @@ import Router from "@koa/router" import * as rowController from "../controllers/row" -import authorized from "../../middleware/authorized" +import authorized, { authorizedResource } from "../../middleware/authorized" import { paramResource, paramSubResource } from "../../middleware/resourceId" import { permissions } from "@budibase/backend-core" import { internalSearchValidator } from "./utils/validators" import trimViewRowInfo from "../../middleware/trimViewRowInfo" +import { extractViewInfoFromID } from "../../db/utils" const { PermissionType, PermissionLevel } = permissions const router: Router = new Router() @@ -269,7 +270,8 @@ router router.post( "/api/v2/views/:viewId/search", - authorized(PermissionType.TABLE, PermissionLevel.READ), + paramResource("viewId", val => extractViewInfoFromID(val).tableId), + authorizedResource(PermissionType.TABLE, PermissionLevel.READ), rowController.views.searchView ) diff --git a/packages/server/src/middleware/authorized.ts b/packages/server/src/middleware/authorized.ts index 915344f747..930fb0f0ea 100644 --- a/packages/server/src/middleware/authorized.ts +++ b/packages/server/src/middleware/authorized.ts @@ -74,7 +74,8 @@ const checkAuthorizedResource = async ( } } -export default ( +const authorized = + ( permType: PermissionType, permLevel?: PermissionLevel, opts = { schema: false } @@ -143,3 +144,14 @@ export default ( // csrf protection return csrf(ctx, next) } + +export default ( + permType: PermissionType, + permLevel?: PermissionLevel, + opts = { schema: false } +) => authorized(permType, permLevel, opts) + +export const authorizedResource = ( + permType: PermissionType, + permLevel?: PermissionLevel +) => authorized(permType, permLevel) diff --git a/packages/server/src/middleware/resourceId.ts b/packages/server/src/middleware/resourceId.ts index 0917941061..1ad0b2a0c1 100644 --- a/packages/server/src/middleware/resourceId.ts +++ b/packages/server/src/middleware/resourceId.ts @@ -43,6 +43,7 @@ export class ResourceIdGetter { } } +/** @deprecated we should use the authorizedResource middleware instead */ export function paramResource(main: string) { return new ResourceIdGetter("params").mainResource(main).build() } From b3802070645278b13a37793e53cc027d4ed801c1 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 24 Aug 2023 09:36:44 +0200 Subject: [PATCH 111/245] Merge resource and authorized, allowing transformers --- packages/server/src/api/routes/row.ts | 8 ++++++-- packages/server/src/middleware/authorized.ts | 21 +++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/server/src/api/routes/row.ts b/packages/server/src/api/routes/row.ts index 60bc4e0735..9f4beb8173 100644 --- a/packages/server/src/api/routes/row.ts +++ b/packages/server/src/api/routes/row.ts @@ -270,8 +270,12 @@ router router.post( "/api/v2/views/:viewId/search", - paramResource("viewId", val => extractViewInfoFromID(val).tableId), - authorizedResource(PermissionType.TABLE, PermissionLevel.READ), + authorizedResource( + PermissionType.TABLE, + PermissionLevel.READ, + "viewId", + val => extractViewInfoFromID(val).tableId + ), rowController.views.searchView ) diff --git a/packages/server/src/middleware/authorized.ts b/packages/server/src/middleware/authorized.ts index 930fb0f0ea..a754bcc1f0 100644 --- a/packages/server/src/middleware/authorized.ts +++ b/packages/server/src/middleware/authorized.ts @@ -8,6 +8,7 @@ import { import { PermissionLevel, PermissionType, Role, UserCtx } from "@budibase/types" import builderMiddleware from "./builder" import { isWebhookEndpoint } from "./utils" +import { paramResource } from "./resourceId" function hasResource(ctx: any) { return ctx.resourceId != null @@ -78,7 +79,8 @@ const authorized = ( permType: PermissionType, permLevel?: PermissionLevel, - opts = { schema: false } + opts = { schema: false }, + resourceId?: { path: string; transformer?: (val: string) => string } ) => async (ctx: any, next: any) => { // webhooks don't need authentication, each webhook unique @@ -99,6 +101,15 @@ const authorized = ? PermissionLevel.WRITE : PermissionLevel.READ const appId = context.getAppId() + + if (resourceId?.path) { + // Reusing the existing middleware to extract the value + paramResource(resourceId.path)(ctx, () => {}) + if (resourceId.transformer) { + ctx.resourceId = resourceId.transformer(ctx.resourceId) + } + } + if (appId && hasResource(ctx)) { resourceRoles = await roles.getRequiredResourceRole(permLevel!, ctx) if (opts && opts.schema) { @@ -153,5 +164,9 @@ export default ( export const authorizedResource = ( permType: PermissionType, - permLevel?: PermissionLevel -) => authorized(permType, permLevel) + permLevel: PermissionLevel, + path: string, + transformer?: (val: string) => string +) => { + return authorized(permType, permLevel, undefined, { path, transformer }) +} From 972cc9916babfc6427624a0e042142f6985aecf6 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 24 Aug 2023 09:39:38 +0200 Subject: [PATCH 112/245] Add inheritance tests --- .../src/api/routes/tests/permissions.spec.ts | 26 +++++++++++++++++++ .../server/src/tests/utilities/api/viewV2.ts | 8 ++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/server/src/api/routes/tests/permissions.spec.ts b/packages/server/src/api/routes/tests/permissions.spec.ts index 118d35f8fd..3437f65a46 100644 --- a/packages/server/src/api/routes/tests/permissions.spec.ts +++ b/packages/server/src/api/routes/tests/permissions.spec.ts @@ -12,6 +12,7 @@ import { PermissionLevel, Row, Table, + ViewV2, } from "@budibase/types" import * as setup from "./utilities" @@ -27,6 +28,7 @@ describe("/permission", () => { let table: Table & { _id: string } let perms: Document[] let row: Row + let view: ViewV2 afterAll(setup.afterAll) @@ -39,6 +41,7 @@ describe("/permission", () => { table = (await config.createTable()) as typeof table row = await config.createRow() + view = await config.api.viewV2.create({ tableId: table._id }) perms = await config.api.permission.set({ roleId: STD_ROLE_ID, resourceId: table._id, @@ -162,6 +165,29 @@ describe("/permission", () => { expect(res.body[0]._id).toEqual(row._id) }) + it("should be able to access the view data when the table is set to public and with no view permissions overrides", async () => { + // replicate changes before checking permissions + await config.publish() + + const res = await config.api.viewV2.search(view.id, undefined, { + usePublicUser: true, + }) + expect(res.body.rows[0]._id).toEqual(row._id) + }) + + it("should be able to access the view data when the table is set to public and with no view permissions overrides", async () => { + await config.api.permission.revoke({ + roleId: STD_ROLE_ID, + resourceId: table._id, + level: PermissionLevel.READ, + }) + + await config.api.viewV2.search(view.id, undefined, { + expectStatus: 403, + usePublicUser: true, + }) + }) + it("shouldn't allow writing from a public user", async () => { const res = await request .post(`/api/${table._id}/rows`) diff --git a/packages/server/src/tests/utilities/api/viewV2.ts b/packages/server/src/tests/utilities/api/viewV2.ts index 1520154641..bba65e187f 100644 --- a/packages/server/src/tests/utilities/api/viewV2.ts +++ b/packages/server/src/tests/utilities/api/viewV2.ts @@ -77,12 +77,16 @@ export class ViewV2API extends TestAPI { search = async ( viewId: string, params?: SearchViewRowRequest, - { expectStatus } = { expectStatus: 200 } + { expectStatus = 200, usePublicUser = false } = {} ) => { return this.request .post(`/api/v2/views/${viewId}/search`) .send(params) - .set(this.config.defaultHeaders()) + .set( + usePublicUser + ? this.config.publicHeaders() + : this.config.defaultHeaders() + ) .expect("Content-Type", /json/) .expect(expectStatus) } From bfa2b491f349030c42c436fcb55a99c23fffa7ef Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 24 Aug 2023 10:22:08 +0200 Subject: [PATCH 113/245] Allow view permission type --- packages/server/src/api/routes/row.ts | 9 ++--- packages/server/src/middleware/authorized.ts | 38 +++++++++++++++----- packages/types/src/sdk/permissions.ts | 1 + 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/server/src/api/routes/row.ts b/packages/server/src/api/routes/row.ts index 9f4beb8173..c29cb65eac 100644 --- a/packages/server/src/api/routes/row.ts +++ b/packages/server/src/api/routes/row.ts @@ -5,7 +5,7 @@ import { paramResource, paramSubResource } from "../../middleware/resourceId" import { permissions } from "@budibase/backend-core" import { internalSearchValidator } from "./utils/validators" import trimViewRowInfo from "../../middleware/trimViewRowInfo" -import { extractViewInfoFromID } from "../../db/utils" + const { PermissionType, PermissionLevel } = permissions const router: Router = new Router() @@ -270,12 +270,7 @@ router router.post( "/api/v2/views/:viewId/search", - authorizedResource( - PermissionType.TABLE, - PermissionLevel.READ, - "viewId", - val => extractViewInfoFromID(val).tableId - ), + authorizedResource(PermissionType.VIEW, PermissionLevel.READ, "viewId"), rowController.views.searchView ) diff --git a/packages/server/src/middleware/authorized.ts b/packages/server/src/middleware/authorized.ts index a754bcc1f0..3d4c44a108 100644 --- a/packages/server/src/middleware/authorized.ts +++ b/packages/server/src/middleware/authorized.ts @@ -6,9 +6,11 @@ import { users, } from "@budibase/backend-core" import { PermissionLevel, PermissionType, Role, UserCtx } from "@budibase/types" +import { features } from "@budibase/pro" import builderMiddleware from "./builder" import { isWebhookEndpoint } from "./utils" import { paramResource } from "./resourceId" +import { extractViewInfoFromID, isViewID } from "../db/utils" function hasResource(ctx: any) { return ctx.resourceId != null @@ -75,12 +77,31 @@ const checkAuthorizedResource = async ( } } +const resourceIdTranformers: Partial< + Record Promise> +> = { + [PermissionType.VIEW]: async ctx => { + const { resourceId } = ctx + if (!isViewID(resourceId)) { + ctx.throw(400, `"${resourceId}" is not a valid viewId`) + } + + if (await features.isViewPermissionEnabled()) { + ctx.subResourceId = ctx.resourceId + ctx.resourceId = extractViewInfoFromID(resourceId).tableId + } else { + ctx.resourceId = extractViewInfoFromID(resourceId).tableId + delete ctx.subResourceId + } + }, +} + const authorized = ( permType: PermissionType, permLevel?: PermissionLevel, opts = { schema: false }, - resourceId?: { path: string; transformer?: (val: string) => string } + resourcePath?: string ) => async (ctx: any, next: any) => { // webhooks don't need authentication, each webhook unique @@ -102,15 +123,15 @@ const authorized = : PermissionLevel.READ const appId = context.getAppId() - if (resourceId?.path) { + if (resourcePath) { // Reusing the existing middleware to extract the value - paramResource(resourceId.path)(ctx, () => {}) - if (resourceId.transformer) { - ctx.resourceId = resourceId.transformer(ctx.resourceId) - } + paramResource(resourcePath)(ctx, () => {}) } if (appId && hasResource(ctx)) { + if (resourceIdTranformers[permType]) { + await resourceIdTranformers[permType]!(ctx) + } resourceRoles = await roles.getRequiredResourceRole(permLevel!, ctx) if (opts && opts.schema) { otherLevelRoles = await roles.getRequiredResourceRole(otherLevel, ctx) @@ -165,8 +186,7 @@ export default ( export const authorizedResource = ( permType: PermissionType, permLevel: PermissionLevel, - path: string, - transformer?: (val: string) => string + resourcePath: string ) => { - return authorized(permType, permLevel, undefined, { path, transformer }) + return authorized(permType, permLevel, undefined, resourcePath) } diff --git a/packages/types/src/sdk/permissions.ts b/packages/types/src/sdk/permissions.ts index 9fe1970e44..a33d4985ee 100644 --- a/packages/types/src/sdk/permissions.ts +++ b/packages/types/src/sdk/permissions.ts @@ -15,4 +15,5 @@ export enum PermissionType { BUILDER = "builder", GLOBAL_BUILDER = "globalBuilder", QUERY = "query", + VIEW = "view", } From 9a7a3b9c7209878ebdce0e4a3b374f0f10aa7d20 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 24 Aug 2023 10:23:21 +0200 Subject: [PATCH 114/245] Rename test --- packages/server/src/api/routes/tests/permissions.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/permissions.spec.ts b/packages/server/src/api/routes/tests/permissions.spec.ts index 3437f65a46..5261259b77 100644 --- a/packages/server/src/api/routes/tests/permissions.spec.ts +++ b/packages/server/src/api/routes/tests/permissions.spec.ts @@ -175,7 +175,7 @@ describe("/permission", () => { expect(res.body.rows[0]._id).toEqual(row._id) }) - it("should be able to access the view data when the table is set to public and with no view permissions overrides", async () => { + it("should not be able to access the view data when the table is not public and there are no view permissions overrides", async () => { await config.api.permission.revoke({ roleId: STD_ROLE_ID, resourceId: table._id, From 84a6f239a93c6b96b5a1b409a7ff603077575be9 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 24 Aug 2023 11:05:31 +0200 Subject: [PATCH 115/245] Migrate tests to ts --- ...{authorized.spec.js => authorized.spec.ts} | 91 +++++++++++-------- 1 file changed, 52 insertions(+), 39 deletions(-) rename packages/server/src/middleware/tests/{authorized.spec.js => authorized.spec.ts} (65%) diff --git a/packages/server/src/middleware/tests/authorized.spec.js b/packages/server/src/middleware/tests/authorized.spec.ts similarity index 65% rename from packages/server/src/middleware/tests/authorized.spec.js rename to packages/server/src/middleware/tests/authorized.spec.ts index 3adc4d99a1..900928f0bc 100644 --- a/packages/server/src/middleware/tests/authorized.spec.js +++ b/packages/server/src/middleware/tests/authorized.spec.ts @@ -1,34 +1,40 @@ jest.mock("../../environment", () => ({ - prod: false, - isTest: () => true, - isProd: () => this.prod, - _set: function(key, value) { - this.prod = value === "production" - } - }) -) -const authorizedMiddleware = require("../authorized").default -const env = require("../../environment") -const { PermissionType, PermissionLevel } = require("@budibase/types") + prod: false, + isTest: () => true, + // @ts-ignore + isProd: () => this.prod, + _set: function (key: string, value: string) { + this.prod = value === "production" + }, +})) +import authorizedMiddleware from "../authorized" +import env from "../../environment" +import { PermissionType, PermissionLevel } from "@budibase/types" const APP_ID = "" class TestConfiguration { - constructor(role) { - this.middleware = authorizedMiddleware(role) + middleware: (ctx: any, next: any) => Promise + next: () => void + throw: () => void + headers: Record + ctx: any + + constructor() { + this.middleware = authorizedMiddleware(PermissionType.APP) this.next = jest.fn() this.throw = jest.fn() this.headers = {} this.ctx = { headers: {}, request: { - url: "" + url: "", }, appId: APP_ID, auth: {}, next: this.next, throw: this.throw, - get: (name) => this.headers[name], + get: (name: string) => this.headers[name], } } @@ -36,32 +42,33 @@ class TestConfiguration { return this.middleware(this.ctx, this.next) } - setUser(user) { + setUser(user: any) { this.ctx.user = user } - setMiddlewareRequiredPermission(...perms) { + setMiddlewareRequiredPermission(...perms: any[]) { + // @ts-ignore this.middleware = authorizedMiddleware(...perms) } - setResourceId(id) { + setResourceId(id: string) { this.ctx.resourceId = id } - setAuthenticated(isAuthed) { + setAuthenticated(isAuthed: boolean) { this.ctx.isAuthenticated = isAuthed } - setRequestUrl(url) { + setRequestUrl(url: string) { this.ctx.request.url = url } - setEnvironment(isProd) { + setEnvironment(isProd: boolean) { env._set("NODE_ENV", isProd ? "production" : "jest") } - setRequestHeaders(headers) { - this.ctx.headers = headers + setRequestHeaders(headers: Record) { + this.ctx.headers = headers } afterEach() { @@ -69,10 +76,9 @@ class TestConfiguration { } } - describe("Authorization middleware", () => { const next = jest.fn() - let config + let config: TestConfiguration afterEach(() => { config.afterEach() @@ -83,8 +89,6 @@ describe("Authorization middleware", () => { }) describe("non-webhook call", () => { - let config - beforeEach(() => { config = new TestConfiguration() config.setEnvironment(true) @@ -102,21 +106,21 @@ describe("Authorization middleware", () => { _id: "user", role: { _id: "ADMIN", - } + }, }) await config.executeMiddleware() expect(config.next).toHaveBeenCalled() }) - + it("throws if the user does not have builder permissions", async () => { config.setEnvironment(false) config.setMiddlewareRequiredPermission(PermissionType.BUILDER) config.setUser({ role: { - _id: "" - } + _id: "", + }, }) await config.executeMiddleware() @@ -127,8 +131,8 @@ describe("Authorization middleware", () => { config.setResourceId(PermissionType.QUERY) config.setUser({ role: { - _id: "" - } + _id: "", + }, }) config.setMiddlewareRequiredPermission(PermissionType.QUERY) @@ -139,25 +143,34 @@ describe("Authorization middleware", () => { it("throws if the user session is not authenticated", async () => { config.setUser({ role: { - _id: "" + _id: "", }, }) config.setAuthenticated(false) await config.executeMiddleware() - expect(config.throw).toHaveBeenCalledWith(403, "Session not authenticated") + expect(config.throw).toHaveBeenCalledWith( + 403, + "Session not authenticated" + ) }) it("throws if the user does not have base permissions to perform the operation", async () => { config.setUser({ role: { - _id: "" + _id: "", }, }) - config.setMiddlewareRequiredPermission(PermissionType.ADMIN, PermissionLevel.BASIC) - + config.setMiddlewareRequiredPermission( + PermissionType.APP, + PermissionLevel.READ + ) + await config.executeMiddleware() - expect(config.throw).toHaveBeenCalledWith(403, "User does not have permission") + expect(config.throw).toHaveBeenCalledWith( + 403, + "User does not have permission" + ) }) }) }) From 8359185a22e704146eb7a48fad182241045adfa2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 24 Aug 2023 11:23:30 +0200 Subject: [PATCH 116/245] Add unhappy paths tests --- packages/server/src/middleware/authorized.ts | 18 ++++--- .../src/middleware/tests/authorized.spec.ts | 49 +++++++++++++++++-- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/packages/server/src/middleware/authorized.ts b/packages/server/src/middleware/authorized.ts index 3d4c44a108..52ced1b9cc 100644 --- a/packages/server/src/middleware/authorized.ts +++ b/packages/server/src/middleware/authorized.ts @@ -82,8 +82,14 @@ const resourceIdTranformers: Partial< > = { [PermissionType.VIEW]: async ctx => { const { resourceId } = ctx + if (!resourceId) { + ctx.throw(400, `Cannot obtain the view id`) + return + } + if (!isViewID(resourceId)) { - ctx.throw(400, `"${resourceId}" is not a valid viewId`) + ctx.throw(400, `"${resourceId}" is not a valid view id`) + return } if (await features.isViewPermissionEnabled()) { @@ -121,17 +127,17 @@ const authorized = permLevel === PermissionLevel.READ ? PermissionLevel.WRITE : PermissionLevel.READ - const appId = context.getAppId() if (resourcePath) { // Reusing the existing middleware to extract the value paramResource(resourcePath)(ctx, () => {}) } - if (appId && hasResource(ctx)) { - if (resourceIdTranformers[permType]) { - await resourceIdTranformers[permType]!(ctx) - } + if (resourceIdTranformers[permType]) { + await resourceIdTranformers[permType]!(ctx) + } + + if (hasResource(ctx)) { resourceRoles = await roles.getRequiredResourceRole(permLevel!, ctx) if (opts && opts.schema) { otherLevelRoles = await roles.getRequiredResourceRole(otherLevel, ctx) diff --git a/packages/server/src/middleware/tests/authorized.spec.ts b/packages/server/src/middleware/tests/authorized.spec.ts index 900928f0bc..4d6f281294 100644 --- a/packages/server/src/middleware/tests/authorized.spec.ts +++ b/packages/server/src/middleware/tests/authorized.spec.ts @@ -3,13 +3,16 @@ jest.mock("../../environment", () => ({ isTest: () => true, // @ts-ignore isProd: () => this.prod, - _set: function (key: string, value: string) { + _set: function (_key: string, value: string) { this.prod = value === "production" }, })) + +import { PermissionType, PermissionLevel } from "@budibase/types" + import authorizedMiddleware from "../authorized" import env from "../../environment" -import { PermissionType, PermissionLevel } from "@budibase/types" +import { generateTableID, generateViewID } from "../../db/utils" const APP_ID = "" @@ -51,7 +54,7 @@ class TestConfiguration { this.middleware = authorizedMiddleware(...perms) } - setResourceId(id: string) { + setResourceId(id?: string) { this.ctx.resourceId = id } @@ -85,6 +88,7 @@ describe("Authorization middleware", () => { }) beforeEach(() => { + jest.clearAllMocks() config = new TestConfiguration() }) @@ -172,5 +176,44 @@ describe("Authorization middleware", () => { "User does not have permission" ) }) + + describe("view type", () => { + const tableId = generateTableID() + const viewId = generateViewID(tableId) + + beforeEach(() => { + config.setMiddlewareRequiredPermission( + PermissionType.VIEW, + PermissionLevel.READ + ) + config.setResourceId(viewId) + + config.setUser({ + role: { + _id: "", + }, + }) + }) + + it("throw an exception if the resource id is not provided", async () => { + config.setResourceId(undefined) + await config.executeMiddleware() + expect(config.throw).toHaveBeenNthCalledWith( + 1, + 400, + "Cannot obtain the view id" + ) + }) + + it("throw an exception if the resource id is not a valid view id", async () => { + config.setResourceId(tableId) + await config.executeMiddleware() + expect(config.throw).toHaveBeenNthCalledWith( + 1, + 400, + `"${tableId}" is not a valid view id` + ) + }) + }) }) }) From cfeb6993cc82a973ac39efb1212ca07eaa85ae5e Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 24 Aug 2023 11:46:36 +0200 Subject: [PATCH 117/245] Test authorised view use cases --- packages/server/src/middleware/authorized.ts | 11 +++- .../src/middleware/tests/authorized.spec.ts | 57 ++++++++++++++++++- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/packages/server/src/middleware/authorized.ts b/packages/server/src/middleware/authorized.ts index 52ced1b9cc..35d373efbf 100644 --- a/packages/server/src/middleware/authorized.ts +++ b/packages/server/src/middleware/authorized.ts @@ -138,9 +138,16 @@ const authorized = } if (hasResource(ctx)) { - resourceRoles = await roles.getRequiredResourceRole(permLevel!, ctx) + const { resourceId, subResourceId } = ctx + resourceRoles = await roles.getRequiredResourceRole(permLevel!, { + resourceId, + subResourceId, + }) if (opts && opts.schema) { - otherLevelRoles = await roles.getRequiredResourceRole(otherLevel, ctx) + otherLevelRoles = await roles.getRequiredResourceRole(otherLevel, { + resourceId, + subResourceId, + }) } } diff --git a/packages/server/src/middleware/tests/authorized.spec.ts b/packages/server/src/middleware/tests/authorized.spec.ts index 4d6f281294..d47430e63f 100644 --- a/packages/server/src/middleware/tests/authorized.spec.ts +++ b/packages/server/src/middleware/tests/authorized.spec.ts @@ -1,3 +1,10 @@ +jest.mock("@budibase/backend-core", () => ({ + ...jest.requireActual("@budibase/backend-core"), + roles: { + ...jest.requireActual("@budibase/backend-core").roles, + getRequiredResourceRole: jest.fn().mockResolvedValue([]), + }, +})) jest.mock("../../environment", () => ({ prod: false, isTest: () => true, @@ -13,9 +20,14 @@ import { PermissionType, PermissionLevel } from "@budibase/types" import authorizedMiddleware from "../authorized" import env from "../../environment" import { generateTableID, generateViewID } from "../../db/utils" +import { roles } from "@budibase/backend-core" +import { mocks } from "@budibase/backend-core/tests" +import { initProMocks } from "../../tests/utilities/mocks/pro" const APP_ID = "" +initProMocks() + class TestConfiguration { middleware: (ctx: any, next: any) => Promise next: () => void @@ -80,7 +92,6 @@ class TestConfiguration { } describe("Authorization middleware", () => { - const next = jest.fn() let config: TestConfiguration afterEach(() => { @@ -89,6 +100,7 @@ describe("Authorization middleware", () => { beforeEach(() => { jest.clearAllMocks() + mocks.licenses.useCloudFree() config = new TestConfiguration() }) @@ -181,6 +193,11 @@ describe("Authorization middleware", () => { const tableId = generateTableID() const viewId = generateViewID(tableId) + const mockedGetRequiredResourceRole = + roles.getRequiredResourceRole as jest.MockedFunction< + typeof roles.getRequiredResourceRole + > + beforeEach(() => { config.setMiddlewareRequiredPermission( PermissionType.VIEW, @@ -188,13 +205,49 @@ describe("Authorization middleware", () => { ) config.setResourceId(viewId) + mockedGetRequiredResourceRole.mockResolvedValue(["PUBLIC"]) + config.setUser({ + _id: "user", role: { - _id: "", + _id: "PUBLIC", }, }) }) + it("will ignore view permissions if flag is off", async () => { + await config.executeMiddleware() + + expect(config.throw).not.toBeCalled() + expect(config.next).toHaveBeenCalled() + + expect(mockedGetRequiredResourceRole).toBeCalledTimes(1) + expect(mockedGetRequiredResourceRole).toBeCalledWith( + PermissionLevel.READ, + expect.objectContaining({ + resourceId: tableId, + subResourceId: undefined, + }) + ) + }) + + it("will use view permissions if flag is on", async () => { + mocks.licenses.useViewPermissions() + await config.executeMiddleware() + + expect(config.throw).not.toBeCalled() + expect(config.next).toHaveBeenCalled() + + expect(mockedGetRequiredResourceRole).toBeCalledTimes(1) + expect(mockedGetRequiredResourceRole).toBeCalledWith( + PermissionLevel.READ, + expect.objectContaining({ + resourceId: tableId, + subResourceId: viewId, + }) + ) + }) + it("throw an exception if the resource id is not provided", async () => { config.setResourceId(undefined) await config.executeMiddleware() From 3e70369832dffc9c4a925267b44b5d5ac5e05f54 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 24 Aug 2023 11:47:42 +0200 Subject: [PATCH 118/245] Use --- .../server/src/sdk/app/permissions/tests/permissions.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server/src/sdk/app/permissions/tests/permissions.spec.ts b/packages/server/src/sdk/app/permissions/tests/permissions.spec.ts index 4c2768dde4..4c233e68fa 100644 --- a/packages/server/src/sdk/app/permissions/tests/permissions.spec.ts +++ b/packages/server/src/sdk/app/permissions/tests/permissions.spec.ts @@ -1,12 +1,13 @@ -import TestConfiguration from "../../../../tests/utilities/TestConfiguration" import { PermissionLevel } from "@budibase/types" import { mocks, structures } from "@budibase/backend-core/tests" import { resourceActionAllowed } from ".." import { generateViewID } from "../../../../db/utils" +import { initProMocks } from "../../../../tests/utilities/mocks/pro" + +initProMocks() describe("permissions sdk", () => { beforeEach(() => { - new TestConfiguration() mocks.licenses.useCloudFree() }) From 12be5a3d839cb03a463456c030cd0de98f927346 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 24 Aug 2023 11:47:51 +0200 Subject: [PATCH 119/245] Setuo init pro mocks --- packages/server/src/tests/utilities/mocks/pro.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 packages/server/src/tests/utilities/mocks/pro.ts diff --git a/packages/server/src/tests/utilities/mocks/pro.ts b/packages/server/src/tests/utilities/mocks/pro.ts new file mode 100644 index 0000000000..a365ff40c4 --- /dev/null +++ b/packages/server/src/tests/utilities/mocks/pro.ts @@ -0,0 +1,10 @@ +// init the licensing mock +import { mocks } from "@budibase/backend-core/tests" +import * as pro from "@budibase/pro" + +export const initProMocks = () => { + mocks.licenses.init(pro) + + // use unlimited license by default + mocks.licenses.useUnlimited() +} From c0581408e9d4e592be3191b1d704234a228bccdd Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 24 Aug 2023 11:48:38 +0200 Subject: [PATCH 120/245] Add extra tests --- .../src/api/routes/tests/permissions.spec.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/server/src/api/routes/tests/permissions.spec.ts b/packages/server/src/api/routes/tests/permissions.spec.ts index 5261259b77..3fa21cb677 100644 --- a/packages/server/src/api/routes/tests/permissions.spec.ts +++ b/packages/server/src/api/routes/tests/permissions.spec.ts @@ -15,6 +15,7 @@ import { ViewV2, } from "@budibase/types" import * as setup from "./utilities" +import { mocks } from "@budibase/backend-core/tests" const { basicRow } = setup.structures const { BUILTIN_ROLE_IDS } = roles @@ -37,6 +38,7 @@ describe("/permission", () => { }) beforeEach(async () => { + mocks.licenses.useCloudFree() mockedSdk.resourceActionAllowed.mockResolvedValue({ allowed: true }) table = (await config.createTable()) as typeof table @@ -181,6 +183,8 @@ describe("/permission", () => { resourceId: table._id, level: PermissionLevel.READ, }) + // replicate changes before checking permissions + await config.publish() await config.api.viewV2.search(view.id, undefined, { expectStatus: 403, @@ -188,6 +192,47 @@ describe("/permission", () => { }) }) + it("should ignore the view permissions if the flag is not on", async () => { + await config.api.permission.set({ + roleId: STD_ROLE_ID, + resourceId: view.id, + level: PermissionLevel.READ, + }) + await config.api.permission.revoke({ + roleId: STD_ROLE_ID, + resourceId: table._id, + level: PermissionLevel.READ, + }) + // replicate changes before checking permissions + await config.publish() + + await config.api.viewV2.search(view.id, undefined, { + expectStatus: 403, + usePublicUser: true, + }) + }) + + it("should use the view permissions if the flag is on", async () => { + mocks.licenses.useViewPermissions() + await config.api.permission.set({ + roleId: STD_ROLE_ID, + resourceId: view.id, + level: PermissionLevel.READ, + }) + await config.api.permission.revoke({ + roleId: STD_ROLE_ID, + resourceId: table._id, + level: PermissionLevel.READ, + }) + // replicate changes before checking permissions + await config.publish() + + const res = await config.api.viewV2.search(view.id, undefined, { + usePublicUser: true, + }) + expect(res.body.rows[0]._id).toEqual(row._id) + }) + it("shouldn't allow writing from a public user", async () => { const res = await request .post(`/api/${table._id}/rows`) From 8d5f6cf7af9a7efec7f2f28b78e55c0b152ede78 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Thu, 24 Aug 2023 11:38:12 +0100 Subject: [PATCH 121/245] add api for per app builder and help function --- packages/frontend-core/src/api/user.js | 22 +++++++++++++++++++ packages/frontend-core/src/constants.js | 2 ++ .../shared-core/src/sdk/documents/users.ts | 7 ++++++ 3 files changed, 31 insertions(+) diff --git a/packages/frontend-core/src/api/user.js b/packages/frontend-core/src/api/user.js index d8723a649c..d0560239ba 100644 --- a/packages/frontend-core/src/api/user.js +++ b/packages/frontend-core/src/api/user.js @@ -250,4 +250,26 @@ export const buildUserEndpoints = API => ({ url: `/api/global/users/count/${appId}`, }) }, + + /** + * Adds a per app builder to the selected app + * @param appId the applications id + * @param userId The id of the user to add as a builder + */ + addAppBuilder: async ({ userId, appId }) => { + return await API.post({ + url: `/api/global/users/${userId}/app/${appId}/builder`, + }) + }, + + /** + * Removes a per app builder to the selected app + * @param appId the applications id + * @param userId The id of the user to remove as a builder + */ + removeAppBuilder: async ({ userId, appId }) => { + return await API.delete({ + url: `/api/global/users/${userId}/app/${appId}/builder`, + }) + }, }) diff --git a/packages/frontend-core/src/constants.js b/packages/frontend-core/src/constants.js index 0497a392f3..2a04886762 100644 --- a/packages/frontend-core/src/constants.js +++ b/packages/frontend-core/src/constants.js @@ -71,6 +71,7 @@ export const Features = { BRANDING: "branding", SCIM: "scim", SYNC_AUTOMATIONS: "syncAutomations", + APP_BUILDERS: "appBuilders", } // Role IDs @@ -80,6 +81,7 @@ export const Roles = { BASIC: "BASIC", PUBLIC: "PUBLIC", BUILDER: "BUILDER", + CREATOR: "CREATOR", } export const Themes = [ diff --git a/packages/shared-core/src/sdk/documents/users.ts b/packages/shared-core/src/sdk/documents/users.ts index 0f0983c6f7..1be8845656 100644 --- a/packages/shared-core/src/sdk/documents/users.ts +++ b/packages/shared-core/src/sdk/documents/users.ts @@ -35,6 +35,13 @@ export function isAdminOrBuilder( return isBuilder(user, appId) || isAdmin(user) } +export function isAdminOrGlobalBuilder( + user: User | ContextUser, + appId?: string +): boolean { + return isGlobalBuilder(user) || isAdmin(user) +} + // check if they are a builder within an app (not necessarily a global builder) export function hasAppBuilderPermissions(user?: User | ContextUser): boolean { if (!user) { From f7e43a766dc1c5167ba65f0099bd77ce34f3ea88 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Thu, 24 Aug 2023 11:38:50 +0100 Subject: [PATCH 122/245] add tag support to picker --- packages/bbui/src/Form/Core/Picker.svelte | 20 +++++++++++++++++++- packages/bbui/src/Form/Core/Select.svelte | 3 ++- packages/bbui/src/Form/Select.svelte | 3 ++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index aada17b318..6658ab0df5 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -8,6 +8,8 @@ import Icon from "../../Icon/Icon.svelte" import StatusLight from "../../StatusLight/StatusLight.svelte" import Popover from "../../Popover/Popover.svelte" + import Tags from "../../Tags/Tags.svelte" + import Tag from "../../Tags/Tag.svelte" export let id = null export let disabled = false @@ -37,6 +39,7 @@ export let customPopoverHeight export let align = "left" export let footer = null + export let tag = null const dispatch = createEventDispatcher() @@ -99,7 +102,7 @@ bind:this={button} > {#if fieldIcon} - {#if !useOptionIconImage} + {#if !useOptionIconImage}x @@ -217,6 +220,13 @@ {getOptionLabel(option, idx)} + {#if option.tag} + + + {option.tag} + + + {/if} .spectrum-Icon) { + margin-top: 2px; + } diff --git a/packages/bbui/src/Form/Core/Select.svelte b/packages/bbui/src/Form/Core/Select.svelte index 2fad886910..e79ead7e8c 100644 --- a/packages/bbui/src/Form/Core/Select.svelte +++ b/packages/bbui/src/Form/Core/Select.svelte @@ -21,7 +21,7 @@ export let sort = false export let align export let footer = null - + export let tag = null const dispatch = createEventDispatcher() let open = false @@ -83,6 +83,7 @@ {isOptionEnabled} {autocomplete} {sort} + {tag} isPlaceholder={value == null || value === ""} placeholderOption={placeholder === false ? null : placeholder} isOptionSelected={option => option === value} diff --git a/packages/bbui/src/Form/Select.svelte b/packages/bbui/src/Form/Select.svelte index e87496652d..a9214320f9 100644 --- a/packages/bbui/src/Form/Select.svelte +++ b/packages/bbui/src/Form/Select.svelte @@ -25,7 +25,7 @@ export let customPopoverHeight export let align export let footer = null - + export let tag = null const dispatch = createEventDispatcher() const onChange = e => { value = e.detail @@ -61,6 +61,7 @@ {isOptionEnabled} {autocomplete} {customPopoverHeight} + {tag} on:change={onChange} on:click /> From 6b14353dcbd59a79fab19a7d817d12841e719497 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Thu, 24 Aug 2023 11:40:00 +0100 Subject: [PATCH 123/245] update side panel to enable selecting of creator role --- .../src/components/common/RoleSelect.svelte | 42 ++++++++++++++-- .../_components/BuilderSidePanel.svelte | 49 ++++++++++++------- .../builder/src/stores/portal/licensing.js | 4 ++ packages/builder/src/stores/portal/users.js | 10 ++++ 4 files changed, 83 insertions(+), 22 deletions(-) diff --git a/packages/builder/src/components/common/RoleSelect.svelte b/packages/builder/src/components/common/RoleSelect.svelte index 09a67cb6fe..62be915dcb 100644 --- a/packages/builder/src/components/common/RoleSelect.svelte +++ b/packages/builder/src/components/common/RoleSelect.svelte @@ -1,6 +1,8 @@
    {user.email}
    -
    - {userTitle(user)} -
{ + addAppBuilder(user._id) + }} on:change={e => { onUpdateUser(user, e.detail) }} @@ -614,7 +625,7 @@ }} autoWidth align="right" - allowedRoles={user.isAdminOrBuilder + allowedRoles={user.isAdminOrGlobalBuilder ? [Constants.Roles.ADMIN] : null} /> diff --git a/packages/builder/src/stores/portal/licensing.js b/packages/builder/src/stores/portal/licensing.js index 4a59f29f52..c42a2006c3 100644 --- a/packages/builder/src/stores/portal/licensing.js +++ b/packages/builder/src/stores/portal/licensing.js @@ -125,6 +125,9 @@ export const createLicensingStore = () => { const syncAutomationsEnabled = license.features.includes( Constants.Features.SYNC_AUTOMATIONS ) + const perAppBuildersEnabled = license.features.includes( + Constants.Features.APP_BUILDERS + ) store.update(state => { return { ...state, @@ -140,6 +143,7 @@ export const createLicensingStore = () => { auditLogsEnabled, enforceableSSO, syncAutomationsEnabled, + perAppBuildersEnabled, } }) }, diff --git a/packages/builder/src/stores/portal/users.js b/packages/builder/src/stores/portal/users.js index 992f6a5418..9cd5ed54ea 100644 --- a/packages/builder/src/stores/portal/users.js +++ b/packages/builder/src/stores/portal/users.js @@ -112,6 +112,14 @@ export function createUsersStore() { return await API.saveUser(user) } + async function addAppBuilder(userId, appId) { + return await API.addAppBuilder({ userId, appId }) + } + + async function removeAppBuilder(userId, appId) { + return await API.removeAppBuilder({ userId, appId }) + } + const getUserRole = user => sdk.users.isAdmin(user) ? "admin" @@ -139,6 +147,8 @@ export function createUsersStore() { getInvites, updateInvite, getUserCountByApp, + addAppBuilder, + removeAppBuilder, // any operation that adds or deletes users acceptInvite, create: refreshUsage(create), From 409df71c127f6c9258ab12af3efa7d31e73dc65b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 24 Aug 2023 12:11:22 +0100 Subject: [PATCH 124/245] Add hover color to divider in screen list --- .../design/[screenId]/_components/ScreenList/index.svelte | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/index.svelte index c5475b9c16..ef5911c0f8 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ScreenList/index.svelte @@ -297,8 +297,12 @@ width: 100%; top: 50%; transform: translateY(-50%); + transition: background 130ms ease-out; } .divider:hover { cursor: row-resize; } + .divider:hover:after { + background: var(--spectrum-global-color-gray-300); + } From 50e3a66f92034d4999cfadced13ef59388e5fcca Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 24 Aug 2023 12:26:46 +0100 Subject: [PATCH 125/245] Fix notifications in grid block in client apps --- packages/frontend-core/src/components/grid/stores/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-core/src/components/grid/stores/index.js b/packages/frontend-core/src/components/grid/stores/index.js index cb858b7293..7b73ea8be6 100644 --- a/packages/frontend-core/src/components/grid/stores/index.js +++ b/packages/frontend-core/src/components/grid/stores/index.js @@ -20,7 +20,6 @@ import * as ViewV2 from "./viewV2" import * as Datasource from "./datasource" const DependencyOrderedStores = [ - Notifications, Sort, Filter, Bounds, @@ -40,6 +39,7 @@ const DependencyOrderedStores = [ Pagination, Clipboard, Config, + Notifications, ] export const attachStores = context => { From 1ec2faf74d3561d52727b2477952238de9096b9e Mon Sep 17 00:00:00 2001 From: Dean Date: Thu, 24 Aug 2023 14:39:53 +0100 Subject: [PATCH 126/245] List refinement, Form Block UX updates for action type. Bug fixes for FormBlock bindings. TableBlock UX updates and Component Setting updates --- packages/bbui/src/Popover/Popover.svelte | 14 ++ .../builder/src/builderStore/dataBinding.js | 86 ++++++- .../src/builderStore/store/frontend.js | 89 +++----- .../bindings/DrawerBindableInput.svelte | 2 + .../ButtonActionEditor.svelte | 2 +- ...ttingsList.svelte => DraggableList.svelte} | 106 +++++---- .../EditFieldPopover.svelte | 207 +++++++---------- .../FieldConfiguration.svelte | 209 ++++++++---------- .../FieldConfiguration/FieldSetting.svelte | 56 +++++ .../controls/FieldConfiguration/utils.js | 46 ++++ .../OptionsEditor/OptionsEditor.svelte | 15 +- .../settings/controls/PropertyControl.svelte | 2 + .../ValidationEditor/ValidationEditor.svelte | 7 +- .../settings/ComponentInfoSection.svelte | 38 +--- .../settings/ComponentSettingsPanel.svelte | 5 +- .../settings/ComponentSettingsSection.svelte | 62 +++--- .../_components/settings/InfoDisplay.svelte | 61 +++++ packages/client/manifest.json | 20 +- .../components/app/blocks/TableBlock.svelte | 9 +- .../app/blocks/form/FormBlock.svelte | 75 +++---- .../app/blocks/form/InnerFormBlock.svelte | 40 ++-- 21 files changed, 666 insertions(+), 485 deletions(-) rename packages/builder/src/components/design/settings/controls/{SettingsList.svelte => DraggableList.svelte} (55%) create mode 100644 packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldSetting.svelte create mode 100644 packages/builder/src/components/design/settings/controls/FieldConfiguration/utils.js create mode 100644 packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/InfoDisplay.svelte diff --git a/packages/bbui/src/Popover/Popover.svelte b/packages/bbui/src/Popover/Popover.svelte index 6706bf7a8b..198f96c672 100644 --- a/packages/bbui/src/Popover/Popover.svelte +++ b/packages/bbui/src/Popover/Popover.svelte @@ -23,6 +23,9 @@ export let animate = true export let customZindex + export let showPopover = true + export let clickOutsideOverride = false + $: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum" export const show = () => { @@ -36,6 +39,9 @@ } const handleOutsideClick = e => { + if (clickOutsideOverride) { + return + } if (open) { // Stop propagation if the source is the anchor let node = e.target @@ -54,6 +60,9 @@ } function handleEscape(e) { + if (!clickOutsideOverride) { + return + } if (open && e.key === "Escape") { hide() } @@ -79,6 +88,7 @@ on:keydown={handleEscape} class="spectrum-Popover is-open" class:customZindex + class:hide-popover={open && !showPopover} role="presentation" style="height: {customHeight}; --customZindex: {customZindex};" transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }} @@ -89,6 +99,10 @@ {/if} 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 859f019a26..94fd98c707 100644 --- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte +++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/EditFieldPopover.svelte @@ -1,86 +1,58 @@ - @@ -93,69 +65,60 @@ }} /> - - - - - - + on:drawerShow={e => { + drawers = [...drawers, e.detail] + }} + on:drawerHide={e => { + drawers = drawers.slice(0, -1) + }} + /> + + + + 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 922b994c5a..596bb95639 100644 --- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte +++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte @@ -1,127 +1,76 @@
- + {#if fieldList?.length} + + {/if}
diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration/utils.js b/packages/builder/src/components/design/settings/controls/FieldConfiguration/utils.js new file mode 100644 index 0000000000..d4a8963dba --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/utils.js @@ -0,0 +1,46 @@ +export const convertOldFieldFormat = fields => { + if (!fields) { + return [] + } + const converted = fields.map(field => { + if (typeof field === "string") { + // existed but was a string + return { + field, + active: true, + } + } else if (typeof field?.active != "boolean") { + // existed but had no state + return { + field: field.name, + active: true, + } + } else { + return field + } + }) + return converted +} + +export const getComponentForField = (field, schema) => { + if (!field || !schema?.[field]) { + return null + } + const type = schema[field].type + return FieldTypeToComponentMap[type] +} + +export const FieldTypeToComponentMap = { + string: "stringfield", + number: "numberfield", + bigint: "bigintfield", + options: "optionsfield", + array: "multifieldselect", + boolean: "booleanfield", + longform: "longformfield", + datetime: "datetimefield", + attachment: "attachmentfield", + link: "relationshipfield", + json: "jsonfield", + barcodeqr: "codescanner", +} diff --git a/packages/builder/src/components/design/settings/controls/OptionsEditor/OptionsEditor.svelte b/packages/builder/src/components/design/settings/controls/OptionsEditor/OptionsEditor.svelte index 1201edd31e..c626081042 100644 --- a/packages/builder/src/components/design/settings/controls/OptionsEditor/OptionsEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/OptionsEditor/OptionsEditor.svelte @@ -24,11 +24,22 @@ } -Define Options - +
+
+
Define Options
+
+ Define the options for this picker. + + diff --git a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte index 5125c3bade..c8135b4f61 100644 --- a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte +++ b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte @@ -100,6 +100,8 @@ {key} {type} {...props} + on:drawerHide + on:drawerShow />
{#if info} diff --git a/packages/builder/src/components/design/settings/controls/ValidationEditor/ValidationEditor.svelte b/packages/builder/src/components/design/settings/controls/ValidationEditor/ValidationEditor.svelte index 6db24e8d69..96953b56b8 100644 --- a/packages/builder/src/components/design/settings/controls/ValidationEditor/ValidationEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/ValidationEditor/ValidationEditor.svelte @@ -5,9 +5,8 @@ export let value = [] export let bindings = [] - export let componentDefinition + export let componentInstance export let type - const dispatch = createEventDispatcher() let drawer @@ -31,7 +30,7 @@ {text}
- + Configure validation rules for this field. @@ -41,7 +40,7 @@ bind:rules={value} {type} {bindings} - {componentDefinition} + fieldName={componentInstance?.field} /> diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentInfoSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentInfoSection.svelte index f0288f0059..e73e6d7841 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentInfoSection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentInfoSection.svelte @@ -1,36 +1,12 @@ - -
-
- - {componentDefinition.name} -
- {componentDefinition.info} -
+ + - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsPanel.svelte index 2ff605cc77..581e69cfaf 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsPanel.svelte @@ -5,7 +5,7 @@ import DesignSection from "./DesignSection.svelte" import CustomStylesSection from "./CustomStylesSection.svelte" import ConditionalUISection from "./ConditionalUISection.svelte" - import ComponentInfoSection from "./ComponentInfoSection.svelte" + import { getBindableProperties, getComponentBindableProperties, @@ -55,9 +55,6 @@
{#if section == "settings"} - {#if componentDefinition?.info} - - {/if} { const settings = definition?.settings ?? [] - console.log( - "ComponentSettingsSection::definition?.settings", - definition?.settings - ) const generalSettings = settings.filter(setting => !setting.section) const customSections = settings.filter(setting => setting.section) let sections = [ @@ -51,23 +50,22 @@ } const updateSetting = async (setting, value) => { - if (typeof onUpdateSetting === "function") { - onUpdateSetting(setting, value) - } else { - try { + try { + if (typeof onUpdateSetting === "function") { + await onUpdateSetting(setting, value) + } else { await store.actions.components.updateSetting(setting.key, value) - - // Send event if required - if (setting.sendEvents) { - analytics.captureEvent(Events.COMPONENT_UPDATED, { - name: componentInstance._component, - setting: setting.key, - value, - }) - } - } catch (error) { - notifications.error("Error updating component prop") } + // Send event if required + if (setting.sendEvents) { + analytics.captureEvent(Events.COMPONENT_UPDATED, { + name: componentInstance._component, + setting: setting.key, + value, + }) + } + } catch (error) { + notifications.error("Error updating component prop") } } @@ -106,7 +104,7 @@ } } - return true + return typeof setting.visible == "boolean" ? setting.visible : true } const canRenderControl = (instance, setting, isScreen) => { @@ -125,9 +123,22 @@ {#each sections as section, idx (section.name)} {#if section.visible} - + + {#if section.info} + + {:else if idx === 0 && section.name === "General" && componentDefinition.info} + + {/if}
- {#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen} + {#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen && showInstanceName} {/if} {/each} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/InfoDisplay.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/InfoDisplay.svelte new file mode 100644 index 0000000000..a48f5d92b8 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/InfoDisplay.svelte @@ -0,0 +1,61 @@ + + +
+ {#if title} +
+ + {title || ""} +
+ {@html body} + {:else} + + + + {@html body} + {/if} +
+ + diff --git a/packages/client/manifest.json b/packages/client/manifest.json index a03b6be8af..d1f718aaf3 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -5282,6 +5282,11 @@ }, { "section": true, + "dependsOn": { + "setting": "actionType", + "value": "Create", + "invert": true + }, "name": "Row details", "info": "How to pass a row ID using bindings", "settings": [ @@ -5289,23 +5294,13 @@ "type": "text", "label": "Row ID", "key": "rowId", - "nested": true, - "dependsOn": { - "setting": "actionType", - "value": "Create", - "invert": true - } + "nested": true }, { "type": "text", "label": "Empty text", "key": "noRowsMessage", - "defaultValue": "We couldn't find a row to display", - "dependsOn": { - "setting": "actionType", - "value": "Create", - "invert": true - } + "defaultValue": "We couldn't find a row to display" } ] }, @@ -5399,6 +5394,7 @@ { "type": "fieldConfiguration", "key": "fields", + "nested": true, "selectAllFields": true }, { diff --git a/packages/client/src/components/app/blocks/TableBlock.svelte b/packages/client/src/components/app/blocks/TableBlock.svelte index efc83fd5ac..e07c26544c 100644 --- a/packages/client/src/components/app/blocks/TableBlock.svelte +++ b/packages/client/src/components/app/blocks/TableBlock.svelte @@ -45,6 +45,9 @@ let enrichedSearchColumns let schemaLoaded = false + // Accommodate old config to ensure delete button does not reappear + $: deleteLabel = sidePanelShowDelete === false ? "" : sidePanelDeleteLabel + $: fetchSchema(dataSource) $: enrichSearchColumns(searchColumns, schema).then( val => (enrichedSearchColumns = val) @@ -245,10 +248,8 @@ bind:id={detailsFormBlockId} props={{ dataSource, - showSaveButton: true, - showDeleteButton: sidePanelShowDelete, - saveButtonLabel: sidePanelSaveLabel, - deleteButtonLabel: sidePanelDeleteLabel, + saveButtonLabel: sidePanelSaveLabel || "Save", //always show + deleteButtonLabel: deleteLabel, //respect config actionType: "Update", rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`, fields: sidePanelFields || normalFields, diff --git a/packages/client/src/components/app/blocks/form/FormBlock.svelte b/packages/client/src/components/app/blocks/form/FormBlock.svelte index c6a149e57c..5d57d10ab6 100644 --- a/packages/client/src/components/app/blocks/form/FormBlock.svelte +++ b/packages/client/src/components/app/blocks/form/FormBlock.svelte @@ -12,55 +12,59 @@ export let fields export let labelPosition export let title + export let showDeleteButton + export let showSaveButton export let saveButtonLabel export let deleteButtonLabel - export let showSaveButton - export let showDeleteButton export let rowId export let actionUrl export let noRowsMessage export let notificationOverride + // Accommodate old config to ensure delete button does not reappear + $: deleteLabel = showDeleteButton === false ? "" : deleteButtonLabel?.trim() + $: saveLabel = showSaveButton === false ? "" : saveButtonLabel?.trim() + const { fetchDatasourceSchema } = getContext("sdk") const convertOldFieldFormat = fields => { - return typeof fields?.[0] === "string" - ? fields.map(field => ({ + if (!fields) { + return [] + } + return fields.map(field => { + if (typeof field === "string") { + // existed but was a string + return { name: field, - displayName: field, active: true, - })) - : fields - } - - //All settings need to derive from the block config now - - // Parse the fields here too. Not present means false. - const getDefaultFields = (fields, schema) => { - let formFields - if (schema && (!fields || fields.length === 0)) { - const defaultFields = [] - - Object.values(schema).forEach(field => { - if (field.autocolumn) return - - defaultFields.push({ - name: field.name, - displayName: field.name, - active: true, - }) - }) - formFields = [...defaultFields] - } else { - formFields = (fields || []).map(field => { + } + } else { + // existed but had no state return { ...field, active: typeof field?.active != "boolean" ? true : field?.active, } - }) - } + } + }) + } - return formFields.filter(field => field.active) + const getDefaultFields = (fields, schema) => { + if (!schema) { + return [] + } + let defaultFields = [] + + if (!fields || fields.length === 0) { + Object.values(schema) + .filter(field => !field.autocolumn) + .forEach(field => { + defaultFields.push({ + name: field.name, + active: true, + }) + }) + } + return [...fields, ...defaultFields].filter(field => field.active) } let schema @@ -94,15 +98,12 @@ fields: fieldsOrDefault, labelPosition, title, - saveButtonLabel, - deleteButtonLabel, - showSaveButton, - showDeleteButton, + saveButtonLabel: saveLabel, + deleteButtonLabel: deleteLabel, schema, repeaterId, notificationOverride, } - const fetchSchema = async () => { schema = (await fetchDatasourceSchema(dataSource)) || {} } diff --git a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte index 5415768dc9..1f69645280 100644 --- a/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte +++ b/packages/client/src/components/app/blocks/form/InnerFormBlock.svelte @@ -13,8 +13,6 @@ export let title export let saveButtonLabel export let deleteButtonLabel - export let showSaveButton - export let showDeleteButton export let schema export let repeaterId export let notificationOverride @@ -100,18 +98,33 @@ }, ] - $: renderDeleteButton = showDeleteButton && actionType === "Update" - $: renderSaveButton = showSaveButton && actionType !== "View" + $: renderDeleteButton = deleteButtonLabel && actionType === "Update" + $: renderSaveButton = saveButtonLabel && actionType !== "View" $: renderButtons = renderDeleteButton || renderSaveButton $: renderHeader = renderButtons || title const getComponentForField = field => { - if (!field || !schema?.[field]) { + const fieldSchemaName = field.field || field.name + if (!fieldSchemaName || !schema?.[fieldSchemaName]) { return null } - const type = schema[field].type + const type = schema[fieldSchemaName].type return FieldTypeToComponentMap[type] } + + const getPropsForField = field => { + let fieldProps = field._component + ? { + ...field, + } + : { + field: field.name, + label: field.name, + placeholder: field.name, + _instanceName: field.name, + } + return fieldProps + } {#if fields?.length} @@ -175,7 +188,7 @@ {#each fields as field, idx} - {#if getComponentForField(field.name)} + {#if getComponentForField(field) && field.active} {/if} From b6e675e3ffc1f957ca457cc4874e0668a3641874 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 24 Aug 2023 15:15:15 +0100 Subject: [PATCH 127/245] Migrate DS+ settings without keys --- .../settings/controls/TableSelect.svelte | 21 +++++++++---------- .../src/components/app/DataProvider.svelte | 18 +++++++++++++++- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/TableSelect.svelte b/packages/builder/src/components/design/settings/controls/TableSelect.svelte index 03b739e09f..ced1151969 100644 --- a/packages/builder/src/components/design/settings/controls/TableSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/TableSelect.svelte @@ -1,6 +1,6 @@ table.name} - getOptionValue={table => table._id} + {options} + getOptionLabel={x => x.label} + getOptionValue={x => x.resourceId} /> diff --git a/packages/builder/src/components/design/settings/controls/TableSelect.svelte b/packages/builder/src/components/design/settings/controls/TableSelect.svelte index ced1151969..4a6f33202b 100644 --- a/packages/builder/src/components/design/settings/controls/TableSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/TableSelect.svelte @@ -1,7 +1,7 @@
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DuplicateRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DuplicateRow.svelte index bb253da56a..db1fff47f2 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DuplicateRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DuplicateRow.svelte @@ -4,7 +4,7 @@ import { tables } from "stores/backend" import { getContextProviderComponents, - getSchemaForTable, + getSchemaForDatasourcePlus, } from "builderStore/dataBinding" import SaveFields from "./SaveFields.svelte" @@ -60,7 +60,7 @@ } const getSchemaFields = (asset, tableId) => { - const { schema } = getSchemaForTable(tableId) + const { schema } = getSchemaForDatasourcePlus(tableId) delete schema._id delete schema._rev return Object.values(schema || {}) diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte index d16a279c68..c1917ad90f 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte @@ -1,10 +1,10 @@
- + role.name} - getOptionValue={role => role._id} - getOptionColour={getColor} - getOptionIcon={getIcon} - isOptionEnabled={option => { - if (option._id == CreatorID && !$licensing.perAppBuildersEnabled) { - return false - } else { - return true - } - }} - {placeholder} - {error} -/> +{#if fancySelect} + role.name} + getOptionValue={role => role._id} + getOptionColour={getColor} + getOptionIcon={getIcon} + isOptionEnabled={option => { + if (option._id == CreatorID && !$licensing.perAppBuildersEnabled) { + return false + } else { + return true + } + }} + {placeholder} + {error} + /> +{:else} + Date: Fri, 1 Sep 2023 11:33:59 +0200 Subject: [PATCH 222/245] Change api interfaces --- .../FlowChart/FlowItem.svelte | 2 +- .../buttons/ManageAccessButton.svelte | 7 +--- .../grid/GridManageAccessButton.svelte | 14 +------- .../DataTable/modals/ManageAccessModal.svelte | 35 +++++++++---------- .../integration/AccessLevelSelect.svelte | 2 +- .../server/src/api/controllers/permission.ts | 26 ++++++-------- .../server/src/sdk/app/permissions/index.ts | 33 +++++++++++++---- packages/types/src/api/web/app/permission.ts | 15 +++++--- 8 files changed, 69 insertions(+), 65 deletions(-) diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index 7a02433411..85c3776fdb 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -73,7 +73,7 @@ if (!perms["execute"]) { role = "BASIC" } else { - role = perms["execute"] + role = perms["execute"].role } } diff --git a/packages/builder/src/components/backend/DataTable/buttons/ManageAccessButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/ManageAccessButton.svelte index 8742eaef76..5c0b7df742 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/ManageAccessButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/ManageAccessButton.svelte @@ -5,7 +5,6 @@ export let resourceId export let disabled = false - export let requiresLicence let modal let resourcePermissions @@ -20,9 +19,5 @@ Access - + diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte index e4c48528f4..0cd008bab1 100644 --- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte +++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte @@ -1,5 +1,4 @@ - + diff --git a/packages/builder/src/components/backend/DataTable/modals/ManageAccessModal.svelte b/packages/builder/src/components/backend/DataTable/modals/ManageAccessModal.svelte index 0e792410ca..5b0f114534 100644 --- a/packages/builder/src/components/backend/DataTable/modals/ManageAccessModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/ManageAccessModal.svelte @@ -15,7 +15,6 @@ export let resourceId export let permissions - export let requiresLicence async function changePermission(level, role) { try { @@ -41,24 +40,20 @@ } } - $: computedPermissions = Object.keys(permissions.permissions).reduce( - (p, c) => { - p[c] = { - selected: - permissions.permissionType[c] === "INHERITED" - ? "inherited" - : permissions.permissions[c], + $: computedPermissions = Object.entries(permissions.permissions).reduce( + (p, [level, roleInfo]) => { + p[level] = { + selectedValue: + roleInfo.permissionType === "INHERITED" ? "inherited" : roleInfo.role, options: [...get(roles)], } - if (permissions.inheritablePermissions) { - p[c].inheritOption = permissions.inheritablePermissions[c] - p[c].options.unshift({ + if (roleInfo.inheritablePermission) { + p[level].inheritOption = roleInfo.inheritablePermission + p[level].options.unshift({ _id: "inherited", name: `Inherit (${ - get(roles).find( - x => x._id === permissions.inheritablePermissions[c] - ).name + get(roles).find(x => x._id === roleInfo.inheritablePermission).name })`, }) } @@ -66,21 +61,23 @@ }, {} ) + + $: requiresPlanToModify = permissions.requiresPlanToModify Manage Access - {#if requiresLicence} + {#if requiresPlanToModify} - {requiresLicence.tier} + {requiresPlanToModify} {/if} - {#if requiresLicence} - {requiresLicence.message} + {#if requiresPlanToModify} + {requiresPlanToModify} {:else} Specify the minimum access level role for this data.
@@ -90,7 +87,7 @@ - +