Merge pull request #11135 from Budibase/design-collab
Multi-user collaboration for everything except automations
This commit is contained in:
commit
0ef0da6b78
|
@ -16,8 +16,6 @@
|
||||||
export let tooltip = undefined
|
export let tooltip = undefined
|
||||||
export let newStyles = true
|
export let newStyles = true
|
||||||
export let id
|
export let id
|
||||||
|
|
||||||
let showTooltip = false
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -35,9 +33,6 @@
|
||||||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click|preventDefault
|
on:click|preventDefault
|
||||||
on:mouseover={() => (showTooltip = true)}
|
|
||||||
on:focus={() => (showTooltip = true)}
|
|
||||||
on:mouseleave={() => (showTooltip = false)}
|
|
||||||
>
|
>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<svg
|
<svg
|
||||||
|
@ -52,19 +47,7 @@
|
||||||
{#if $$slots}
|
{#if $$slots}
|
||||||
<span class="spectrum-Button-label"><slot /></span>
|
<span class="spectrum-Button-label"><slot /></span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !disabled && tooltip}
|
{#if tooltip}
|
||||||
<div class="tooltip-icon">
|
|
||||||
<svg
|
|
||||||
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
|
||||||
focusable="false"
|
|
||||||
aria-hidden="true"
|
|
||||||
aria-label="Info"
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-icon-18-InfoOutline" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if showTooltip && tooltip}
|
|
||||||
<div class="tooltip">
|
<div class="tooltip">
|
||||||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -75,7 +58,6 @@
|
||||||
button {
|
button {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spectrum-Button-label {
|
.spectrum-Button-label {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -93,11 +75,13 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: calc(100% - 3px);
|
top: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 130ms ease-out;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.tooltip-icon {
|
button:hover .tooltip {
|
||||||
padding-left: var(--spacing-m);
|
opacity: 1;
|
||||||
line-height: 0;
|
|
||||||
}
|
}
|
||||||
.spectrum-Button--primary.new-styles {
|
.spectrum-Button--primary.new-styles {
|
||||||
background: var(--spectrum-global-color-gray-800);
|
background: var(--spectrum-global-color-gray-800);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import "@spectrum-css/link/dist/index-vars.css"
|
import "@spectrum-css/link/dist/index-vars.css"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||||
|
|
||||||
export let href = "#"
|
export let href = "#"
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
|
@ -10,18 +11,61 @@
|
||||||
export let overBackground = false
|
export let overBackground = false
|
||||||
export let target
|
export let target
|
||||||
export let download
|
export let download
|
||||||
|
export let disabled = false
|
||||||
|
export let tooltip = null
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const onClick = e => {
|
||||||
|
if (!disabled) {
|
||||||
|
dispatch("click")
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
on:click={e => dispatch("click") && e.stopPropagation()}
|
on:click={onClick}
|
||||||
{href}
|
{href}
|
||||||
{target}
|
{target}
|
||||||
{download}
|
{download}
|
||||||
|
class:disabled
|
||||||
class:spectrum-Link--primary={primary}
|
class:spectrum-Link--primary={primary}
|
||||||
class:spectrum-Link--secondary={secondary}
|
class:spectrum-Link--secondary={secondary}
|
||||||
class:spectrum-Link--overBackground={overBackground}
|
class:spectrum-Link--overBackground={overBackground}
|
||||||
class:spectrum-Link--quiet={quiet}
|
class:spectrum-Link--quiet={quiet}
|
||||||
class="spectrum-Link spectrum-Link--size{size}"><slot /></a
|
class="spectrum-Link spectrum-Link--size{size}"
|
||||||
>
|
>
|
||||||
|
<slot />
|
||||||
|
{#if tooltip}
|
||||||
|
<div class="tooltip">
|
||||||
|
<Tooltip textWrapping direction="bottom" text={tooltip} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
a.disabled {
|
||||||
|
color: var(--spectrum-global-color-gray-500);
|
||||||
|
}
|
||||||
|
a.disabled:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 100%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: 130ms ease-out;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
a:hover .tooltip {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -90,6 +90,6 @@
|
||||||
.spectrum-Popover {
|
.spectrum-Popover {
|
||||||
min-width: var(--spectrum-global-dimension-size-2000);
|
min-width: var(--spectrum-global-dimension-size-2000);
|
||||||
border-color: var(--spectrum-global-color-gray-300);
|
border-color: var(--spectrum-global-color-gray-300);
|
||||||
overflow: auto;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { getAutomationStore } from "./store/automation"
|
||||||
import { getTemporalStore } from "./store/temporal"
|
import { getTemporalStore } from "./store/temporal"
|
||||||
import { getThemeStore } from "./store/theme"
|
import { getThemeStore } from "./store/theme"
|
||||||
import { getUserStore } from "./store/users"
|
import { getUserStore } from "./store/users"
|
||||||
|
import { getDeploymentStore } from "./store/deployments"
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import { findComponent, findComponentPath } from "./componentUtils"
|
import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
@ -14,6 +15,7 @@ export const automationStore = getAutomationStore()
|
||||||
export const themeStore = getThemeStore()
|
export const themeStore = getThemeStore()
|
||||||
export const temporalStore = getTemporalStore()
|
export const temporalStore = getTemporalStore()
|
||||||
export const userStore = getUserStore()
|
export const userStore = getUserStore()
|
||||||
|
export const deploymentStore = getDeploymentStore()
|
||||||
|
|
||||||
// Setup history for screens
|
// Setup history for screens
|
||||||
export const screenHistoryStore = createHistoryStore({
|
export const screenHistoryStore = createHistoryStore({
|
||||||
|
@ -118,3 +120,20 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
|
||||||
x => x._id === $automationStore.selectedAutomationId
|
x => x._id === $automationStore.selectedAutomationId
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Derive map of resource IDs to other users.
|
||||||
|
// We only ever care about a single user in each resource, so if multiple users
|
||||||
|
// share the same datasource we can just overwrite them.
|
||||||
|
export const userSelectedResourceMap = derived(userStore, $userStore => {
|
||||||
|
let map = {}
|
||||||
|
$userStore.forEach(user => {
|
||||||
|
if (user.builderMetadata?.selectedResourceId) {
|
||||||
|
map[user.builderMetadata?.selectedResourceId] = user
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isOnlyUser = derived(userStore, $userStore => {
|
||||||
|
return $userStore.length === 1
|
||||||
|
})
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { API } from "api"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export const getDeploymentStore = () => {
|
||||||
|
let store = writable([])
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
store.set(await API.getAppDeployments())
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error fetching deployments")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: {
|
||||||
|
load,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ import {
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { getComponentFieldOptions } from "helpers/formFields"
|
import { getComponentFieldOptions } from "helpers/formFields"
|
||||||
import { createBuilderWebsocket } from "builderStore/websocket"
|
import { createBuilderWebsocket } from "builderStore/websocket"
|
||||||
|
import { BuilderSocketEvent } from "@budibase/shared-core"
|
||||||
|
|
||||||
const INITIAL_FRONTEND_STATE = {
|
const INITIAL_FRONTEND_STATE = {
|
||||||
initialised: false,
|
initialised: false,
|
||||||
|
@ -353,6 +354,33 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
return await sequentialScreenPatch(patchFn, screenId)
|
return await sequentialScreenPatch(patchFn, screenId)
|
||||||
},
|
},
|
||||||
|
replace: async (screenId, screen) => {
|
||||||
|
if (!screenId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!screen) {
|
||||||
|
// Screen deletion
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
screens: state.screens.filter(x => x._id !== screenId),
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
const index = get(store).screens.findIndex(x => x._id === screen._id)
|
||||||
|
if (index === -1) {
|
||||||
|
// Screen addition
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
screens: [...state.screens, screen],
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
// Screen update
|
||||||
|
store.update(state => {
|
||||||
|
state.screens[index] = screen
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
delete: async screens => {
|
delete: async screens => {
|
||||||
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
||||||
|
|
||||||
|
@ -1365,6 +1393,21 @@ export const getFrontendStore = () => {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
websocket: {
|
||||||
|
selectResource: id => {
|
||||||
|
websocket.emit(BuilderSocketEvent.SelectResource, {
|
||||||
|
resourceId: id,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
replace: metadata => {
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
...metadata,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return store
|
return store
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { createWebsocket } from "@budibase/frontend-core"
|
import { createWebsocket } from "@budibase/frontend-core"
|
||||||
import { userStore, store } from "builderStore"
|
import { userStore, store, deploymentStore } from "builderStore"
|
||||||
import { datasources, tables } from "stores/backend"
|
import { datasources, tables } from "stores/backend"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
|
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
|
||||||
|
import { apps } from "stores/portal"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
export const createBuilderWebsocket = appId => {
|
export const createBuilderWebsocket = appId => {
|
||||||
const socket = createWebsocket("/socket/builder")
|
const socket = createWebsocket("/socket/builder")
|
||||||
|
@ -31,7 +33,6 @@ export const createBuilderWebsocket = appId => {
|
||||||
})
|
})
|
||||||
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
|
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
|
||||||
if (userId === get(auth)?.user?._id) {
|
if (userId === get(auth)?.user?._id) {
|
||||||
notifications.success("You can now edit screens and automations")
|
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
hasLock: true,
|
hasLock: true,
|
||||||
|
@ -39,15 +40,32 @@ export const createBuilderWebsocket = appId => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Table events
|
// Data section events
|
||||||
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
|
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
|
||||||
tables.replaceTable(id, table)
|
tables.replaceTable(id, table)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Datasource events
|
|
||||||
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
|
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
|
||||||
datasources.replaceDatasource(id, datasource)
|
datasources.replaceDatasource(id, datasource)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Design section events
|
||||||
|
socket.onOther(BuilderSocketEvent.ScreenChange, ({ id, screen }) => {
|
||||||
|
store.actions.screens.replace(id, screen)
|
||||||
|
})
|
||||||
|
socket.onOther(BuilderSocketEvent.AppMetadataChange, ({ metadata }) => {
|
||||||
|
store.actions.metadata.replace(metadata)
|
||||||
|
})
|
||||||
|
socket.onOther(
|
||||||
|
BuilderSocketEvent.AppPublishChange,
|
||||||
|
async ({ user, published }) => {
|
||||||
|
await apps.load()
|
||||||
|
if (published) {
|
||||||
|
await deploymentStore.actions.load()
|
||||||
|
}
|
||||||
|
const verb = published ? "published" : "unpublished"
|
||||||
|
notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return socket
|
return socket
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
} from "helpers/data/utils"
|
} from "helpers/data/utils"
|
||||||
import IntegrationIcon from "./IntegrationIcon.svelte"
|
import IntegrationIcon from "./IntegrationIcon.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
|
import { userSelectedResourceMap } from "builderStore"
|
||||||
|
|
||||||
let openDataSources = []
|
let openDataSources = []
|
||||||
|
|
||||||
|
@ -166,6 +167,7 @@
|
||||||
selected={$isActive("./table/:tableId") &&
|
selected={$isActive("./table/:tableId") &&
|
||||||
$tables.selected?._id === TableNames.USERS}
|
$tables.selected?._id === TableNames.USERS}
|
||||||
on:click={() => selectTable(TableNames.USERS)}
|
on:click={() => selectTable(TableNames.USERS)}
|
||||||
|
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
|
||||||
/>
|
/>
|
||||||
{#each enrichedDataSources as datasource, idx}
|
{#each enrichedDataSources as datasource, idx}
|
||||||
<NavItem
|
<NavItem
|
||||||
|
@ -176,6 +178,7 @@
|
||||||
withArrow={true}
|
withArrow={true}
|
||||||
on:click={() => selectDatasource(datasource)}
|
on:click={() => selectDatasource(datasource)}
|
||||||
on:iconClick={() => toggleNode(datasource)}
|
on:iconClick={() => toggleNode(datasource)}
|
||||||
|
selectedBy={$userSelectedResourceMap[datasource._id]}
|
||||||
>
|
>
|
||||||
<div class="datasource-icon" slot="icon">
|
<div class="datasource-icon" slot="icon">
|
||||||
<IntegrationIcon
|
<IntegrationIcon
|
||||||
|
@ -201,6 +204,7 @@
|
||||||
selected={$isActive("./query/:queryId") &&
|
selected={$isActive("./query/:queryId") &&
|
||||||
$queries.selectedQueryId === query._id}
|
$queries.selectedQueryId === query._id}
|
||||||
on:click={() => $goto(`./query/${query._id}`)}
|
on:click={() => $goto(`./query/${query._id}`)}
|
||||||
|
selectedBy={$userSelectedResourceMap[query._id]}
|
||||||
>
|
>
|
||||||
<EditQueryPopover {query} />
|
<EditQueryPopover {query} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
@ -212,7 +216,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.hierarchy-items-container {
|
.hierarchy-items-container {
|
||||||
margin: 0 calc(-1 * var(--spacing-xl));
|
margin: 0 calc(-1 * var(--spacing-l));
|
||||||
}
|
}
|
||||||
.datasource-icon {
|
.datasource-icon {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import { goto, isActive } from "@roxi/routify"
|
import { goto, isActive } from "@roxi/routify"
|
||||||
|
import { userSelectedResourceMap } from "builderStore"
|
||||||
|
|
||||||
const alphabetical = (a, b) =>
|
const alphabetical = (a, b) =>
|
||||||
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
selected={$isActive("./table/:tableId") &&
|
selected={$isActive("./table/:tableId") &&
|
||||||
$tables.selected?._id === table._id}
|
$tables.selected?._id === table._id}
|
||||||
on:click={() => selectTable(table._id)}
|
on:click={() => selectTable(table._id)}
|
||||||
|
selectedBy={$userSelectedResourceMap[table._id]}
|
||||||
>
|
>
|
||||||
{#if table._id !== TableNames.USERS}
|
{#if table._id !== TableNames.USERS}
|
||||||
<EditTablePopover {table} />
|
<EditTablePopover {table} />
|
||||||
|
@ -42,6 +44,7 @@
|
||||||
text={viewName}
|
text={viewName}
|
||||||
selected={$isActive("./view") && $views.selected?.name === viewName}
|
selected={$isActive("./view") && $views.selected?.name === viewName}
|
||||||
on:click={() => $goto(`./view/${encodeURIComponent(viewName)}`)}
|
on:click={() => $goto(`./view/${encodeURIComponent(viewName)}`)}
|
||||||
|
selectedBy={$userSelectedResourceMap[viewName]}
|
||||||
>
|
>
|
||||||
<EditViewPopover
|
<EditViewPopover
|
||||||
view={{ name: viewName, ...table.views[viewName] }}
|
view={{ name: viewName, ...table.views[viewName] }}
|
||||||
|
|
|
@ -208,8 +208,8 @@
|
||||||
|
|
||||||
async function deployApp() {
|
async function deployApp() {
|
||||||
try {
|
try {
|
||||||
await API.deployAppChanges()
|
await API.publishAppChanges($store.appId)
|
||||||
notifications.success("Application published successfully")
|
notifications.success("App published successfully")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error publishing app")
|
notifications.error("Error publishing app")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher, getContext } from "svelte"
|
import { createEventDispatcher, getContext } from "svelte"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let icon
|
export let icon
|
||||||
export let withArrow = false
|
export let withArrow = false
|
||||||
|
@ -18,12 +19,15 @@
|
||||||
export let rightAlignIcon = false
|
export let rightAlignIcon = false
|
||||||
export let id
|
export let id
|
||||||
export let showTooltip = false
|
export let showTooltip = false
|
||||||
|
export let selectedBy = null
|
||||||
|
|
||||||
const scrollApi = getContext("scroll")
|
const scrollApi = getContext("scroll")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let contentRef
|
let contentRef
|
||||||
|
|
||||||
$: selected && contentRef && scrollToView()
|
$: selected && contentRef && scrollToView()
|
||||||
|
$: style = getStyle(indentLevel, selectedBy)
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
scrollToView()
|
scrollToView()
|
||||||
|
@ -42,6 +46,14 @@
|
||||||
const bounds = contentRef.getBoundingClientRect()
|
const bounds = contentRef.getBoundingClientRect()
|
||||||
scrollApi.scrollTo(bounds)
|
scrollApi.scrollTo(bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStyle = (indentLevel, selectedBy) => {
|
||||||
|
let style = `padding-left:calc(${indentLevel * 14}px);`
|
||||||
|
if (selectedBy) {
|
||||||
|
style += `--selected-by-color:${helpers.getUserColor(selectedBy)};`
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -51,8 +63,7 @@
|
||||||
class:withActions
|
class:withActions
|
||||||
class:scrollable
|
class:scrollable
|
||||||
class:highlighted
|
class:highlighted
|
||||||
style={`padding-left: calc(${indentLevel * 14}px)`}
|
class:selectedBy
|
||||||
{draggable}
|
|
||||||
on:dragend
|
on:dragend
|
||||||
on:dragstart
|
on:dragstart
|
||||||
on:dragover
|
on:dragover
|
||||||
|
@ -61,6 +72,8 @@
|
||||||
ondragover="return false"
|
ondragover="return false"
|
||||||
ondragenter="return false"
|
ondragenter="return false"
|
||||||
{id}
|
{id}
|
||||||
|
{style}
|
||||||
|
{draggable}
|
||||||
>
|
>
|
||||||
<div class="nav-item-content" bind:this={contentRef}>
|
<div class="nav-item-content" bind:this={contentRef}>
|
||||||
{#if withArrow}
|
{#if withArrow}
|
||||||
|
@ -97,6 +110,9 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if selectedBy}
|
||||||
|
<div class="selected-by-label">{helpers.getUserLabel(selectedBy)}</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -111,6 +127,7 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.nav-item.scrollable {
|
.nav-item.scrollable {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -142,6 +159,37 @@
|
||||||
padding-left: var(--spacing-l);
|
padding-left: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Selected user styles */
|
||||||
|
.nav-item.selectedBy:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: calc(100% - 4px);
|
||||||
|
height: 28px;
|
||||||
|
border: 2px solid var(--selected-by-color);
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.selected-by-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--selected-by-color);
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: white;
|
||||||
|
transform: translateY(calc(1px - 100%));
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 130ms ease-out;
|
||||||
|
}
|
||||||
|
.nav-item.selectedBy:hover .selected-by-label {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Needed to fully display the actions icon */
|
/* Needed to fully display the actions icon */
|
||||||
.nav-item.scrollable .nav-item-content {
|
.nav-item.scrollable .nav-item-content {
|
||||||
padding-right: 1px;
|
padding-right: 1px;
|
||||||
|
|
|
@ -14,15 +14,12 @@
|
||||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import analytics, { Events, EventSource } from "analytics"
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { apps } from "stores/portal"
|
import { apps } from "stores/portal"
|
||||||
import { store } from "builderStore"
|
import { deploymentStore, store, isOnlyUser } from "builderStore"
|
||||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||||
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
@ -34,37 +31,31 @@
|
||||||
let updateAppModal
|
let updateAppModal
|
||||||
let revertModal
|
let revertModal
|
||||||
let versionModal
|
let versionModal
|
||||||
|
|
||||||
let appActionPopover
|
let appActionPopover
|
||||||
let appActionPopoverOpen = false
|
let appActionPopoverOpen = false
|
||||||
let appActionPopoverAnchor
|
let appActionPopoverAnchor
|
||||||
|
|
||||||
let publishing = false
|
let publishing = false
|
||||||
|
|
||||||
$: filteredApps = $apps.filter(app => app.devId === application)
|
$: filteredApps = $apps.filter(app => app.devId === application)
|
||||||
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||||
|
$: latestDeployments = $deploymentStore
|
||||||
$: deployments = []
|
|
||||||
$: latestDeployments = deployments
|
|
||||||
.filter(deployment => deployment.status === "SUCCESS")
|
.filter(deployment => deployment.status === "SUCCESS")
|
||||||
.sort((a, b) => a.updatedAt > b.updatedAt)
|
.sort((a, b) => a.updatedAt > b.updatedAt)
|
||||||
|
|
||||||
$: isPublished =
|
$: isPublished =
|
||||||
selectedApp?.status === "published" && latestDeployments?.length > 0
|
selectedApp?.status === "published" && latestDeployments?.length > 0
|
||||||
|
|
||||||
$: updateAvailable =
|
$: updateAvailable =
|
||||||
$store.upgradableVersion &&
|
$store.upgradableVersion &&
|
||||||
$store.version &&
|
$store.version &&
|
||||||
$store.upgradableVersion !== $store.version
|
$store.upgradableVersion !== $store.version
|
||||||
|
|
||||||
$: canPublish = !publishing && loaded
|
$: canPublish = !publishing && loaded
|
||||||
|
$: lastDeployed = getLastDeployedString($deploymentStore)
|
||||||
|
|
||||||
const initialiseApp = async () => {
|
const initialiseApp = async () => {
|
||||||
const applicationPkg = await API.fetchAppPackage($store.devId)
|
const applicationPkg = await API.fetchAppPackage($store.devId)
|
||||||
await store.actions.initialise(applicationPkg)
|
await store.actions.initialise(applicationPkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateDeploymentString = () => {
|
const getLastDeployedString = deployments => {
|
||||||
return deployments?.length
|
return deployments?.length
|
||||||
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
|
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
|
||||||
time:
|
time:
|
||||||
|
@ -73,27 +64,6 @@
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const reviewPendingDeployments = (deployments, newDeployments) => {
|
|
||||||
if (deployments.length > 0) {
|
|
||||||
const pending = checkIncomingDeploymentStatus(deployments, newDeployments)
|
|
||||||
if (pending.length) {
|
|
||||||
notifications.warning(
|
|
||||||
"Deployment has been queued and will be processed shortly"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchDeployments() {
|
|
||||||
try {
|
|
||||||
const newDeployments = await API.getAppDeployments()
|
|
||||||
reviewPendingDeployments(deployments, newDeployments)
|
|
||||||
return newDeployments
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error("Error fetching deployment overview")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const previewApp = () => {
|
const previewApp = () => {
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
|
@ -116,14 +86,11 @@
|
||||||
async function publishApp() {
|
async function publishApp() {
|
||||||
try {
|
try {
|
||||||
publishing = true
|
publishing = true
|
||||||
|
|
||||||
await API.publishAppChanges($store.appId)
|
await API.publishAppChanges($store.appId)
|
||||||
|
notifications.send("App published successfully", {
|
||||||
notifications.send("App published", {
|
|
||||||
type: "success",
|
type: "success",
|
||||||
icon: "GlobeCheck",
|
icon: "GlobeCheck",
|
||||||
})
|
})
|
||||||
|
|
||||||
await completePublish()
|
await completePublish()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
@ -163,210 +130,199 @@
|
||||||
const completePublish = async () => {
|
const completePublish = async () => {
|
||||||
try {
|
try {
|
||||||
await apps.load()
|
await apps.load()
|
||||||
deployments = await fetchDeployments()
|
await deploymentStore.actions.load()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error("Error refreshing app")
|
notifications.error("Error refreshing app")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (!$apps.length) {
|
|
||||||
await apps.load()
|
|
||||||
}
|
|
||||||
deployments = await fetchDeployments()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $store.hasLock}
|
<div class="action-top-nav">
|
||||||
<div class="action-top-nav" class:has-lock={$store.hasLock}>
|
<div class="action-buttons">
|
||||||
<div class="action-buttons">
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
{#if updateAvailable && $isOnlyUser}
|
||||||
{#if updateAvailable}
|
<div class="app-action-button version" on:click={versionModal.show}>
|
||||||
<div class="app-action-button version" on:click={versionModal.show}>
|
|
||||||
<div class="app-action">
|
|
||||||
<ActionButton quiet>
|
|
||||||
<StatusLight notice />
|
|
||||||
Update
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<TourWrap
|
|
||||||
tourStepKey={$store.onboarding
|
|
||||||
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
|
||||||
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
|
||||||
>
|
|
||||||
<div class="app-action-button users">
|
|
||||||
<div class="app-action" id="builder-app-users-button">
|
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
icon="UserGroup"
|
|
||||||
on:click={() => {
|
|
||||||
store.update(state => {
|
|
||||||
state.builderSidePanel = true
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Users
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TourWrap>
|
|
||||||
|
|
||||||
<div class="app-action-button preview">
|
|
||||||
<div class="app-action">
|
<div class="app-action">
|
||||||
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
<ActionButton quiet>
|
||||||
Preview
|
<StatusLight notice />
|
||||||
|
Update
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<TourWrap
|
||||||
<div
|
tourStepKey={$store.onboarding
|
||||||
class="app-action-button publish app-action-popover"
|
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
||||||
on:click={() => {
|
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
||||||
if (!appActionPopoverOpen) {
|
>
|
||||||
appActionPopover.show()
|
<div class="app-action-button users">
|
||||||
} else {
|
<div class="app-action" id="builder-app-users-button">
|
||||||
appActionPopover.hide()
|
<ActionButton
|
||||||
}
|
quiet
|
||||||
}}
|
icon="UserGroup"
|
||||||
>
|
on:click={() => {
|
||||||
<div bind:this={appActionPopoverAnchor}>
|
store.update(state => {
|
||||||
<div class="app-action">
|
state.builderSidePanel = true
|
||||||
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
|
return state
|
||||||
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
|
})
|
||||||
<span class="publish-open" id="builder-app-publish-button">
|
}}
|
||||||
Publish
|
>
|
||||||
<Icon
|
Users
|
||||||
name={appActionPopoverOpen ? "ChevronUp" : "ChevronDown"}
|
</ActionButton>
|
||||||
size="M"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</TourWrap>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Popover
|
</div>
|
||||||
bind:this={appActionPopover}
|
</TourWrap>
|
||||||
align="right"
|
|
||||||
disabled={!isPublished}
|
|
||||||
anchor={appActionPopoverAnchor}
|
|
||||||
offset={35}
|
|
||||||
on:close={() => {
|
|
||||||
appActionPopoverOpen = false
|
|
||||||
}}
|
|
||||||
on:open={() => {
|
|
||||||
appActionPopoverOpen = true
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="app-action-popover-content">
|
|
||||||
<Layout noPadding gap="M">
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<Body size="M">
|
|
||||||
<span
|
|
||||||
class="app-link"
|
|
||||||
on:click={() => {
|
|
||||||
if (isPublished) {
|
|
||||||
viewApp()
|
|
||||||
} else {
|
|
||||||
appActionPopover.hide()
|
|
||||||
updateAppModal.show()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{$store.url}
|
|
||||||
{#if isPublished}
|
|
||||||
<Icon size="S" name="LinkOut" />
|
|
||||||
{:else}
|
|
||||||
<Icon size="S" name="Edit" />
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</Body>
|
|
||||||
|
|
||||||
<Body size="S">
|
<div class="app-action-button preview">
|
||||||
<span class="publish-popover-status">
|
<div class="app-action">
|
||||||
{#if isPublished}
|
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
||||||
<span class="status-text">
|
Preview
|
||||||
{updateDeploymentString(deployments)}
|
</ActionButton>
|
||||||
</span>
|
|
||||||
<span class="unpublish-link">
|
|
||||||
<Link quiet on:click={unpublishApp}>Unpublish</Link>
|
|
||||||
</span>
|
|
||||||
<span class="revert-link">
|
|
||||||
<Link quiet secondary on:click={revertApp}>Revert</Link>
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
<span class="status-text unpublished">Not published</span>
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</Body>
|
|
||||||
<div class="action-buttons">
|
|
||||||
{#if $store.hasLock}
|
|
||||||
{#if isPublished}
|
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
icon="Code"
|
|
||||||
on:click={() => {
|
|
||||||
$goto("./settings/embed")
|
|
||||||
appActionPopover.hide()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Embed
|
|
||||||
</ActionButton>
|
|
||||||
{/if}
|
|
||||||
<Button
|
|
||||||
cta
|
|
||||||
on:click={publishApp}
|
|
||||||
id={"builder-app-publish-button"}
|
|
||||||
disabled={!canPublish}
|
|
||||||
>
|
|
||||||
Publish
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modals -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<ConfirmDialog
|
<div
|
||||||
bind:this={unpublishModal}
|
class="app-action-button publish app-action-popover"
|
||||||
title="Confirm unpublish"
|
on:click={() => {
|
||||||
okText="Unpublish app"
|
if (!appActionPopoverOpen) {
|
||||||
onOk={confirmUnpublishApp}
|
appActionPopover.show()
|
||||||
>
|
} else {
|
||||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
appActionPopover.hide()
|
||||||
</ConfirmDialog>
|
}
|
||||||
|
|
||||||
<Modal bind:this={updateAppModal} padding={false} width="600px">
|
|
||||||
<UpdateAppModal
|
|
||||||
app={{
|
|
||||||
name: $store.name,
|
|
||||||
url: $store.url,
|
|
||||||
icon: $store.icon,
|
|
||||||
appId: $store.appId,
|
|
||||||
}}
|
}}
|
||||||
onUpdateComplete={async () => {
|
>
|
||||||
await initialiseApp()
|
<div bind:this={appActionPopoverAnchor}>
|
||||||
}}
|
<div class="app-action">
|
||||||
/>
|
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
|
||||||
</Modal>
|
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
|
||||||
|
<span class="publish-open" id="builder-app-publish-button">
|
||||||
|
Publish
|
||||||
|
<Icon
|
||||||
|
name={appActionPopoverOpen ? "ChevronUp" : "ChevronDown"}
|
||||||
|
size="M"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</TourWrap>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Popover
|
||||||
|
bind:this={appActionPopover}
|
||||||
|
align="right"
|
||||||
|
disabled={!isPublished}
|
||||||
|
anchor={appActionPopoverAnchor}
|
||||||
|
offset={35}
|
||||||
|
on:close={() => {
|
||||||
|
appActionPopoverOpen = false
|
||||||
|
}}
|
||||||
|
on:open={() => {
|
||||||
|
appActionPopoverOpen = true
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="app-action-popover-content">
|
||||||
|
<Layout noPadding gap="M">
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<Body size="M">
|
||||||
|
<span
|
||||||
|
class="app-link"
|
||||||
|
on:click={() => {
|
||||||
|
if (isPublished) {
|
||||||
|
viewApp()
|
||||||
|
} else {
|
||||||
|
appActionPopover.hide()
|
||||||
|
updateAppModal.show()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$store.url}
|
||||||
|
{#if isPublished}
|
||||||
|
<Icon size="S" name="LinkOut" />
|
||||||
|
{:else}
|
||||||
|
<Icon size="S" name="Edit" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
|
||||||
<RevertModal bind:this={revertModal} />
|
<Body size="S">
|
||||||
<VersionModal hideIcon bind:this={versionModal} />
|
<span class="publish-popover-status">
|
||||||
{:else}
|
{#if isPublished}
|
||||||
<div class="app-action-button preview-locked">
|
<span class="status-text">
|
||||||
<div class="app-action">
|
{lastDeployed}
|
||||||
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
</span>
|
||||||
Preview
|
<span class="unpublish-link">
|
||||||
</ActionButton>
|
<Link quiet on:click={unpublishApp}>Unpublish</Link>
|
||||||
|
</span>
|
||||||
|
<span class="revert-link">
|
||||||
|
<Link
|
||||||
|
disabled={!$isOnlyUser}
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
on:click={revertApp}
|
||||||
|
tooltip="Unavailable - another user is editing this app"
|
||||||
|
>
|
||||||
|
Revert
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="status-text unpublished">Not published</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
<div class="action-buttons">
|
||||||
|
{#if isPublished}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
icon="Code"
|
||||||
|
on:click={() => {
|
||||||
|
$goto("./settings/embed")
|
||||||
|
appActionPopover.hide()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Embed
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
on:click={publishApp}
|
||||||
|
id={"builder-app-publish-button"}
|
||||||
|
disabled={!canPublish}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
|
|
||||||
|
<!-- Modals -->
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={unpublishModal}
|
||||||
|
title="Confirm unpublish"
|
||||||
|
okText="Unpublish app"
|
||||||
|
onOk={confirmUnpublishApp}
|
||||||
|
>
|
||||||
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<Modal bind:this={updateAppModal} padding={false} width="600px">
|
||||||
|
<UpdateAppModal
|
||||||
|
app={{
|
||||||
|
name: $store.name,
|
||||||
|
url: $store.url,
|
||||||
|
icon: $store.icon,
|
||||||
|
appId: $store.appId,
|
||||||
|
}}
|
||||||
|
onUpdateComplete={async () => {
|
||||||
|
await initialiseApp()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<RevertModal bind:this={revertModal} />
|
||||||
|
<VersionModal hideIcon bind:this={versionModal} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.app-action-popover-content {
|
.app-action-popover-content {
|
||||||
|
@ -450,10 +406,6 @@
|
||||||
gap: var(--spectrum-actionbutton-icon-gap);
|
gap: var(--spectrum-actionbutton-icon-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-action-button.preview-locked {
|
|
||||||
padding-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-action {
|
.app-action {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
notifications,
|
|
||||||
ModalContent,
|
|
||||||
Layout,
|
|
||||||
ProgressCircle,
|
|
||||||
CopyInput,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { API } from "api"
|
|
||||||
import analytics, { Events, EventSource } from "analytics"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import TourWrap from "../portal/onboarding/TourWrap.svelte"
|
|
||||||
import { TOUR_STEP_KEYS } from "../portal/onboarding/tours.js"
|
|
||||||
|
|
||||||
let publishModal
|
|
||||||
let asyncModal
|
|
||||||
let publishCompleteModal
|
|
||||||
|
|
||||||
let published
|
|
||||||
|
|
||||||
$: publishedUrl = published ? `${window.origin}/app${published.appUrl}` : ""
|
|
||||||
|
|
||||||
export let onOk
|
|
||||||
|
|
||||||
async function publishApp() {
|
|
||||||
try {
|
|
||||||
//In Progress
|
|
||||||
asyncModal.show()
|
|
||||||
publishModal.hide()
|
|
||||||
|
|
||||||
published = await API.publishAppChanges($store.appId)
|
|
||||||
|
|
||||||
if (typeof onOk === "function") {
|
|
||||||
await onOk()
|
|
||||||
}
|
|
||||||
|
|
||||||
//Request completed
|
|
||||||
asyncModal.hide()
|
|
||||||
publishCompleteModal.show()
|
|
||||||
} catch (error) {
|
|
||||||
analytics.captureException(error)
|
|
||||||
notifications.error("Error publishing app")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewApp = () => {
|
|
||||||
if (published) {
|
|
||||||
analytics.captureEvent(Events.APP_VIEW_PUBLISHED, {
|
|
||||||
appId: $store.appId,
|
|
||||||
eventSource: EventSource.PORTAL,
|
|
||||||
})
|
|
||||||
window.open(publishedUrl, "_blank")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
|
|
||||||
<Button cta on:click={publishModal.show} id={"builder-app-publish-button"}>
|
|
||||||
Publish
|
|
||||||
</Button>
|
|
||||||
</TourWrap>
|
|
||||||
<Modal bind:this={publishModal}>
|
|
||||||
<ModalContent
|
|
||||||
title="Publish to production"
|
|
||||||
confirmText="Publish"
|
|
||||||
onConfirm={publishApp}
|
|
||||||
>
|
|
||||||
The changes you have made will be published to the production version of the
|
|
||||||
application.
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<!-- Publish in progress -->
|
|
||||||
<Modal bind:this={asyncModal}>
|
|
||||||
<ModalContent
|
|
||||||
showCancelButton={false}
|
|
||||||
showConfirmButton={false}
|
|
||||||
showCloseIcon={false}
|
|
||||||
>
|
|
||||||
<Layout justifyItems="center">
|
|
||||||
<ProgressCircle size="XL" />
|
|
||||||
</Layout>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<!-- Publish complete -->
|
|
||||||
<Modal bind:this={publishCompleteModal}>
|
|
||||||
<ModalContent confirmText="Done" cancelText="View App" onCancel={viewApp}>
|
|
||||||
<div slot="header" class="app-published-header">
|
|
||||||
<svg
|
|
||||||
width="26px"
|
|
||||||
height="26px"
|
|
||||||
class="spectrum-Icon success-icon"
|
|
||||||
focusable="false"
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-icon-18-GlobeCheck" />
|
|
||||||
</svg>
|
|
||||||
<span class="app-published-header-text">App Published!</span>
|
|
||||||
</div>
|
|
||||||
<CopyInput value={publishedUrl} label="You can view your app at:" />
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.app-published-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.success-icon {
|
|
||||||
color: var(--spectrum-global-color-green-600);
|
|
||||||
}
|
|
||||||
.app-published-header .app-published-header-text {
|
|
||||||
padding-left: var(--spacing-l);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,236 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount, onDestroy } from "svelte"
|
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
|
||||||
import { slide } from "svelte/transition"
|
|
||||||
import { Heading, Button, Modal, ModalContent } from "@budibase/bbui"
|
|
||||||
import { API } from "api"
|
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import {
|
|
||||||
checkIncomingDeploymentStatus,
|
|
||||||
DeploymentStatus,
|
|
||||||
} from "components/deploy/utils"
|
|
||||||
|
|
||||||
const DATE_OPTIONS = {
|
|
||||||
fullDate: {
|
|
||||||
weekday: "long",
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
},
|
|
||||||
timeOnly: {
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "numeric",
|
|
||||||
hourCycle: "h12",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const POLL_INTERVAL = 5000
|
|
||||||
|
|
||||||
export let appId
|
|
||||||
|
|
||||||
let modal
|
|
||||||
let errorReasonModal
|
|
||||||
let errorReason
|
|
||||||
let poll
|
|
||||||
let deployments = []
|
|
||||||
let urlComponent = $store.url || `/${appId}`
|
|
||||||
let deploymentUrl = `${urlComponent}`
|
|
||||||
|
|
||||||
const formatDate = (date, format) =>
|
|
||||||
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)
|
|
||||||
|
|
||||||
async function fetchDeployments() {
|
|
||||||
try {
|
|
||||||
const newDeployments = await API.getAppDeployments()
|
|
||||||
if (deployments.length > 0) {
|
|
||||||
const pendingDeployments = checkIncomingDeploymentStatus(
|
|
||||||
deployments,
|
|
||||||
newDeployments
|
|
||||||
)
|
|
||||||
if (pendingDeployments.length) {
|
|
||||||
showErrorReasonModal(pendingDeployments[0].err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deployments = newDeployments
|
|
||||||
} catch (err) {
|
|
||||||
clearInterval(poll)
|
|
||||||
notifications.error("Error fetching deployment overview")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showErrorReasonModal(err) {
|
|
||||||
if (!err) return
|
|
||||||
errorReason = err
|
|
||||||
errorReasonModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
fetchDeployments()
|
|
||||||
poll = setInterval(fetchDeployments, POLL_INTERVAL)
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(() => clearInterval(poll))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if deployments.length > 0}
|
|
||||||
<section class="deployment-history" in:slide>
|
|
||||||
<header>
|
|
||||||
<Heading>Deployment History</Heading>
|
|
||||||
<div class="deploy-div">
|
|
||||||
{#if deployments.some(deployment => deployment.status === DeploymentStatus.SUCCESS)}
|
|
||||||
<a target="_blank" href={deploymentUrl}> View Your Deployed App → </a>
|
|
||||||
<Button primary on:click={() => modal.show()}>View webhooks</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="deployment-list">
|
|
||||||
{#each deployments as deployment}
|
|
||||||
<article class="deployment">
|
|
||||||
<div class="deployment-info">
|
|
||||||
<span class="deploy-date">
|
|
||||||
{formatDate(deployment.updatedAt, "fullDate")}
|
|
||||||
</span>
|
|
||||||
<span class="deploy-time">
|
|
||||||
{formatDate(deployment.updatedAt, "timeOnly")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="deployment-right">
|
|
||||||
{#if deployment.status.toLowerCase() === "pending"}
|
|
||||||
<Spinner size="10" />
|
|
||||||
{/if}
|
|
||||||
<div
|
|
||||||
on:click={() => showErrorReasonModal(deployment.err)}
|
|
||||||
class={`deployment-status ${deployment.status}`}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{deployment.status}
|
|
||||||
{#if deployment.status === DeploymentStatus.FAILURE}
|
|
||||||
<i class="ri-information-line" />
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
<Modal bind:this={modal} width="30%">
|
|
||||||
<CreateWebhookDeploymentModal />
|
|
||||||
</Modal>
|
|
||||||
<Modal bind:this={errorReasonModal} width="30%">
|
|
||||||
<ModalContent
|
|
||||||
title="Deployment Error"
|
|
||||||
confirmText="OK"
|
|
||||||
showCancelButton={false}
|
|
||||||
>
|
|
||||||
{errorReason}
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
section {
|
|
||||||
padding: var(--spacing-xl) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deployment-list {
|
|
||||||
height: 40vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
padding-left: var(--spacing-l);
|
|
||||||
padding-bottom: var(--spacing-xl);
|
|
||||||
padding-right: var(--spacing-l);
|
|
||||||
border-bottom: var(--border-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.deploy-div {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deployment-history {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.deployment {
|
|
||||||
padding: var(--spacing-l);
|
|
||||||
height: 60px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: var(--border-light);
|
|
||||||
}
|
|
||||||
.deployment:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deployment-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.deploy-date {
|
|
||||||
font-size: var(--font-size-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
.deploy-time {
|
|
||||||
color: var(--grey-7);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.deployment-right {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deployment-status {
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
padding: var(--spacing-s);
|
|
||||||
border-radius: var(--border-radius-s);
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: lowercase;
|
|
||||||
width: 80px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.deployment-status:first-letter {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--blue);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.SUCCESS {
|
|
||||||
color: var(--green);
|
|
||||||
background: var(--green-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.PENDING {
|
|
||||||
color: var(--yellow);
|
|
||||||
background: var(--yellow-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.FAILURE {
|
|
||||||
color: var(--red);
|
|
||||||
background: var(--red-light);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
position: relative;
|
|
||||||
top: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -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
|
|
||||||
}, [])
|
|
||||||
}
|
|
|
@ -1,32 +1,62 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { Tooltip } from "@budibase/bbui"
|
||||||
|
|
||||||
export let text
|
export let text
|
||||||
export let url
|
export let url
|
||||||
export let active = false
|
export let active = false
|
||||||
|
export let disabled = false
|
||||||
|
export let tooltip = null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if url}
|
<div class="side-nav-item">
|
||||||
<a on:click href={url} class:active>
|
{#if url}
|
||||||
{text || ""}
|
<a class="text" on:click href={url} class:active class:disabled>
|
||||||
</a>
|
{text || ""}
|
||||||
{:else}
|
</a>
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
{:else}
|
||||||
<span on:click class:active>
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
{text || ""}
|
<div class="text" on:click class:active class:disabled>
|
||||||
</span>
|
{text || ""}
|
||||||
{/if}
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if tooltip}
|
||||||
|
<div class="tooltip">
|
||||||
|
<Tooltip textWrapping direction="right" text={tooltip} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
a,
|
.side-nav-item {
|
||||||
span {
|
position: relative;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
display: block;
|
||||||
padding: var(--spacing-s) var(--spacing-m);
|
padding: var(--spacing-s) var(--spacing-m);
|
||||||
color: var(--spectrum-global-color-gray-900);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 130ms ease-out;
|
transition: background 130ms ease-out;
|
||||||
}
|
}
|
||||||
.active,
|
.active,
|
||||||
span:hover,
|
.text:hover {
|
||||||
a:hover {
|
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--spectrum-global-color-gray-500) !important;
|
||||||
|
}
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
left: 100%;
|
||||||
|
top: 50%;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 130ms ease-out;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.side-nav-item:hover .tooltip {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -114,26 +114,24 @@ export const syncURLToState = options => {
|
||||||
|
|
||||||
// Updates the URL with new state values
|
// Updates the URL with new state values
|
||||||
const mapStateToUrl = state => {
|
const mapStateToUrl = state => {
|
||||||
let needsUpdate = false
|
|
||||||
const urlValue = cachedParams?.[urlParam]
|
const urlValue = cachedParams?.[urlParam]
|
||||||
const stateValue = state?.[stateKey]
|
const stateValue = state?.[stateKey]
|
||||||
if (stateValue !== urlValue) {
|
|
||||||
needsUpdate = true
|
// As the store updated, validate that the current state value is valid
|
||||||
log(`url.${urlParam} (${urlValue}) <= state.${stateKey} (${stateValue})`)
|
if (validate && fallbackUrl) {
|
||||||
if (validate && fallbackUrl) {
|
if (!validate(stateValue)) {
|
||||||
if (!validate(stateValue)) {
|
log("Invalid state param!", stateValue)
|
||||||
log("Invalid state param!", stateValue)
|
redirectUrl(fallbackUrl)
|
||||||
redirectUrl(fallbackUrl)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid updating the URL if not necessary to prevent a wasted render
|
// Avoid updating the URL if not necessary to prevent a wasted render
|
||||||
// cycle
|
// cycle
|
||||||
if (!needsUpdate) {
|
if (stateValue === urlValue) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log(`url.${urlParam} (${urlValue}) <= state.${stateKey} (${stateValue})`)
|
||||||
|
|
||||||
// Navigate to the new URL
|
// Navigate to the new URL
|
||||||
if (!get(isChangingPage)) {
|
if (!get(isChangingPage)) {
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
import { Constants, Utils } from "@budibase/frontend-core"
|
import { Constants, Utils } from "@budibase/frontend-core"
|
||||||
import { emailValidator } from "helpers/validation"
|
import { emailValidator } from "helpers/validation"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
|
import { fly } from "svelte/transition"
|
||||||
|
|
||||||
let query = null
|
let query = null
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
@ -418,16 +419,14 @@
|
||||||
<svelte:window on:keydown={handleKeyDown} />
|
<svelte:window on:keydown={handleKeyDown} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
transition:fly={{ x: 400, duration: 260 }}
|
||||||
id="builder-side-panel-container"
|
id="builder-side-panel-container"
|
||||||
class:open={$store.builderSidePanel}
|
use:clickOutside={() => {
|
||||||
use:clickOutside={$store.builderSidePanel
|
store.update(state => {
|
||||||
? () => {
|
state.builderSidePanel = false
|
||||||
store.update(state => {
|
return state
|
||||||
state.builderSidePanel = false
|
})
|
||||||
return state
|
}}
|
||||||
})
|
|
||||||
}
|
|
||||||
: () => {}}
|
|
||||||
>
|
>
|
||||||
<div class="builder-side-panel-header">
|
<div class="builder-side-panel-header">
|
||||||
<Heading size="S">Users</Heading>
|
<Heading size="S">Users</Heading>
|
||||||
|
@ -737,12 +736,11 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
transition: transform 130ms ease-out;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
right: 0;
|
right: 0;
|
||||||
transform: translateX(100%);
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
box-shadow: 0 0 40px 10px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.builder-side-panel-header,
|
.builder-side-panel-header,
|
||||||
|
@ -792,11 +790,6 @@
|
||||||
font-style: normal;
|
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 {
|
.builder-side-panel-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, automationStore, userStore } from "builderStore"
|
import {
|
||||||
|
store,
|
||||||
|
automationStore,
|
||||||
|
userStore,
|
||||||
|
deploymentStore,
|
||||||
|
} from "builderStore"
|
||||||
import { roles, flags } from "stores/backend"
|
import { roles, flags } from "stores/backend"
|
||||||
import { auth } from "stores/portal"
|
import { auth, apps } from "stores/portal"
|
||||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
|
@ -44,6 +49,8 @@
|
||||||
await automationStore.actions.fetch()
|
await automationStore.actions.fetch()
|
||||||
await roles.fetch()
|
await roles.fetch()
|
||||||
await flags.fetch()
|
await flags.fetch()
|
||||||
|
await apps.load()
|
||||||
|
await deploymentStore.actions.load()
|
||||||
loaded = true
|
loaded = true
|
||||||
return pkg
|
return pkg
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -69,18 +76,13 @@
|
||||||
|
|
||||||
// Event handler for the command palette
|
// Event handler for the command palette
|
||||||
const handleKeyDown = e => {
|
const handleKeyDown = e => {
|
||||||
if (e.key === "k" && (e.ctrlKey || e.metaKey) && $store.hasLock) {
|
if (e.key === "k" && (e.ctrlKey || e.metaKey)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
commandPaletteModal.toggle()
|
commandPaletteModal.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initTour = async () => {
|
const initTour = async () => {
|
||||||
// Skip tour if we don't have the lock
|
|
||||||
if (!$store.hasLock) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if onboarding is enabled.
|
// Check if onboarding is enabled.
|
||||||
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
||||||
if (!$auth.user?.onboardedAt) {
|
if (!$auth.user?.onboardedAt) {
|
||||||
|
@ -140,7 +142,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="root" class:blur={$store.showPreview}>
|
<div class="root" class:blur={$store.showPreview}>
|
||||||
<div class="top-nav" class:has-lock={$store.hasLock}>
|
<div class="top-nav">
|
||||||
{#if $store.initialised}
|
{#if $store.initialised}
|
||||||
<div class="topleftnav">
|
<div class="topleftnav">
|
||||||
<span class="back-to-apps">
|
<span class="back-to-apps">
|
||||||
|
@ -151,37 +153,25 @@
|
||||||
on:click={() => $goto("../../portal/apps")}
|
on:click={() => $goto("../../portal/apps")}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
{#if $store.hasLock}
|
<Tabs {selected} size="M">
|
||||||
<Tabs {selected} size="M">
|
{#each $layout.children as { path, title }}
|
||||||
{#each $layout.children as { path, title }}
|
<TourWrap tourStepKey={`builder-${title}-section`}>
|
||||||
<TourWrap tourStepKey={`builder-${title}-section`}>
|
<Tab
|
||||||
<Tab
|
quiet
|
||||||
quiet
|
selected={$isActive(path)}
|
||||||
selected={$isActive(path)}
|
on:click={topItemNavigate(path)}
|
||||||
on:click={topItemNavigate(path)}
|
title={capitalise(title)}
|
||||||
title={capitalise(title)}
|
id={`builder-${title}-tab`}
|
||||||
id={`builder-${title}-tab`}
|
/>
|
||||||
/>
|
</TourWrap>
|
||||||
</TourWrap>
|
{/each}
|
||||||
{/each}
|
</Tabs>
|
||||||
</Tabs>
|
|
||||||
{:else}
|
|
||||||
<div class="secondary-editor">
|
|
||||||
<Icon name="LockClosed" />
|
|
||||||
<div
|
|
||||||
class="secondary-editor-body"
|
|
||||||
title="Another user is currently editing your screens and automations"
|
|
||||||
>
|
|
||||||
Another user is currently editing your screens and automations
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="topcenternav">
|
<div class="topcenternav">
|
||||||
<Heading size="XS">{$store.name}</Heading>
|
<Heading size="XS">{$store.name}</Heading>
|
||||||
</div>
|
</div>
|
||||||
<div class="toprightnav">
|
<div class="toprightnav">
|
||||||
<span class:nav-lock={!$store.hasLock}>
|
<span>
|
||||||
<UserAvatars users={$userStore} />
|
<UserAvatars users={$userStore} />
|
||||||
</span>
|
</span>
|
||||||
<AppActions {application} {loaded} />
|
<AppActions {application} {loaded} />
|
||||||
|
@ -248,10 +238,6 @@
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-nav.has-lock {
|
|
||||||
padding-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topcenternav {
|
.topcenternav {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -290,23 +276,6 @@
|
||||||
margin-right: var(--spacing-l);
|
margin-right: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-editor {
|
|
||||||
align-self: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 8px;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-left: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-editor-body {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
min-width: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
|
@ -8,15 +8,6 @@
|
||||||
import { onDestroy, onMount } from "svelte"
|
import { onDestroy, onMount } from "svelte"
|
||||||
import { syncURLToState } from "helpers/urlStateSync"
|
import { syncURLToState } from "helpers/urlStateSync"
|
||||||
import * as routify from "@roxi/routify"
|
import * as routify from "@roxi/routify"
|
||||||
import { store } from "builderStore"
|
|
||||||
import { redirect } from "@roxi/routify"
|
|
||||||
|
|
||||||
// Prevent access for other users than the lock holder
|
|
||||||
$: {
|
|
||||||
if (!$store.hasLock) {
|
|
||||||
$redirect("../data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep URL and state in sync for selected screen ID
|
// Keep URL and state in sync for selected screen ID
|
||||||
const stopSyncing = syncURLToState({
|
const stopSyncing = syncURLToState({
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
import { syncURLToState } from "helpers/urlStateSync"
|
import { syncURLToState } from "helpers/urlStateSync"
|
||||||
import * as routify from "@roxi/routify"
|
import * as routify from "@roxi/routify"
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
$: datasourceId = $datasources.selectedDatasourceId
|
||||||
|
$: store.actions.websocket.selectResource(datasourceId)
|
||||||
|
|
||||||
const stopSyncing = syncURLToState({
|
const stopSyncing = syncURLToState({
|
||||||
urlParam: "datasourceId",
|
urlParam: "datasourceId",
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
|
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
$: store.actions.websocket.selectResource(BUDIBASE_INTERNAL_DB_ID)
|
||||||
$: internalTablesBySourceId = $tables.list.filter(
|
$: internalTablesBySourceId = $tables.list.filter(
|
||||||
table =>
|
table =>
|
||||||
table.type !== "external" &&
|
table.type !== "external" &&
|
||||||
|
|
|
@ -6,8 +6,11 @@
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
|
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
$: store.actions.websocket.selectResource(DEFAULT_BB_DATASOURCE_ID)
|
||||||
$: internalTablesBySourceId = $tables.list.filter(
|
$: internalTablesBySourceId = $tables.list.filter(
|
||||||
table =>
|
table =>
|
||||||
table.type !== "external" && table.sourceId === DEFAULT_BB_DATASOURCE_ID
|
table.type !== "external" && table.sourceId === DEFAULT_BB_DATASOURCE_ID
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
import { syncURLToState } from "helpers/urlStateSync"
|
import { syncURLToState } from "helpers/urlStateSync"
|
||||||
import * as routify from "@roxi/routify"
|
import * as routify from "@roxi/routify"
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
$: queryId = $queries.selectedQueryId
|
||||||
|
$: store.actions.websocket.selectResource(queryId)
|
||||||
|
|
||||||
const stopSyncing = syncURLToState({
|
const stopSyncing = syncURLToState({
|
||||||
urlParam: "queryId",
|
urlParam: "queryId",
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import * as routify from "@roxi/routify"
|
import * as routify from "@roxi/routify"
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
$: tableId = $tables.selectedTableId
|
||||||
|
$: store.actions.websocket.selectResource(tableId)
|
||||||
|
|
||||||
const stopSyncing = syncURLToState({
|
const stopSyncing = syncURLToState({
|
||||||
urlParam: "tableId",
|
urlParam: "tableId",
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
import { syncURLToState } from "helpers/urlStateSync"
|
import { syncURLToState } from "helpers/urlStateSync"
|
||||||
import * as routify from "@roxi/routify"
|
import * as routify from "@roxi/routify"
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
$: viewName = $views.selectedViewName
|
||||||
|
$: store.actions.websocket.selectResource(viewName)
|
||||||
|
|
||||||
const stopSyncing = syncURLToState({
|
const stopSyncing = syncURLToState({
|
||||||
urlParam: "viewName",
|
urlParam: "viewName",
|
||||||
|
|
|
@ -7,6 +7,9 @@
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte"
|
||||||
const { isActive, goto } = routify
|
const { isActive, goto } = routify
|
||||||
|
|
||||||
|
$: screenId = $store.selectedScreenId
|
||||||
|
$: store.actions.websocket.selectResource(screenId)
|
||||||
|
|
||||||
// Keep URL and state in sync for selected screen ID
|
// Keep URL and state in sync for selected screen ID
|
||||||
const stopSyncing = syncURLToState({
|
const stopSyncing = syncURLToState({
|
||||||
urlParam: "screenId",
|
urlParam: "screenId",
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import ComponentTree from "./ComponentTree.svelte"
|
import ComponentTree from "./ComponentTree.svelte"
|
||||||
import { dndStore } from "./dndStore.js"
|
import { dndStore } from "./dndStore.js"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { store, selectedScreen } from "builderStore"
|
import { store, selectedScreen, userSelectedResourceMap } from "builderStore"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
||||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||||
|
@ -41,6 +41,7 @@
|
||||||
$store.selectedComponentId = $selectedScreen?.props._id
|
$store.selectedComponentId = $selectedScreen?.props._id
|
||||||
}}
|
}}
|
||||||
id={`component-${$selectedScreen?.props._id}`}
|
id={`component-${$selectedScreen?.props._id}`}
|
||||||
|
selectedBy={$userSelectedResourceMap[$selectedScreen?.props._id]}
|
||||||
>
|
>
|
||||||
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { store } from "builderStore"
|
import { store, userSelectedResourceMap } from "builderStore"
|
||||||
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
@ -123,6 +123,7 @@
|
||||||
selected={$store.selectedComponentId === component._id}
|
selected={$store.selectedComponentId === component._id}
|
||||||
{opened}
|
{opened}
|
||||||
highlighted={isChildOfSelectedComponent(component)}
|
highlighted={isChildOfSelectedComponent(component)}
|
||||||
|
selectedBy={$userSelectedResourceMap[component._id]}
|
||||||
>
|
>
|
||||||
<ComponentDropdownMenu {component} />
|
<ComponentDropdownMenu {component} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
|
|
@ -7,6 +7,9 @@
|
||||||
import ComponentListPanel from "./_components/navigation/ComponentListPanel.svelte"
|
import ComponentListPanel from "./_components/navigation/ComponentListPanel.svelte"
|
||||||
import ComponentSettingsPanel from "./_components/settings/ComponentSettingsPanel.svelte"
|
import ComponentSettingsPanel from "./_components/settings/ComponentSettingsPanel.svelte"
|
||||||
|
|
||||||
|
$: componentId = $store.selectedComponentId
|
||||||
|
$: store.actions.websocket.selectResource(componentId)
|
||||||
|
|
||||||
const cleanUrl = url => {
|
const cleanUrl = url => {
|
||||||
// Strip trailing slashes
|
// Strip trailing slashes
|
||||||
if (url?.endsWith("/index")) {
|
if (url?.endsWith("/index")) {
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
$: role = $roles.find(role => role._id === roleId)
|
$: role = $roles.find(role => role._id === roleId)
|
||||||
$: tooltip =
|
$: tooltip =
|
||||||
roleId === Roles.PUBLIC
|
roleId === Roles.PUBLIC
|
||||||
? "This screen is open to the public"
|
? "Open to the public"
|
||||||
: `Requires at least ${role?.name} access`
|
: `Requires ${role?.name} access`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -44,14 +44,14 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 130px;
|
width: 200px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.tooltip :global(.spectrum-Tooltip) {
|
.tooltip :global(.spectrum-Tooltip) {
|
||||||
background: var(--color);
|
background: var(--color);
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
max-width: 130px;
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
.tooltip :global(.spectrum-Tooltip-tip) {
|
.tooltip :global(.spectrum-Tooltip-tip) {
|
||||||
border-top-color: var(--color);
|
border-top-color: var(--color);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { Search, Layout, Select, Body, Button } from "@budibase/bbui"
|
import { Search, Layout, Select, Body, Button } from "@budibase/bbui"
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
import { store, sortedScreens } from "builderStore"
|
import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
||||||
import ScreenWizard from "./ScreenWizard.svelte"
|
import ScreenWizard from "./ScreenWizard.svelte"
|
||||||
|
@ -60,6 +60,7 @@
|
||||||
on:click={() => store.actions.screens.select(screen._id)}
|
on:click={() => store.actions.screens.select(screen._id)}
|
||||||
rightAlignIcon
|
rightAlignIcon
|
||||||
showTooltip
|
showTooltip
|
||||||
|
selectedBy={$userSelectedResourceMap[screen._id]}
|
||||||
>
|
>
|
||||||
<ScreenDropdownMenu screenId={screen._id} />
|
<ScreenDropdownMenu screenId={screen._id} />
|
||||||
<RoleIndicator slot="right" roleId={screen.routing.roleId} />
|
<RoleIndicator slot="right" roleId={screen.routing.roleId} />
|
||||||
|
|
|
@ -1,14 +1,2 @@
|
||||||
<script>
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import { redirect } from "@roxi/routify"
|
|
||||||
|
|
||||||
// Prevent access for other users than the lock holder
|
|
||||||
$: {
|
|
||||||
if (!$store.hasLock) {
|
|
||||||
$redirect("../data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- routify:options index=2 -->
|
<!-- routify:options index=2 -->
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { Page, Layout } from "@budibase/bbui"
|
import { Page, Layout } from "@budibase/bbui"
|
||||||
import { url, isActive } from "@roxi/routify"
|
import { url, isActive } from "@roxi/routify"
|
||||||
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
||||||
|
import { isOnlyUser } from "builderStore"
|
||||||
|
|
||||||
let deleteModal
|
let deleteModal
|
||||||
</script>
|
</script>
|
||||||
|
@ -49,6 +50,10 @@
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
deleteModal.show()
|
deleteModal.show()
|
||||||
}}
|
}}
|
||||||
|
disabled={!$isOnlyUser}
|
||||||
|
tooltip={$isOnlyUser
|
||||||
|
? null
|
||||||
|
: "Unavailable - another user is editing this app"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SideNav>
|
</SideNav>
|
||||||
|
@ -61,7 +66,7 @@
|
||||||
<DeleteModal bind:this={deleteModal} />
|
<DeleteModal bind:this={deleteModal} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.delete-action :global(span) {
|
.delete-action :global(.text) {
|
||||||
color: var(--spectrum-global-color-red-400);
|
color: var(--spectrum-global-color-red-400);
|
||||||
}
|
}
|
||||||
.delete-action {
|
.delete-action {
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import CreateRestoreModal from "./CreateRestoreModal.svelte"
|
import CreateRestoreModal from "./CreateRestoreModal.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { isOnlyUser } from "builderStore"
|
||||||
|
|
||||||
export let row
|
export let row
|
||||||
|
|
||||||
|
@ -45,7 +46,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if row.type !== "restore"}
|
{#if row.type !== "restore"}
|
||||||
<MenuItem on:click={restoreDialog.show} icon="Revert">Restore</MenuItem>
|
<MenuItem
|
||||||
|
on:click={restoreDialog.show}
|
||||||
|
icon="Revert"
|
||||||
|
disabled={!$isOnlyUser}
|
||||||
|
tooltip={$isOnlyUser
|
||||||
|
? null
|
||||||
|
: "Unavailable - another user is editing this app"}
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</MenuItem>
|
||||||
<MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem>
|
<MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem>
|
||||||
<MenuItem on:click={downloadExport} icon="Download">Download</MenuItem>
|
<MenuItem on:click={downloadExport} icon="Download">Download</MenuItem>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -63,9 +63,8 @@
|
||||||
}}
|
}}
|
||||||
disabled={appDeployed}
|
disabled={appDeployed}
|
||||||
tooltip={appDeployed
|
tooltip={appDeployed
|
||||||
? "You must unpublish your app to make changes to these settings"
|
? "You must unpublish your app to make changes"
|
||||||
: null}
|
: null}
|
||||||
icon={appDeployed ? "HelpOutline" : null}
|
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout, Heading, Body, Divider, Button } from "@budibase/bbui"
|
import { Layout, Heading, Body, Divider, Button } from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store, isOnlyUser } from "builderStore"
|
||||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
|
|
||||||
let versionModal
|
let versionModal
|
||||||
|
@ -22,7 +22,16 @@
|
||||||
Updates can contain new features, performance improvements and bug fixes.
|
Updates can contain new features, performance improvements and bug fixes.
|
||||||
</Body>
|
</Body>
|
||||||
<div>
|
<div>
|
||||||
<Button cta on:click={versionModal.show}>Update app</Button>
|
<Button
|
||||||
|
cta
|
||||||
|
on:click={versionModal.show}
|
||||||
|
disabled={!$isOnlyUser}
|
||||||
|
tooltip={$isOnlyUser
|
||||||
|
? null
|
||||||
|
: "Unavailable - another user is editing this app"}
|
||||||
|
>
|
||||||
|
Update app
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Body>
|
<Body>
|
||||||
|
@ -31,7 +40,15 @@
|
||||||
You're running the latest!
|
You're running the latest!
|
||||||
</Body>
|
</Body>
|
||||||
<div>
|
<div>
|
||||||
<Button secondary on:click={versionModal.show}>Revert app</Button>
|
<Button
|
||||||
|
secondary
|
||||||
|
on:click={versionModal.show}
|
||||||
|
tooltip={$isOnlyUser
|
||||||
|
? null
|
||||||
|
: "Unavailable - another user is editing this app"}
|
||||||
|
>
|
||||||
|
Revert app
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<Tooltip
|
<Tooltip
|
||||||
direction={tooltipDirection}
|
direction={tooltipDirection}
|
||||||
textWrapping
|
textWrapping
|
||||||
text={user.email}
|
text={helpers.getUserLabel(user)}
|
||||||
size="S"
|
size="S"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,13 +46,15 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.tooltip {
|
.tooltip {
|
||||||
display: none;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 130ms ease-out;
|
||||||
}
|
}
|
||||||
.user-avatar:hover .tooltip {
|
.user-avatar:hover .tooltip {
|
||||||
display: block;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -30,8 +30,9 @@ export const deriveStores = context => {
|
||||||
([$users, $focusedCellId]) => {
|
([$users, $focusedCellId]) => {
|
||||||
let map = {}
|
let map = {}
|
||||||
$users.forEach(user => {
|
$users.forEach(user => {
|
||||||
if (user.focusedCellId && user.focusedCellId !== $focusedCellId) {
|
const cellId = user.gridMetadata?.focusedCellId
|
||||||
map[user.focusedCellId] = user
|
if (cellId && cellId !== $focusedCellId) {
|
||||||
|
map[cellId] = user
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return map
|
return map
|
||||||
|
|
|
@ -53,6 +53,7 @@ import {
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
|
import { builderSocket } from "../../websockets"
|
||||||
|
|
||||||
// utility function, need to do away with this
|
// utility function, need to do away with this
|
||||||
async function getLayouts() {
|
async function getLayouts() {
|
||||||
|
@ -439,6 +440,14 @@ export async function update(ctx: UserCtx) {
|
||||||
await events.app.updated(app)
|
await events.app.updated(app)
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = app
|
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) {
|
export async function updateClient(ctx: UserCtx) {
|
||||||
|
@ -569,6 +578,7 @@ export async function unpublish(ctx: UserCtx) {
|
||||||
await unpublishApp(ctx)
|
await unpublishApp(ctx)
|
||||||
await postDestroyApp(ctx)
|
await postDestroyApp(ctx)
|
||||||
ctx.status = 204
|
ctx.status = 204
|
||||||
|
builderSocket?.emitAppUnpublish(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sync(ctx: UserCtx) {
|
export async function sync(ctx: UserCtx) {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
import { backups } from "@budibase/pro"
|
import { backups } from "@budibase/pro"
|
||||||
import { AppBackupTrigger } from "@budibase/types"
|
import { AppBackupTrigger } from "@budibase/types"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
import { builderSocket } from "../../../websockets"
|
||||||
|
|
||||||
// the max time we can wait for an invalidation to complete before considering it failed
|
// the max time we can wait for an invalidation to complete before considering it failed
|
||||||
const MAX_PENDING_TIME_MS = 30 * 60000
|
const MAX_PENDING_TIME_MS = 30 * 60000
|
||||||
|
@ -201,4 +202,5 @@ export const publishApp = async function (ctx: any) {
|
||||||
|
|
||||||
await events.app.published(app)
|
await events.app.published(app)
|
||||||
ctx.body = deployment
|
ctx.body = deployment
|
||||||
|
builderSocket?.emitAppPublish(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { updateAppPackage } from "./application"
|
import { updateAppPackage } from "./application"
|
||||||
import { Plugin, ScreenProps, BBContext } from "@budibase/types"
|
import { Plugin, ScreenProps, BBContext } from "@budibase/types"
|
||||||
|
import { builderSocket } from "../../websockets"
|
||||||
|
|
||||||
export async function fetch(ctx: BBContext) {
|
export async function fetch(ctx: BBContext) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
@ -87,13 +88,17 @@ export async function save(ctx: BBContext) {
|
||||||
if (eventFn) {
|
if (eventFn) {
|
||||||
await eventFn(screen)
|
await eventFn(screen)
|
||||||
}
|
}
|
||||||
ctx.message = `Screen ${screen.name} saved.`
|
const savedScreen = {
|
||||||
ctx.body = {
|
|
||||||
...screen,
|
...screen,
|
||||||
_id: response.id,
|
_id: response.id,
|
||||||
_rev: response.rev,
|
_rev: response.rev,
|
||||||
|
}
|
||||||
|
ctx.message = `Screen ${screen.name} saved.`
|
||||||
|
ctx.body = {
|
||||||
|
...savedScreen,
|
||||||
pluginAdded,
|
pluginAdded,
|
||||||
}
|
}
|
||||||
|
builderSocket?.emitScreenUpdate(ctx, savedScreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function destroy(ctx: BBContext) {
|
export async function destroy(ctx: BBContext) {
|
||||||
|
@ -108,6 +113,7 @@ export async function destroy(ctx: BBContext) {
|
||||||
message: "Screen deleted successfully",
|
message: "Screen deleted successfully",
|
||||||
}
|
}
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
|
builderSocket?.emitScreenDeletion(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function findPlugins(component: ScreenProps, foundPlugins: string[]) {
|
function findPlugins(component: ScreenProps, foundPlugins: string[]) {
|
||||||
|
|
|
@ -3,11 +3,18 @@ import { BaseSocket } from "./websocket"
|
||||||
import { permissions, events } from "@budibase/backend-core"
|
import { permissions, events } from "@budibase/backend-core"
|
||||||
import http from "http"
|
import http from "http"
|
||||||
import Koa from "koa"
|
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 { gridSocket } from "./index"
|
||||||
import { clearLock, updateLock } from "../utilities/redis"
|
import { clearLock, updateLock } from "../utilities/redis"
|
||||||
import { Socket } from "socket.io"
|
import { Socket } from "socket.io"
|
||||||
import { BuilderSocketEvent } from "@budibase/shared-core"
|
import { BuilderSocketEvent, GridSocketEvent } from "@budibase/shared-core"
|
||||||
|
|
||||||
export default class BuilderSocket extends BaseSocket {
|
export default class BuilderSocket extends BaseSocket {
|
||||||
constructor(app: Koa, server: http.Server) {
|
constructor(app: Koa, server: http.Server) {
|
||||||
|
@ -32,6 +39,11 @@ export default class BuilderSocket extends BaseSocket {
|
||||||
// Reply with all current sessions
|
// Reply with all current sessions
|
||||||
callback({ users: sessions })
|
callback({ users: sessions })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle users selecting a new cell
|
||||||
|
socket?.on(BuilderSocketEvent.SelectResource, ({ resourceId }) => {
|
||||||
|
this.updateUser(socket, { selectedResourceId: resourceId })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async onDisconnect(socket: Socket) {
|
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) {
|
emitTableUpdate(ctx: any, table: Table) {
|
||||||
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.TableChange, {
|
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.TableChange, {
|
||||||
id: table._id,
|
id: table._id,
|
||||||
|
@ -101,4 +122,38 @@ export default class BuilderSocket extends BaseSocket {
|
||||||
datasource: null,
|
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<App>) {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
emitRowUpdate(ctx: any, row: Row) {
|
||||||
const tableId = getTableId(ctx)
|
const tableId = getTableId(ctx)
|
||||||
const room = `${ctx.appId}-${tableId}`
|
const room = `${ctx.appId}-${tableId}`
|
||||||
|
|
|
@ -86,6 +86,10 @@ export enum BuilderSocketEvent {
|
||||||
TableChange = "TableChange",
|
TableChange = "TableChange",
|
||||||
DatasourceChange = "DatasourceChange",
|
DatasourceChange = "DatasourceChange",
|
||||||
LockTransfer = "LockTransfer",
|
LockTransfer = "LockTransfer",
|
||||||
|
ScreenChange = "ScreenChange",
|
||||||
|
AppMetadataChange = "AppMetadataChange",
|
||||||
|
SelectResource = "SelectResource",
|
||||||
|
AppPublishChange = "AppPublishChange",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SocketSessionTTL = 60
|
export const SocketSessionTTL = 60
|
||||||
|
|
Loading…
Reference in New Issue