Merge pull request #2233 from Budibase/peek-screen
Open screen in modal
This commit is contained in:
commit
6a9358086c
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -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`)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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 => {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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.`
|
||||||
|
|
Loading…
Reference in New Issue