1
0
Fork 0
mirror of synced 2024-10-04 03:54:37 +13:00

Merge branch 'develop' of github.com:Budibase/budibase into fix/google-sheets-issues

This commit is contained in:
mike12345567 2023-06-05 09:36:45 +01:00
commit 770f7c1dc6
30 changed files with 175 additions and 117 deletions

View file

@ -1,5 +1,5 @@
{
"version": "2.6.19-alpha.48",
"version": "2.6.19-alpha.50",
"npmClient": "yarn",
"packages": [
"packages/backend-core",

View file

@ -16,6 +16,7 @@ export enum Header {
LICENSE_KEY = "x-budibase-license-key",
API_VER = "x-budibase-api-version",
APP_ID = "x-budibase-app-id",
SESSION_ID = "x-budibase-session-id",
TYPE = "x-budibase-type",
PREVIEW_ROLE = "x-budibase-role",
TENANT_ID = "x-budibase-tenant-id",

View file

@ -15,25 +15,23 @@ export const createBuilderWebsocket = appId => {
socket.on("connect_error", err => {
console.log("Failed to connect to builder websocket:", err.message)
})
// User events
socket.on(SocketEvent.UserUpdate, userStore.actions.updateUser)
socket.on(SocketEvent.UserDisconnect, userStore.actions.removeUser)
// Table events
socket.on(BuilderSocketEvent.TableChange, ({ id, table }) => {
tables.replaceTable(id, table)
})
// Datasource events
socket.on(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
datasources.replaceDatasource(id, datasource)
})
// Clean up user store on disconnect
socket.on("disconnect", () => {
userStore.actions.reset()
})
// User events
socket.onOther(SocketEvent.UserUpdate, userStore.actions.updateUser)
socket.onOther(SocketEvent.UserDisconnect, userStore.actions.removeUser)
// Table events
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
tables.replaceTable(id, table)
})
// Datasource events
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
datasources.replaceDatasource(id, datasource)
})
return socket
}

View file

@ -37,6 +37,7 @@
allowDeleteRows={!isUsersTable}
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
showAvatars={false}
on:updatetable={e => tables.replaceTable(id, e.detail)}
>
<svelte:fragment slot="controls">
{#if isInternal}

View file

@ -81,6 +81,7 @@
<Label>{label}</Label>
<Editor
editorHeight="250"
editorWidth="320"
mode="json"
on:change={({ detail }) => (value = detail.value)}
value={stringVal}

View file

@ -83,7 +83,7 @@
.help {
z-index: 2;
position: absolute;
bottom: var(--spacing-xl);
bottom: 24px;
right: 24px;
}

View file

@ -2,13 +2,11 @@
import { Button } from "@budibase/bbui"
</script>
<div class="beta-background" />
<div class="beta">
Enjoying the Grid?
<Button
size="M"
cta
on:click={() => window.open("https://t.maze.co/156382627", "_blank")}
on:click={() => window.open("https://t.maze.co/165900794", "_blank")}
>
Give Feedback
</Button>
@ -17,30 +15,16 @@
<style>
.beta {
position: absolute;
bottom: 32px;
right: 32px;
bottom: 24px;
right: 24px;
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
z-index: 10;
}
.beta :global(.spectrum-Button) {
background: var(--spectrum-global-color-magenta-400);
border-color: var(--spectrum-global-color-magenta-400);
}
.beta-background {
z-index: 0;
pointer-events: none;
position: absolute;
bottom: -230px;
right: -105px;
width: 1400px;
height: 320px;
transform: rotate(-22deg);
background: linear-gradient(
to top,
var(--cell-background) 20%,
transparent
);
}
</style>

View file

@ -3,6 +3,7 @@
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
import Panel from "components/design/Panel.svelte"
import { isActive, goto } from "@roxi/routify"
import BetaButton from "./_components/BetaButton.svelte"
</script>
<!-- routify:options index=1 -->
@ -19,6 +20,7 @@
<div class="content">
<slot />
</div>
<BetaButton />
</div>
<style>
@ -39,5 +41,6 @@
justify-content: flex-start;
align-items: stretch;
flex: 1 1 auto;
z-index: 1;
}
</style>

View file

@ -13,7 +13,7 @@
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
import { createRestDatasource } from "builderStore/datasource"
import DatasourceOption from "./_DatasourceOption.svelte"
import DatasourceOption from "./_components/DatasourceOption.svelte"
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"

View file

@ -20,7 +20,7 @@
Breadcrumb,
Header,
} from "components/portal/page"
import { apps, auth, overview } from "stores/portal"
import { apps, overview } from "stores/portal"
import { AppStatus } from "constants"
import analytics, { Events, EventSource } from "analytics"
import { store } from "builderStore"
@ -52,8 +52,6 @@
$: appId = $overview.selectedAppId
$: initialiseApp(appId)
$: isPublished = app?.status === AppStatus.DEPLOYED
$: appLocked = !!app?.lockedBy
$: lockedByYou = $auth.user.email === app?.lockedBy?.email
const initialiseApp = async appId => {
loaded = false
@ -139,14 +137,7 @@
</Button>
</span>
<span class="desktop">
<Button
size="M"
cta
disabled={appLocked && !lockedByYou}
on:click={editApp}
>
Edit
</Button>
<Button size="M" cta on:click={editApp}>Edit</Button>
</span>
<ActionMenu align="right">
<span slot="control" class="app-overview-actions-icon">
@ -158,13 +149,7 @@
</MenuItem>
</span>
<span class="mobile">
<MenuItem
icon="Edit"
disabled={appLocked && !lockedByYou}
on:click={editApp}
>
Edit
</MenuItem>
<MenuItem icon="Edit" on:click={editApp}>Edit</MenuItem>
</span>
<MenuItem
on:click={() => exportApp({ published: false })}

View file

@ -58,7 +58,7 @@
}
onMount(async () => {
await Promise.all(fetchConfig(), fetchAPIKey())
await Promise.all([fetchConfig(), fetchAPIKey()])
})
const copyToClipboard = async value => {

View file

@ -62,7 +62,7 @@ export function createTablesStore() {
}
const savedTable = await API.saveTable(updatedTable)
replaceTable(table._id, savedTable)
replaceTable(savedTable._id, savedTable)
await datasources.fetch()
select(savedTable._id)
return savedTable

View file

@ -18,7 +18,9 @@ export const initWebsocket = () => {
}
// Initialise connection
socket = createWebsocket("/socket/client", false)
socket = createWebsocket("/socket/client", {
heartbeat: false,
})
// Event handlers
socket.on("plugin-update", data => {

View file

@ -1,3 +1,4 @@
import { Helpers } from "@budibase/bbui"
import { ApiVersion } from "../constants"
import { buildAnalyticsEndpoints } from "./analytics"
import { buildAppEndpoints } from "./app"
@ -30,6 +31,14 @@ import { buildEnvironmentVariableEndpoints } from "./environmentVariables"
import { buildEventEndpoints } from "./events"
import { buildAuditLogsEndpoints } from "./auditLogs"
/**
* Random identifier to uniquely identify a session in a tab. This is
* used to determine the originator of calls to the API, which is in
* turn used to determine who caused a websocket message to be sent, so
* that we can ignore events caused by ourselves.
*/
export const APISessionID = Helpers.uuid()
const defaultAPIClientConfig = {
/**
* Certain definitions can't change at runtime for client apps, such as the
@ -116,6 +125,7 @@ export const createAPIClient = config => {
// Build headers
let headers = { Accept: "application/json" }
headers["x-budibase-session-id"] = APISessionID
if (!external) {
headers["x-budibase-api-version"] = ApiVersion
}

View file

@ -7,7 +7,6 @@
import { createAPIClient } from "../../../api"
import { attachStores } from "../stores"
import BulkDeleteHandler from "../controls/BulkDeleteHandler.svelte"
import BetaButton from "../controls/BetaButton.svelte"
import GridBody from "./GridBody.svelte"
import ResizeOverlay from "../overlays/ResizeOverlay.svelte"
import ReorderOverlay from "../overlays/ReorderOverlay.svelte"
@ -144,7 +143,6 @@
<HeaderRow />
<GridBody />
</div>
<BetaButton />
{#if allowAddRows}
<NewRow />
{/if}

View file

@ -1,4 +1,4 @@
export const Padding = 128
export const Padding = 256
export const MaxCellRenderHeight = 252
export const MaxCellRenderWidthOverflow = 200
export const ScrollBarSize = 8

View file

@ -26,15 +26,15 @@ export const createGridWebsocket = context => {
})
// User events
socket.on(SocketEvent.UserUpdate, user => {
socket.onOther(SocketEvent.UserUpdate, user => {
users.actions.updateUser(user)
})
socket.on(SocketEvent.UserDisconnect, user => {
socket.onOther(SocketEvent.UserDisconnect, user => {
users.actions.removeUser(user)
})
// Row events
socket.on(GridSocketEvent.RowChange, async data => {
socket.onOther(GridSocketEvent.RowChange, async data => {
if (data.id) {
rows.actions.replaceRow(data.id, data.row)
} else if (data.row.id) {
@ -44,7 +44,7 @@ export const createGridWebsocket = context => {
})
// Table events
socket.on(GridSocketEvent.TableChange, data => {
socket.onOther(GridSocketEvent.TableChange, data => {
// Only update table if one exists. If the table was deleted then we don't
// want to know - let the builder navigate away
if (data.table) {

View file

@ -46,7 +46,7 @@ export const createStores = () => {
}
export const deriveStores = context => {
const { table, columns, stickyColumn, API } = context
const { table, columns, stickyColumn, API, dispatch } = context
// Updates the tables primary display column
const changePrimaryDisplay = async column => {
@ -90,6 +90,10 @@ export const deriveStores = context => {
// Update local state
table.set(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)
// Update server
await API.saveTable(newTable)
}

View file

@ -214,8 +214,7 @@ export const deriveStores = context => {
const addRow = async (row, idx, bubble = false) => {
try {
// Create row
let newRow = await API.saveRow({ ...row, tableId: get(tableId) })
newRow = await fetchRow(newRow._id)
const newRow = await API.saveRow({ ...row, tableId: get(tableId) })
// Update state
if (idx != null) {
@ -437,6 +436,9 @@ export const deriveStores = context => {
// Checks if we have a row with a certain ID
const hasRow = id => {
if (id === NewRowID) {
return true
}
return get(rowLookupMap)[id] != null
}

View file

@ -1,17 +1,26 @@
import { io } from "socket.io-client"
import { SocketEvent, SocketSessionTTL } from "@budibase/shared-core"
import { APISessionID } from "../api"
export const createWebsocket = (path, heartbeat = true) => {
const DefaultOptions = {
heartbeat: true,
}
export const createWebsocket = (path, options = DefaultOptions) => {
if (!path) {
throw "A websocket path must be provided"
}
const { heartbeat } = {
...DefaultOptions,
...options,
}
// Determine connection info
const tls = location.protocol === "https:"
const proto = tls ? "wss:" : "ws:"
const host = location.hostname
const port = location.port || (tls ? 443 : 80)
const socket = io(`${proto}//${host}:${port}`, {
let socket = io(`${proto}//${host}:${port}`, {
path,
// Cap reconnection attempts to 3 (total of 15 seconds before giving up)
reconnectionAttempts: 3,
@ -37,5 +46,15 @@ export const createWebsocket = (path, heartbeat = true) => {
clearInterval(interval)
})
// Helper utility to ignore events that were triggered due to API
// changes made by us
socket.onOther = (event, callback) => {
socket.on(event, data => {
if (data?.apiSessionId !== APISessionID) {
callback(data)
}
})
}
return socket
}

View file

@ -1,7 +1,7 @@
import {
SortDirection,
FieldTypes,
NoEmptyFilterStrings,
SortDirection,
} from "../../../constants"
import {
breakExternalTableId,
@ -11,20 +11,34 @@ import { ExternalRequest, RunConfig } from "./ExternalRequest"
import * as exporters from "../view/exporters"
import { apiFileReturn } from "../../../utilities/fileSystem"
import {
Operation,
UserCtx,
Row,
PaginationJson,
Table,
Datasource,
IncludeRelationship,
Operation,
PaginationJson,
Row,
SortJson,
Table,
UserCtx,
} from "@budibase/types"
import sdk from "../../../sdk"
import * as utils from "./utils"
const { cleanExportRows } = require("./utils")
async function getRow(
tableId: string,
rowId: string,
opts?: { relationships?: boolean }
) {
const response = (await handleRequest(Operation.READ, tableId, {
id: breakRowIdField(rowId),
includeSqlRelationships: opts?.relationships
? IncludeRelationship.INCLUDE
: IncludeRelationship.EXCLUDE,
})) as Row[]
return response ? response[0] : response
}
export async function handleRequest(
operation: Operation,
tableId: string,
@ -63,11 +77,15 @@ export async function patch(ctx: UserCtx) {
if (!validateResult.valid) {
throw { validation: validateResult.errors }
}
return handleRequest(Operation.UPDATE, tableId, {
const response = await handleRequest(Operation.UPDATE, tableId, {
id: breakRowIdField(id),
row: inputs,
includeSqlRelationships: IncludeRelationship.INCLUDE,
})
const row = await getRow(tableId, id, { relationships: true })
return {
...response,
row,
}
}
export async function save(ctx: UserCtx) {
@ -80,10 +98,20 @@ export async function save(ctx: UserCtx) {
if (!validateResult.valid) {
throw { validation: validateResult.errors }
}
return handleRequest(Operation.CREATE, tableId, {
const response = await handleRequest(Operation.CREATE, tableId, {
row: inputs,
includeSqlRelationships: IncludeRelationship.EXCLUDE,
})
const responseRow = response as { row: Row }
const rowId = responseRow.row._id
if (rowId) {
const row = await getRow(tableId, rowId, { relationships: true })
return {
...response,
row,
}
} else {
return response
}
}
export async function fetchView(ctx: UserCtx) {
@ -104,11 +132,7 @@ export async function fetch(ctx: UserCtx) {
export async function find(ctx: UserCtx) {
const id = ctx.params.rowId
const tableId = ctx.params.tableId
const response = (await handleRequest(Operation.READ, tableId, {
id: breakRowIdField(id),
includeSqlRelationships: IncludeRelationship.EXCLUDE,
})) as Row[]
return response ? response[0] : response
return getRow(tableId, id)
}
export async function destroy(ctx: UserCtx) {

View file

@ -50,7 +50,7 @@ export const save = async (ctx: any) => {
if (body && body._id) {
return patch(ctx)
}
const { row, table } = await quotas.addRow(() =>
const { row, table, squashed } = await quotas.addRow(() =>
quotas.addQuery(() => pickApi(tableId).save(ctx), {
datasourceId: tableId,
})
@ -58,8 +58,9 @@ export const save = async (ctx: any) => {
ctx.status = 200
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
ctx.message = `${table.name} saved successfully`
ctx.body = row
gridSocket?.emitRowUpdate(ctx, row)
// prefer squashed for response
ctx.body = row || squashed
gridSocket?.emitRowUpdate(ctx, row || squashed)
}
export async function fetchView(ctx: any) {
const tableId = utils.getTableId(ctx)

View file

@ -7,6 +7,7 @@ import {
import { FieldTypes, FormulaTypes } from "../../../constants"
import { context } from "@budibase/backend-core"
import { Table, Row } from "@budibase/types"
import * as linkRows from "../../../db/linkedRows"
const { isEqual } = require("lodash")
const { cloneDeep } = require("lodash/fp")
@ -166,5 +167,9 @@ export async function finaliseRow(
if (updateFormula) {
await updateRelatedFormula(table, enrichedRow)
}
return { row: enrichedRow, table }
const squashed = await linkRows.squashLinksToPrimaryDisplay(
table,
enrichedRow
)
return { row: enrichedRow, squashed, table }
}

View file

@ -189,11 +189,13 @@ export async function attachFullLinkedDocs(table: Table, rows: Row[]) {
*/
export async function squashLinksToPrimaryDisplay(
table: Table,
enriched: Row[]
enriched: Row[] | Row
) {
// will populate this as we find them
const linkedTables = [table]
for (let row of enriched) {
const isArray = Array.isArray(enriched)
let enrichedArray = !isArray ? [enriched] : enriched
for (let row of enrichedArray) {
// this only fetches the table if its not already in array
const rowTable = await getLinkedTable(row.tableId!, linkedTables)
for (let [column, schema] of Object.entries(rowTable?.schema || {})) {
@ -213,5 +215,5 @@ export async function squashLinksToPrimaryDisplay(
row[column] = newLinks
}
}
return enriched
return isArray ? enrichedArray : enrichedArray[0]
}

View file

@ -216,7 +216,10 @@ export async function outputProcessing(
}
}
if (opts.squash) {
enriched = await linkRows.squashLinksToPrimaryDisplay(table, enriched)
enriched = (await linkRows.squashLinksToPrimaryDisplay(
table,
enriched
)) as Row[]
}
return wasArray ? enriched : enriched[0]
}

View file

@ -46,29 +46,32 @@ export default class BuilderSocket extends BaseSocket {
}
emitTableUpdate(ctx: any, table: Table) {
this.io
.in(ctx.appId)
.emit(BuilderSocketEvent.TableChange, { id: table._id, table })
gridSocket?.emitTableUpdate(table)
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.TableChange, {
id: table._id,
table,
})
gridSocket?.emitTableUpdate(ctx, table)
}
emitTableDeletion(ctx: any, id: string) {
this.io
.in(ctx.appId)
.emit(BuilderSocketEvent.TableChange, { id, table: null })
gridSocket?.emitTableDeletion(id)
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.TableChange, {
id,
table: null,
})
gridSocket?.emitTableDeletion(ctx, id)
}
emitDatasourceUpdate(ctx: any, datasource: Datasource) {
this.io.in(ctx.appId).emit(BuilderSocketEvent.DatasourceChange, {
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.DatasourceChange, {
id: datasource._id,
datasource,
})
}
emitDatasourceDeletion(ctx: any, id: string) {
this.io
.in(ctx.appId)
.emit(BuilderSocketEvent.DatasourceChange, { id, datasource: null })
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.DatasourceChange, {
id,
datasource: null,
})
}
}

View file

@ -31,21 +31,25 @@ export default class GridSocket extends BaseSocket {
emitRowUpdate(ctx: any, row: Row) {
const tableId = getTableId(ctx)
this.io.in(tableId).emit(GridSocketEvent.RowChange, { id: row._id, row })
this.emitToRoom(ctx, tableId, GridSocketEvent.RowChange, {
id: row._id,
row,
})
}
emitRowDeletion(ctx: any, id: string) {
const tableId = getTableId(ctx)
this.io.in(tableId).emit(GridSocketEvent.RowChange, { id, row: null })
this.emitToRoom(ctx, tableId, GridSocketEvent.RowChange, { id, row: null })
}
emitTableUpdate(table: Table) {
this.io
.in(table._id!)
.emit(GridSocketEvent.TableChange, { id: table._id, table })
emitTableUpdate(ctx: any, table: Table) {
this.emitToRoom(ctx, table._id!, GridSocketEvent.TableChange, {
id: table._id,
table,
})
}
emitTableDeletion(id: string) {
this.io.in(id).emit(GridSocketEvent.TableChange, { id, table: null })
emitTableDeletion(ctx: any, id: string) {
this.emitToRoom(ctx, id, GridSocketEvent.TableChange, { id, table: null })
}
}

View file

@ -3,7 +3,7 @@ import http from "http"
import Koa from "koa"
import Cookies from "cookies"
import { userAgent } from "koa-useragent"
import { auth, redis } from "@budibase/backend-core"
import { auth, Header, redis } from "@budibase/backend-core"
import currentApp from "../middleware/currentapp"
import { createAdapter } from "@socket.io/redis-adapter"
import { Socket } from "socket.io"
@ -271,4 +271,13 @@ export class BaseSocket {
emit(event: string, payload: any) {
this.io.sockets.emit(event, payload)
}
// Emit an event to everyone in a room, including metadata of whom
// the originator of the request was
emitToRoom(ctx: any, room: string, event: string, payload: any) {
this.io.in(room).emit(event, {
...payload,
apiSessionId: ctx.headers?.[Header.SESSION_ID],
})
}
}

View file

@ -11,7 +11,6 @@ import {
tenancy,
} from "@budibase/backend-core"
import { checkAnyUserExists } from "../../../utilities/users"
import { getLicensedConfig } from "../../../utilities/configs"
import {
Config,
ConfigType,