diff --git a/packages/bbui/src/Button/Button.svelte b/packages/bbui/src/Button/Button.svelte index efd5f33bd2..91affdb6c7 100644 --- a/packages/bbui/src/Button/Button.svelte +++ b/packages/bbui/src/Button/Button.svelte @@ -16,8 +16,6 @@ export let tooltip = undefined export let newStyles = true export let id - - let showTooltip = false - {/if} - - -
- {#each deployments as deployment} -
-
- - {formatDate(deployment.updatedAt, "fullDate")} - - - {formatDate(deployment.updatedAt, "timeOnly")} - -
-
- {#if deployment.status.toLowerCase() === "pending"} - - {/if} -
showErrorReasonModal(deployment.err)} - class={`deployment-status ${deployment.status}`} - > - - {deployment.status} - {#if deployment.status === DeploymentStatus.FAILURE} - - {/if} - -
-
-
- {/each} -
- -{/if} - - - - - - {errorReason} - - - - diff --git a/packages/builder/src/components/deploy/utils.js b/packages/builder/src/components/deploy/utils.js deleted file mode 100644 index cb254f0dbf..0000000000 --- a/packages/builder/src/components/deploy/utils.js +++ /dev/null @@ -1,25 +0,0 @@ -export const DeploymentStatus = { - SUCCESS: "SUCCESS", - PENDING: "PENDING", - FAILURE: "FAILURE", -} - -// Required to check any updated deployment statuses between polls -export function checkIncomingDeploymentStatus(current, incoming) { - return incoming.reduce((acc, incomingDeployment) => { - if (incomingDeployment.status === DeploymentStatus.FAILURE) { - const currentDeployment = current.find( - deployment => deployment._id === incomingDeployment._id - ) - - //We have just been notified of an ongoing deployments failure - if ( - !currentDeployment || - currentDeployment.status === DeploymentStatus.PENDING - ) { - acc.push(incomingDeployment) - } - } - return acc - }, []) -} diff --git a/packages/builder/src/components/portal/page/SideNavItem.svelte b/packages/builder/src/components/portal/page/SideNavItem.svelte index ff627acfc5..b77d516ebd 100644 --- a/packages/builder/src/components/portal/page/SideNavItem.svelte +++ b/packages/builder/src/components/portal/page/SideNavItem.svelte @@ -1,32 +1,62 @@ -{#if url} - - {text || ""} - -{:else} - - - {text || ""} - -{/if} +
+ {#if url} + + {text || ""} + + {:else} + +
+ {text || ""} +
+ {/if} + {#if tooltip} +
+ +
+ {/if} +
diff --git a/packages/builder/src/helpers/urlStateSync.js b/packages/builder/src/helpers/urlStateSync.js index 47f3438468..c4c48fb3fb 100644 --- a/packages/builder/src/helpers/urlStateSync.js +++ b/packages/builder/src/helpers/urlStateSync.js @@ -114,26 +114,24 @@ export const syncURLToState = options => { // Updates the URL with new state values const mapStateToUrl = state => { - let needsUpdate = false const urlValue = cachedParams?.[urlParam] const stateValue = state?.[stateKey] - if (stateValue !== urlValue) { - needsUpdate = true - log(`url.${urlParam} (${urlValue}) <= state.${stateKey} (${stateValue})`) - if (validate && fallbackUrl) { - if (!validate(stateValue)) { - log("Invalid state param!", stateValue) - redirectUrl(fallbackUrl) - return - } + + // As the store updated, validate that the current state value is valid + if (validate && fallbackUrl) { + if (!validate(stateValue)) { + log("Invalid state param!", stateValue) + redirectUrl(fallbackUrl) + return } } // Avoid updating the URL if not necessary to prevent a wasted render // cycle - if (!needsUpdate) { + if (stateValue === urlValue) { return } + log(`url.${urlParam} (${urlValue}) <= state.${stateKey} (${stateValue})`) // Navigate to the new URL if (!get(isChangingPage)) { diff --git a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte index 3249ca2514..721055d78a 100644 --- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte @@ -21,6 +21,7 @@ import { Constants, Utils } from "@budibase/frontend-core" import { emailValidator } from "helpers/validation" import { roles } from "stores/backend" + import { fly } from "svelte/transition" let query = null let loaded = false @@ -418,16 +419,14 @@
{ - store.update(state => { - state.builderSidePanel = false - return state - }) - } - : () => {}} + use:clickOutside={() => { + store.update(state => { + state.builderSidePanel = false + return state + }) + }} >
Users @@ -737,12 +736,11 @@ flex-direction: column; overflow-y: auto; overflow-x: hidden; - transition: transform 130ms ease-out; position: absolute; width: 400px; right: 0; - transform: translateX(100%); height: 100%; + box-shadow: 0 0 40px 10px rgba(0, 0, 0, 0.1); } .builder-side-panel-header, @@ -792,11 +790,6 @@ font-style: normal; } - #builder-side-panel-container.open { - transform: translateX(0); - box-shadow: 0 0 40px 10px rgba(0, 0, 0, 0.1); - } - .builder-side-panel-header { display: flex; flex-direction: row; diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index 0d5942c39e..e6c79335a9 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -1,7 +1,12 @@
store.actions.screens.select(screen._id)} rightAlignIcon showTooltip + selectedBy={$userSelectedResourceMap[screen._id]} > diff --git a/packages/builder/src/pages/builder/app/[application]/design/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/design/_layout.svelte index d23514ae6d..ec21d909aa 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_layout.svelte @@ -1,14 +1,2 @@ - - diff --git a/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte index 225e3977c3..de65333f6f 100644 --- a/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte @@ -3,6 +3,7 @@ import { Page, Layout } from "@budibase/bbui" import { url, isActive } from "@roxi/routify" import DeleteModal from "components/deploy/DeleteModal.svelte" + import { isOnlyUser } from "builderStore" let deleteModal @@ -49,6 +50,10 @@ on:click={() => { deleteModal.show() }} + disabled={!$isOnlyUser} + tooltip={$isOnlyUser + ? null + : "Unavailable - another user is editing this app"} />
@@ -61,7 +66,7 @@ diff --git a/packages/frontend-core/src/components/grid/stores/users.js b/packages/frontend-core/src/components/grid/stores/users.js index b6e74ef276..5a39f3769a 100644 --- a/packages/frontend-core/src/components/grid/stores/users.js +++ b/packages/frontend-core/src/components/grid/stores/users.js @@ -30,8 +30,9 @@ export const deriveStores = context => { ([$users, $focusedCellId]) => { let map = {} $users.forEach(user => { - if (user.focusedCellId && user.focusedCellId !== $focusedCellId) { - map[user.focusedCellId] = user + const cellId = user.gridMetadata?.focusedCellId + if (cellId && cellId !== $focusedCellId) { + map[cellId] = user } }) return map diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index 53453b8538..c068a422b0 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -53,6 +53,7 @@ import { } from "@budibase/types" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import sdk from "../../sdk" +import { builderSocket } from "../../websockets" // utility function, need to do away with this async function getLayouts() { @@ -439,6 +440,14 @@ export async function update(ctx: UserCtx) { await events.app.updated(app) ctx.status = 200 ctx.body = app + builderSocket?.emitAppMetadataUpdate(ctx, { + theme: app.theme, + customTheme: app.customTheme, + navigation: app.navigation, + name: app.name, + url: app.url, + icon: app.icon, + }) } export async function updateClient(ctx: UserCtx) { @@ -569,6 +578,7 @@ export async function unpublish(ctx: UserCtx) { await unpublishApp(ctx) await postDestroyApp(ctx) ctx.status = 204 + builderSocket?.emitAppUnpublish(ctx) } export async function sync(ctx: UserCtx) { diff --git a/packages/server/src/api/controllers/deploy/index.ts b/packages/server/src/api/controllers/deploy/index.ts index 6a31998c14..a49434bbd1 100644 --- a/packages/server/src/api/controllers/deploy/index.ts +++ b/packages/server/src/api/controllers/deploy/index.ts @@ -9,6 +9,7 @@ import { import { backups } from "@budibase/pro" import { AppBackupTrigger } from "@budibase/types" import sdk from "../../../sdk" +import { builderSocket } from "../../../websockets" // the max time we can wait for an invalidation to complete before considering it failed const MAX_PENDING_TIME_MS = 30 * 60000 @@ -201,4 +202,5 @@ export const publishApp = async function (ctx: any) { await events.app.published(app) ctx.body = deployment + builderSocket?.emitAppPublish(ctx) } diff --git a/packages/server/src/api/controllers/screen.ts b/packages/server/src/api/controllers/screen.ts index 9cbd019d6e..ddfec91c0c 100644 --- a/packages/server/src/api/controllers/screen.ts +++ b/packages/server/src/api/controllers/screen.ts @@ -8,6 +8,7 @@ import { } from "@budibase/backend-core" import { updateAppPackage } from "./application" import { Plugin, ScreenProps, BBContext } from "@budibase/types" +import { builderSocket } from "../../websockets" export async function fetch(ctx: BBContext) { const db = context.getAppDB() @@ -87,13 +88,17 @@ export async function save(ctx: BBContext) { if (eventFn) { await eventFn(screen) } - ctx.message = `Screen ${screen.name} saved.` - ctx.body = { + const savedScreen = { ...screen, _id: response.id, _rev: response.rev, + } + ctx.message = `Screen ${screen.name} saved.` + ctx.body = { + ...savedScreen, pluginAdded, } + builderSocket?.emitScreenUpdate(ctx, savedScreen) } export async function destroy(ctx: BBContext) { @@ -108,6 +113,7 @@ export async function destroy(ctx: BBContext) { message: "Screen deleted successfully", } ctx.status = 200 + builderSocket?.emitScreenDeletion(ctx, id) } function findPlugins(component: ScreenProps, foundPlugins: string[]) { diff --git a/packages/server/src/websockets/builder.ts b/packages/server/src/websockets/builder.ts index 2524d9608b..cff42ce178 100644 --- a/packages/server/src/websockets/builder.ts +++ b/packages/server/src/websockets/builder.ts @@ -3,11 +3,18 @@ import { BaseSocket } from "./websocket" import { permissions, events } from "@budibase/backend-core" import http from "http" import Koa from "koa" -import { Datasource, Table, SocketSession, ContextUser } from "@budibase/types" +import { + Datasource, + Table, + SocketSession, + ContextUser, + Screen, + App, +} from "@budibase/types" import { gridSocket } from "./index" import { clearLock, updateLock } from "../utilities/redis" import { Socket } from "socket.io" -import { BuilderSocketEvent } from "@budibase/shared-core" +import { BuilderSocketEvent, GridSocketEvent } from "@budibase/shared-core" export default class BuilderSocket extends BaseSocket { constructor(app: Koa, server: http.Server) { @@ -32,6 +39,11 @@ export default class BuilderSocket extends BaseSocket { // Reply with all current sessions callback({ users: sessions }) }) + + // Handle users selecting a new cell + socket?.on(BuilderSocketEvent.SelectResource, ({ resourceId }) => { + this.updateUser(socket, { selectedResourceId: resourceId }) + }) } async onDisconnect(socket: Socket) { @@ -72,6 +84,15 @@ export default class BuilderSocket extends BaseSocket { } } + async updateUser(socket: Socket, patch: Object) { + await super.updateUser(socket, { + builderMetadata: { + ...socket.data.builderMetadata, + ...patch, + }, + }) + } + emitTableUpdate(ctx: any, table: Table) { this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.TableChange, { id: table._id, @@ -101,4 +122,38 @@ export default class BuilderSocket extends BaseSocket { datasource: null, }) } + + emitScreenUpdate(ctx: any, screen: Screen) { + this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.ScreenChange, { + id: screen._id, + screen, + }) + } + + emitScreenDeletion(ctx: any, id: string) { + this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.ScreenChange, { + id, + screen: null, + }) + } + + emitAppMetadataUpdate(ctx: any, metadata: Partial) { + this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.AppMetadataChange, { + metadata, + }) + } + + emitAppPublish(ctx: any) { + this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.AppPublishChange, { + published: true, + user: ctx.user, + }) + } + + emitAppUnpublish(ctx: any) { + this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.AppPublishChange, { + published: false, + user: ctx.user, + }) + } } diff --git a/packages/server/src/websockets/grid.ts b/packages/server/src/websockets/grid.ts index ffe26828bc..f95137ee08 100644 --- a/packages/server/src/websockets/grid.ts +++ b/packages/server/src/websockets/grid.ts @@ -69,6 +69,15 @@ export default class GridSocket extends BaseSocket { }) } + async updateUser(socket: Socket, patch: Object) { + await super.updateUser(socket, { + gridMetadata: { + ...socket.data.gridMetadata, + ...patch, + }, + }) + } + emitRowUpdate(ctx: any, row: Row) { const tableId = getTableId(ctx) const room = `${ctx.appId}-${tableId}` diff --git a/packages/shared-core/src/constants.ts b/packages/shared-core/src/constants.ts index 307285012b..ce6b8d94fa 100644 --- a/packages/shared-core/src/constants.ts +++ b/packages/shared-core/src/constants.ts @@ -86,6 +86,10 @@ export enum BuilderSocketEvent { TableChange = "TableChange", DatasourceChange = "DatasourceChange", LockTransfer = "LockTransfer", + ScreenChange = "ScreenChange", + AppMetadataChange = "AppMetadataChange", + SelectResource = "SelectResource", + AppPublishChange = "AppPublishChange", } export const SocketSessionTTL = 60