Merge pull request #2233 from Budibase/peek-screen

Open screen in modal
This commit is contained in:
Andrew Kingston 2021-08-04 12:08:49 +01:00 committed by GitHub
commit 6a9358086c
25 changed files with 380 additions and 139 deletions

View File

@ -70,6 +70,7 @@
> >
<div class="modal-wrapper" on:mousedown|self={cancel}> <div class="modal-wrapper" on:mousedown|self={cancel}>
<div class="modal-inner-wrapper" on:mousedown|self={cancel}> <div class="modal-inner-wrapper" on:mousedown|self={cancel}>
<slot name="outside" />
<div <div
use:focusFirstInput use:focusFirstInput
class="spectrum-Modal is-open" class="spectrum-Modal is-open"
@ -93,6 +94,7 @@
z-index: 999; z-index: 999;
overflow: auto; overflow: auto;
overflow-x: hidden; overflow-x: hidden;
background: rgba(0, 0, 0, 0.75);
} }
.modal-wrapper { .modal-wrapper {
@ -112,6 +114,7 @@
justify-content: center; justify-content: center;
align-items: flex-start; align-items: flex-start;
width: 0; width: 0;
position: relative;
} }
.spectrum-Modal { .spectrum-Modal {
@ -122,6 +125,7 @@
--spectrum-dialog-confirm-border-radius: var( --spectrum-dialog-confirm-border-radius: var(
--spectrum-global-dimension-size-100 --spectrum-global-dimension-size-100
); );
max-width: 100%;
} }
:global(.spectrum--lightest .spectrum-Modal.inline) { :global(.spectrum--lightest .spectrum-Modal.inline) {
border: var(--border-light); border: var(--border-light);

View File

@ -15,6 +15,7 @@
export let showCloseIcon = true export let showCloseIcon = true
export let onConfirm = undefined export let onConfirm = undefined
export let disabled = false export let disabled = false
export let showDivider = true
const { hide, cancel } = getContext(Context.Modal) const { hide, cancel } = getContext(Context.Modal)
let loading = false let loading = false
@ -41,11 +42,17 @@
aria-modal="true" aria-modal="true"
> >
<div class="spectrum-Dialog-grid"> <div class="spectrum-Dialog-grid">
<h1 class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"> {#if title}
<h1
class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
class:noDivider={!showDivider}
>
{title} {title}
</h1> </h1>
{#if showDivider}
<Divider size="M" /> <Divider size="M" />
{/if}
{/if}
<!-- TODO: Remove content-grid class once Layout components are in bbui --> <!-- TODO: Remove content-grid class once Layout components are in bbui -->
<section class="spectrum-Dialog-content content-grid"> <section class="spectrum-Dialog-content content-grid">
<slot /> <slot />
@ -72,8 +79,8 @@
</div> </div>
{/if} {/if}
{#if showCloseIcon} {#if showCloseIcon}
<div class="close-icon" on:click={hide}> <div class="close-icon">
<Icon hoverable name="Close" /> <Icon hoverable name="Close" on:click={cancel} />
</div> </div>
{/if} {/if}
</div> </div>
@ -96,6 +103,9 @@
.spectrum-Dialog-heading { .spectrum-Dialog-heading {
font-family: var(--font-sans); font-family: var(--font-sans);
} }
.spectrum-Dialog-heading.noDivider {
margin-bottom: 12px;
}
.spectrum-Dialog-buttonGroup { .spectrum-Dialog-buttonGroup {
gap: var(--spectrum-global-dimension-static-size-200); gap: var(--spectrum-global-dimension-static-size-200);

View File

@ -0,0 +1,20 @@
<script>
export let type = "info"
export let icon = "Info"
export let message = ""
</script>
<div class="spectrum-Toast spectrum-Toast--{type}">
{#if icon}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Toast-typeIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
{/if}
<div class="spectrum-Toast-body">
<div class="spectrum-Toast-content">{message || ""}</div>
</div>
</div>

View File

@ -2,30 +2,16 @@
import "@spectrum-css/toast/dist/index-vars.css" import "@spectrum-css/toast/dist/index-vars.css"
import Portal from "svelte-portal" import Portal from "svelte-portal"
import { flip } from "svelte/animate" import { flip } from "svelte/animate"
import { fly } from "svelte/transition"
import { notifications } from "../Stores/notifications" import { notifications } from "../Stores/notifications"
import Notification from "./Notification.svelte"
import { fly } from "svelte/transition"
</script> </script>
<Portal target=".modal-container"> <Portal target=".modal-container">
<div class="notifications"> <div class="notifications">
{#each $notifications as { type, icon, message, id } (id)} {#each $notifications as { type, icon, message, id } (id)}
<div <div animate:flip transition:fly={{ y: -30 }}>
animate:flip <Notification {type} {icon} {message} />
transition:fly={{ y: -30 }}
class="spectrum-Toast spectrum-Toast--{type} notification-offset"
>
{#if icon}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Toast-typeIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
{/if}
<div class="spectrum-Toast-body">
<div class="spectrum-Toast-content">{message}</div>
</div>
</div> </div>
{/each} {/each}
</div> </div>
@ -34,7 +20,7 @@
<style> <style>
.notifications { .notifications {
position: fixed; position: fixed;
top: 10px; top: 20px;
left: 0; left: 0;
right: 0; right: 0;
margin: 0 auto; margin: 0 auto;
@ -45,8 +31,6 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
pointer-events: none; pointer-events: none;
} gap: 10px;
.notification-offset {
margin-bottom: 10px;
} }
</style> </style>

View File

@ -38,6 +38,7 @@ export { default as MenuItem } from "./Menu/Item.svelte"
export { default as Modal } from "./Modal/Modal.svelte" export { default as Modal } from "./Modal/Modal.svelte"
export { default as ModalContent } from "./Modal/ModalContent.svelte" export { default as ModalContent } from "./Modal/ModalContent.svelte"
export { default as NotificationDisplay } from "./Notification/NotificationDisplay.svelte" export { default as NotificationDisplay } from "./Notification/NotificationDisplay.svelte"
export { default as Notification } from "./Notification/Notification.svelte"
export { default as SideNavigation } from "./SideNavigation/Navigation.svelte" export { default as SideNavigation } from "./SideNavigation/Navigation.svelte"
export { default as SideNavigationItem } from "./SideNavigation/Item.svelte" export { default as SideNavigationItem } from "./SideNavigation/Item.svelte"
export { default as DatePicker } from "./Form/DatePicker.svelte" export { default as DatePicker } from "./Form/DatePicker.svelte"

View File

@ -0,0 +1,17 @@
<script>
import { Body } from "@budibase/bbui"
</script>
<div class="root">
<Body size="S">This action doesn't require any additional settings.</Body>
<Body size="S">
This action won't do anything if there isn't a screen modal open.
</Body>
</div>
<style>
.root {
max-width: 800px;
margin: 0 auto;
}
</style>

View File

@ -1,5 +1,5 @@
<script> <script>
import { Label } from "@budibase/bbui" import { Label, Checkbox } from "@budibase/bbui"
import { getBindableProperties } from "builderStore/dataBinding" import { getBindableProperties } from "builderStore/dataBinding"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
@ -18,19 +18,17 @@
on:change={value => (parameters.url = value.detail)} on:change={value => (parameters.url = value.detail)}
{bindings} {bindings}
/> />
<div />
<Checkbox text="Open screen in modal" bind:value={parameters.peek} />
</div> </div>
<style> <style>
.root { .root {
display: flex; display: grid;
flex-direction: row; align-items: center;
align-items: baseline; gap: var(--spacing-m);
grid-template-columns: auto 1fr;
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
} }
.root :global(> div) {
flex: 1;
margin-left: var(--spacing-l);
}
</style> </style>

View File

@ -6,6 +6,7 @@ import TriggerAutomation from "./TriggerAutomation.svelte"
import ValidateForm from "./ValidateForm.svelte" import ValidateForm from "./ValidateForm.svelte"
import LogOut from "./LogOut.svelte" import LogOut from "./LogOut.svelte"
import ClearForm from "./ClearForm.svelte" import ClearForm from "./ClearForm.svelte"
import CloseScreenModal from "./CloseScreenModal.svelte"
// Defines which actions are available to configure in the front end. // Defines which actions are available to configure in the front end.
// Unfortunately the "name" property is used as the identifier so please don't // Unfortunately the "name" property is used as the identifier so please don't
@ -47,4 +48,8 @@ export default [
name: "Clear Form", name: "Clear Form",
component: ClearForm, component: ClearForm,
}, },
{
name: "Close Screen Modal",
component: CloseScreenModal,
},
] ]

View File

@ -38,15 +38,15 @@ const makeApiCall = async ({ method, url, body, json = true }) => {
case 200: case 200:
return response.json() return response.json()
case 401: case 401:
notificationStore.danger("Invalid credentials") notificationStore.actions.error("Invalid credentials")
return handleError(`Invalid credentials`) return handleError(`Invalid credentials`)
case 404: case 404:
notificationStore.danger("Not found") notificationStore.actions.warning("Not found")
return handleError(`${url}: Not Found`) return handleError(`${url}: Not Found`)
case 400: case 400:
return handleError(`${url}: Bad Request`) return handleError(`${url}: Bad Request`)
case 403: case 403:
notificationStore.danger( notificationStore.actions.error(
"Your session has expired, or you don't have permission to access that data" "Your session has expired, or you don't have permission to access that data"
) )
return handleError(`${url}: Forbidden`) return handleError(`${url}: Forbidden`)

View File

@ -9,7 +9,7 @@ export const triggerAutomation = async (automationId, fields) => {
body: { fields }, body: { fields },
}) })
res.error res.error
? notificationStore.danger("An error has occurred") ? notificationStore.actions.error("An error has occurred")
: notificationStore.success("Automation triggered") : notificationStore.actions.success("Automation triggered")
return res return res
} }

View File

@ -7,7 +7,7 @@ import API from "./api"
export const executeQuery = async ({ queryId, parameters }) => { export const executeQuery = async ({ queryId, parameters }) => {
const query = await API.get({ url: `/api/queries/${queryId}` }) const query = await API.get({ url: `/api/queries/${queryId}` })
if (query?.datasourceId == null) { if (query?.datasourceId == null) {
notificationStore.danger("That query couldn't be found") notificationStore.actions.error("That query couldn't be found")
return return
} }
const res = await API.post({ const res = await API.post({
@ -17,9 +17,9 @@ export const executeQuery = async ({ queryId, parameters }) => {
}, },
}) })
if (res.error) { if (res.error) {
notificationStore.danger("An error has occurred") notificationStore.actions.error("An error has occurred")
} else if (!query.readable) { } else if (!query.readable) {
notificationStore.success("Query executed successfully") notificationStore.actions.success("Query executed successfully")
dataSourceStore.actions.invalidateDataSource(query.datasourceId) dataSourceStore.actions.invalidateDataSource(query.datasourceId)
} }
return res return res

View File

@ -27,8 +27,8 @@ export const saveRow = async row => {
body: row, body: row,
}) })
res.error res.error
? notificationStore.danger("An error has occurred") ? notificationStore.actions.error("An error has occurred")
: notificationStore.success("Row saved") : notificationStore.actions.success("Row saved")
// Refresh related datasources // Refresh related datasources
dataSourceStore.actions.invalidateDataSource(row.tableId) dataSourceStore.actions.invalidateDataSource(row.tableId)
@ -48,8 +48,8 @@ export const updateRow = async row => {
body: row, body: row,
}) })
res.error res.error
? notificationStore.danger("An error has occurred") ? notificationStore.actions.error("An error has occurred")
: notificationStore.success("Row updated") : notificationStore.actions.success("Row updated")
// Refresh related datasources // Refresh related datasources
dataSourceStore.actions.invalidateDataSource(row.tableId) dataSourceStore.actions.invalidateDataSource(row.tableId)
@ -72,8 +72,8 @@ export const deleteRow = async ({ tableId, rowId, revId }) => {
}, },
}) })
res.error res.error
? notificationStore.danger("An error has occurred") ? notificationStore.actions.error("An error has occurred")
: notificationStore.success("Row deleted") : notificationStore.actions.success("Row deleted")
// Refresh related datasources // Refresh related datasources
dataSourceStore.actions.invalidateDataSource(tableId) dataSourceStore.actions.invalidateDataSource(tableId)
@ -95,8 +95,8 @@ export const deleteRows = async ({ tableId, rows }) => {
}, },
}) })
res.error res.error
? notificationStore.danger("An error has occurred") ? notificationStore.actions.error("An error has occurred")
: notificationStore.success(`${rows.length} row(s) deleted`) : notificationStore.actions.success(`${rows.length} row(s) deleted`)
// Refresh related datasources // Refresh related datasources
dataSourceStore.actions.invalidateDataSource(tableId) dataSourceStore.actions.invalidateDataSource(tableId)

View File

@ -4,6 +4,7 @@
import Component from "./Component.svelte" import Component from "./Component.svelte"
import NotificationDisplay from "./NotificationDisplay.svelte" import NotificationDisplay from "./NotificationDisplay.svelte"
import ConfirmationDisplay from "./ConfirmationDisplay.svelte" import ConfirmationDisplay from "./ConfirmationDisplay.svelte"
import PeekScreenDisplay from "./PeekScreenDisplay.svelte"
import Provider from "./Provider.svelte" import Provider from "./Provider.svelte"
import SDK from "../sdk" import SDK from "../sdk"
import { import {
@ -100,6 +101,7 @@
</div> </div>
<NotificationDisplay /> <NotificationDisplay />
<ConfirmationDisplay /> <ConfirmationDisplay />
<PeekScreenDisplay />
<!-- Key block needs to be outside the if statement or it breaks --> <!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId} {#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder} {#if $builderStore.inBuilder}

View File

@ -1,36 +1,34 @@
<script> <script>
import { flip } from "svelte/animate" import { notificationStore } from "../store"
import { Notification } from "@budibase/bbui"
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
import { getContext } from "svelte"
const { notifications } = getContext("sdk")
export let themes = {
danger: "#E26D69",
success: "#84C991",
warning: "#f0ad4e",
info: "#5bc0de",
default: "#aaaaaa",
}
</script> </script>
<div class="notifications"> <div class="notifications">
{#each $notifications as notification (notification.id)} {#if $notificationStore}
{#key $notificationStore.id}
<div <div
animate:flip in:fly={{
class="toast" duration: 300,
style="background: {themes[notification.type]};" y: -20,
transition:fly={{ y: -30 }} delay: $notificationStore.delay ? 300 : 0,
}}
out:fly={{ y: -20, duration: 150 }}
> >
<div class="content">{notification.message}</div> <Notification
{#if notification.icon}<i class={notification.icon} />{/if} type={$notificationStore.type}
message={$notificationStore.message}
icon={$notificationStore.icon}
/>
</div> </div>
{/each} {/key}
{/if}
</div> </div>
<style> <style>
.notifications { .notifications {
position: fixed; position: fixed;
top: 10px; top: 20px;
left: 0; left: 0;
right: 0; right: 0;
margin: 0 auto; margin: 0 auto;
@ -42,19 +40,4 @@
align-items: center; align-items: center;
pointer-events: none; pointer-events: none;
} }
.toast {
flex: 0 0 auto;
margin-bottom: 10px;
border-radius: var(--border-radius-s);
/* The toasts now support being auto sized, so this static width could be removed */
width: 40vw;
}
.content {
padding: 10px;
display: block;
color: white;
font-weight: 600;
}
</style> </style>

View File

@ -0,0 +1,120 @@
<script>
import {
peekStore,
dataSourceStore,
notificationStore,
routeStore,
} from "../store"
import { Modal, ModalContent, ActionButton } from "@budibase/bbui"
import { onDestroy } from "svelte"
let iframe
let listenersAttached = false
const invalidateDataSource = event => {
const { dataSourceId } = event.detail
dataSourceStore.actions.invalidateDataSource(dataSourceId)
}
const proxyNotification = event => {
const { message, type, icon } = event.detail
notificationStore.actions.send(message, type, icon)
}
const attachListeners = () => {
// Mirror datasource invalidation to keep the parent window up to date
iframe.contentWindow.addEventListener(
"invalidate-datasource",
invalidateDataSource
)
// Listen for a close event to close the screen peek
iframe.contentWindow.addEventListener(
"close-screen-modal",
peekStore.actions.hidePeek
)
// Proxy notifications back to the parent window instead of iframe
iframe.contentWindow.addEventListener("notification", proxyNotification)
}
const handleCancel = () => {
peekStore.actions.hidePeek()
iframe.contentWindow.removeEventListener(
"invalidate-datasource",
invalidateDataSource
)
iframe.contentWindow.removeEventListener(
"close-screen-modal",
peekStore.actions.hidePeek
)
iframe.contentWindow.removeEventListener("notification", proxyNotification)
}
const handleFullscreen = () => {
if ($peekStore.external) {
window.location = $peekStore.href
} else {
routeStore.actions.navigate($peekStore.url)
handleCancel()
}
}
$: {
if (iframe && !listenersAttached) {
attachListeners()
listenersAttached = true
} else if (!iframe) {
listenersAttached = false
}
}
onDestroy(() => {
if (iframe) {
handleCancel()
}
})
</script>
{#if $peekStore.showPeek}
<Modal fixed on:cancel={handleCancel}>
<div class="actions spectrum--darkest" slot="outside">
<ActionButton size="S" quiet icon="OpenIn" on:click={handleFullscreen}>
Full screen
</ActionButton>
<ActionButton size="S" quiet icon="Close" on:click={handleCancel}>
Close
</ActionButton>
</div>
<ModalContent
showCancelButton={false}
showConfirmButton={false}
size="L"
showDivider={false}
showCloseIcon={false}
>
<iframe title="Peek" bind:this={iframe} src={$peekStore.href} />
</ModalContent>
</Modal>
{/if}
<style>
iframe {
margin: -40px;
border: none;
width: calc(100% + 80px);
height: 640px;
max-height: calc(100vh - 120px);
transition: width 1s ease, height 1s ease, top 1s ease, left 1s ease;
border-radius: var(--spectrum-global-dimension-size-100);
}
.actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
position: absolute;
top: 0;
width: 640px;
max-width: 100%;
}
</style>

View File

@ -1,6 +1,6 @@
<script> <script>
import { setContext, getContext } from "svelte" import { setContext, getContext } from "svelte"
import Router from "svelte-spa-router" import Router, { querystring } from "svelte-spa-router"
import { routeStore } from "../store" import { routeStore } from "../store"
import Screen from "./Screen.svelte" import Screen from "./Screen.svelte"
@ -16,6 +16,18 @@
id: $routeStore.routeSessionId, id: $routeStore.routeSessionId,
} }
// Keep query params up to date
$: {
let queryParams = {}
if ($querystring) {
const urlSearchParams = new URLSearchParams($querystring)
for (const [key, value] of urlSearchParams) {
queryParams[key] = value
}
}
routeStore.actions.setQueryParams(queryParams)
}
const getRouterConfig = routes => { const getRouterConfig = routes => {
let config = {} let config = {}
routes.forEach(route => { routes.forEach(route => {

View File

@ -15,7 +15,7 @@ import { ActionTypes } from "./constants"
export default { export default {
API, API,
authStore, authStore,
notifications: notificationStore, notificationStore,
routeStore, routeStore,
screenStore, screenStore,
builderStore, builderStore,

View File

@ -1,5 +1,4 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { notificationStore } from "./notification"
export const createDataSourceStore = () => { export const createDataSourceStore = () => {
const store = writable([]) const store = writable([])
@ -67,12 +66,17 @@ export const createDataSourceStore = () => {
const relatedInstances = get(store).filter(instance => { const relatedInstances = get(store).filter(instance => {
return instance.dataSourceId === dataSourceId return instance.dataSourceId === dataSourceId
}) })
if (relatedInstances?.length) {
notificationStore.blockNotifications(1000)
}
relatedInstances?.forEach(instance => { relatedInstances?.forEach(instance => {
instance.refresh() instance.refresh()
}) })
// Emit this as a window event, so parent screens which are iframing us in
// can also invalidate the same datasource
window.dispatchEvent(
new CustomEvent("invalidate-datasource", {
detail: { dataSourceId },
})
)
} }
return { return {

View File

@ -6,6 +6,7 @@ export { screenStore } from "./screens"
export { builderStore } from "./builder" export { builderStore } from "./builder"
export { dataSourceStore } from "./dataSource" export { dataSourceStore } from "./dataSource"
export { confirmationStore } from "./confirmation" export { confirmationStore } from "./confirmation"
export { peekStore } from "./peek"
// Context stores are layered and duplicated, so it is not a singleton // Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context" export { createContextStore } from "./context"

View File

@ -1,56 +1,63 @@
import { writable } from "svelte/store" import { writable, get } from "svelte/store"
import { generate } from "shortid"
import { routeStore } from "./routes"
const NOTIFICATION_TIMEOUT = 3000 const NOTIFICATION_TIMEOUT = 3000
const createNotificationStore = () => { const createNotificationStore = () => {
const timeoutIds = new Set() let timeout
const _notifications = writable([], () => { let block = false
const store = writable(null, () => {
return () => { return () => {
// clear all the timers clearTimeout(timeout)
timeoutIds.forEach(timeoutId => {
clearTimeout(timeoutId)
})
_notifications.set([])
} }
}) })
let block = false
const blockNotifications = (timeout = 1000) => { const blockNotifications = (timeout = 1000) => {
block = true block = true
setTimeout(() => (block = false), timeout) setTimeout(() => (block = false), timeout)
} }
const send = (message, type = "default") => { const send = (message, type = "info", icon) => {
if (block) { if (block) {
return return
} }
let _id = id()
_notifications.update(state => { // If peeking, pass notifications back to parent window
return [...state, { id: _id, type, message }] if (get(routeStore).queryParams?.peek) {
window.dispatchEvent(
new CustomEvent("notification", {
detail: { message, type, icon },
}) })
const timeoutId = setTimeout(() => { )
_notifications.update(state => { return
return state.filter(({ id }) => id !== _id)
})
}, NOTIFICATION_TIMEOUT)
timeoutIds.add(timeoutId)
} }
const { subscribe } = _notifications store.set({
id: generate(),
type,
message,
icon,
delay: get(store) != null,
})
clearTimeout(timeout)
timeout = setTimeout(() => {
store.set(null)
}, NOTIFICATION_TIMEOUT)
}
return { return {
subscribe, subscribe: store.subscribe,
actions: {
send, send,
danger: msg => send(msg, "danger"), info: msg => send(msg, "info", "Info"),
warning: msg => send(msg, "warning"), success: msg => send(msg, "success", "CheckmarkCircle"),
info: msg => send(msg, "info"), warning: msg => send(msg, "warning", "Alert"),
success: msg => send(msg, "success"), error: msg => send(msg, "error", "Alert"),
blockNotifications, blockNotifications,
},
} }
} }
function id() {
return "_" + Math.random().toString(36).substr(2, 9)
}
export const notificationStore = createNotificationStore() export const notificationStore = createNotificationStore()

View File

@ -0,0 +1,36 @@
import { writable } from "svelte/store"
const initialState = {
showPeek: false,
url: null,
href: null,
external: false,
}
const createPeekStore = () => {
const store = writable(initialState)
const showPeek = url => {
let href = url
let external = !url.startsWith("/")
if (!external) {
href = `${window.location.href.split("#")[0]}#${url}?peek=true`
}
store.set({
showPeek: true,
url,
href,
external,
})
}
const hidePeek = () => {
store.set(initialState)
}
return {
subscribe: store.subscribe,
actions: { showPeek, hidePeek },
}
}
export const peekStore = createPeekStore()

View File

@ -9,6 +9,7 @@ const createRouteStore = () => {
activeRoute: null, activeRoute: null,
routeSessionId: Math.random(), routeSessionId: Math.random(),
routerLoaded: false, routerLoaded: false,
queryParams: {},
} }
const store = writable(initialState) const store = writable(initialState)
@ -41,6 +42,17 @@ const createRouteStore = () => {
return state return state
}) })
} }
const setQueryParams = queryParams => {
store.update(state => {
state.queryParams = {
...queryParams,
// Never unset the peek param - screen peek modals should always be
// in a peek state, even if they navigate to a different page
peek: queryParams.peek || state.queryParams?.peek,
}
return state
})
}
const setActiveRoute = route => { const setActiveRoute = route => {
store.update(state => { store.update(state => {
state.activeRoute = state.routes.find(x => x.path === route) state.activeRoute = state.routes.find(x => x.path === route)
@ -58,6 +70,7 @@ const createRouteStore = () => {
fetchRoutes, fetchRoutes,
navigate, navigate,
setRouteParams, setRouteParams,
setQueryParams,
setActiveRoute, setActiveRoute,
setRouterLoaded, setRouterLoaded,
}, },

View File

@ -4,6 +4,7 @@ import {
builderStore, builderStore,
confirmationStore, confirmationStore,
authStore, authStore,
peekStore,
} from "../store" } from "../store"
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api" import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
import { ActionTypes } from "../constants" import { ActionTypes } from "../constants"
@ -39,8 +40,11 @@ const triggerAutomationHandler = async action => {
} }
const navigationHandler = action => { const navigationHandler = action => {
const { url } = action.parameters const { url, peek } = action.parameters
if (url) { if (url) {
if (peek) {
peekStore.actions.showPeek(url)
} else {
const external = !url.startsWith("/") const external = !url.startsWith("/")
if (external) { if (external) {
window.location.href = url window.location.href = url
@ -49,6 +53,7 @@ const navigationHandler = action => {
} }
} }
} }
}
const queryExecutionHandler = async action => { const queryExecutionHandler = async action => {
const { datasourceId, queryId, queryParams } = action.parameters const { datasourceId, queryId, queryParams } = action.parameters
@ -94,6 +99,12 @@ const clearFormHandler = async (action, context) => {
) )
} }
const closeScreenModalHandler = () => {
// Emit this as a window event, so parent screens which are iframing us in
// can close the modal
window.dispatchEvent(new Event("close-screen-modal"))
}
const handlerMap = { const handlerMap = {
["Save Row"]: saveRowHandler, ["Save Row"]: saveRowHandler,
["Delete Row"]: deleteRowHandler, ["Delete Row"]: deleteRowHandler,
@ -104,6 +115,7 @@ const handlerMap = {
["Refresh Datasource"]: refreshDatasourceHandler, ["Refresh Datasource"]: refreshDatasourceHandler,
["Log Out"]: logoutHandler, ["Log Out"]: logoutHandler,
["Clear Form"]: clearFormHandler, ["Clear Form"]: clearFormHandler,
["Close Screen Modal"]: closeScreenModalHandler,
} }
const confirmTextMap = { const confirmTextMap = {

View File

@ -1,6 +1,7 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { Heading, Icon } from "@budibase/bbui" import { Heading, Icon } from "@budibase/bbui"
import { routeStore } from "../../client/src/store"
const { styleable, linkable, builderStore } = getContext("sdk") const { styleable, linkable, builderStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
@ -26,6 +27,14 @@
Small: "s", Small: "s",
} }
// Permanently go into peek mode if we ever get the peek flag
let isPeeking = false
$: {
if ($routeStore.queryParams?.peek) {
isPeeking = true
}
}
$: validLinks = links?.filter(link => link.text && link.url) || [] $: validLinks = links?.filter(link => link.text && link.url) || []
$: typeClass = navigationClasses[navigation] || "none" $: typeClass = navigationClasses[navigation] || "none"
$: widthClass = widthClasses[width] || "l" $: widthClass = widthClasses[width] || "l"
@ -51,7 +60,7 @@
<div class="layout layout--{typeClass}" use:styleable={$component.styles}> <div class="layout layout--{typeClass}" use:styleable={$component.styles}>
{#if typeClass !== "none"} {#if typeClass !== "none"}
<div class="nav-wrapper" class:sticky> <div class="nav-wrapper" class:sticky class:hidden={isPeeking}>
<div class="nav nav--{typeClass} size--{widthClass}"> <div class="nav nav--{typeClass} size--{widthClass}">
<div class="nav-header"> <div class="nav-header">
{#if validLinks?.length} {#if validLinks?.length}
@ -139,6 +148,9 @@
border-bottom: 1px solid var(--spectrum-global-color-gray-300); border-bottom: 1px solid var(--spectrum-global-color-gray-300);
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.05);
} }
.nav-wrapper.hidden {
display: none;
}
.layout--top .nav-wrapper.sticky { .layout--top .nav-wrapper.sticky {
position: sticky; position: sticky;
top: 0; top: 0;

View File

@ -10,12 +10,12 @@
let fieldState let fieldState
let fieldApi let fieldApi
const { API, notifications } = getContext("sdk") const { API, notificationStore } = getContext("sdk")
const formContext = getContext("form") const formContext = getContext("form")
const BYTES_IN_MB = 1000000 const BYTES_IN_MB = 1000000
const handleFileTooLarge = fileSizeLimit => { const handleFileTooLarge = fileSizeLimit => {
notifications.warning( notificationStore.actions.warning(
`Files cannot exceed ${ `Files cannot exceed ${
fileSizeLimit / BYTES_IN_MB fileSizeLimit / BYTES_IN_MB
} MB. Please try again with smaller files.` } MB. Please try again with smaller files.`