Add initial work on peeking screens, only show one notification at a time, use spectrum notifications
This commit is contained in:
parent
9ad07f3cf7
commit
e5418deb89
|
@ -38,15 +38,15 @@ const makeApiCall = async ({ method, url, body, json = true }) => {
|
|||
case 200:
|
||||
return response.json()
|
||||
case 401:
|
||||
notificationStore.danger("Invalid credentials")
|
||||
notificationStore.actions.error("Invalid credentials")
|
||||
return handleError(`Invalid credentials`)
|
||||
case 404:
|
||||
notificationStore.danger("Not found")
|
||||
notificationStore.actions.warning("Not found")
|
||||
return handleError(`${url}: Not Found`)
|
||||
case 400:
|
||||
return handleError(`${url}: Bad Request`)
|
||||
case 403:
|
||||
notificationStore.danger(
|
||||
notificationStore.actions.error(
|
||||
"Your session has expired, or you don't have permission to access that data"
|
||||
)
|
||||
return handleError(`${url}: Forbidden`)
|
||||
|
|
|
@ -9,7 +9,7 @@ export const triggerAutomation = async (automationId, fields) => {
|
|||
body: { fields },
|
||||
})
|
||||
res.error
|
||||
? notificationStore.danger("An error has occurred")
|
||||
: notificationStore.success("Automation triggered")
|
||||
? notificationStore.actions.error("An error has occurred")
|
||||
: notificationStore.actions.success("Automation triggered")
|
||||
return res
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import API from "./api"
|
|||
export const executeQuery = async ({ queryId, parameters }) => {
|
||||
const query = await API.get({ url: `/api/queries/${queryId}` })
|
||||
if (query?.datasourceId == null) {
|
||||
notificationStore.danger("That query couldn't be found")
|
||||
notificationStore.actions.error("That query couldn't be found")
|
||||
return
|
||||
}
|
||||
const res = await API.post({
|
||||
|
@ -17,9 +17,9 @@ export const executeQuery = async ({ queryId, parameters }) => {
|
|||
},
|
||||
})
|
||||
if (res.error) {
|
||||
notificationStore.danger("An error has occurred")
|
||||
notificationStore.actions.error("An error has occurred")
|
||||
} else if (!query.readable) {
|
||||
notificationStore.success("Query executed successfully")
|
||||
notificationStore.actions.success("Query executed successfully")
|
||||
dataSourceStore.actions.invalidateDataSource(query.datasourceId)
|
||||
}
|
||||
return res
|
||||
|
|
|
@ -27,8 +27,8 @@ export const saveRow = async row => {
|
|||
body: row,
|
||||
})
|
||||
res.error
|
||||
? notificationStore.danger("An error has occurred")
|
||||
: notificationStore.success("Row saved")
|
||||
? notificationStore.actions.error("An error has occurred")
|
||||
: notificationStore.actions.success("Row saved")
|
||||
|
||||
// Refresh related datasources
|
||||
dataSourceStore.actions.invalidateDataSource(row.tableId)
|
||||
|
@ -48,8 +48,8 @@ export const updateRow = async row => {
|
|||
body: row,
|
||||
})
|
||||
res.error
|
||||
? notificationStore.danger("An error has occurred")
|
||||
: notificationStore.success("Row updated")
|
||||
? notificationStore.actions.error("An error has occurred")
|
||||
: notificationStore.actions.success("Row updated")
|
||||
|
||||
// Refresh related datasources
|
||||
dataSourceStore.actions.invalidateDataSource(row.tableId)
|
||||
|
@ -72,8 +72,8 @@ export const deleteRow = async ({ tableId, rowId, revId }) => {
|
|||
},
|
||||
})
|
||||
res.error
|
||||
? notificationStore.danger("An error has occurred")
|
||||
: notificationStore.success("Row deleted")
|
||||
? notificationStore.actions.error("An error has occurred")
|
||||
: notificationStore.actions.success("Row deleted")
|
||||
|
||||
// Refresh related datasources
|
||||
dataSourceStore.actions.invalidateDataSource(tableId)
|
||||
|
@ -95,8 +95,8 @@ export const deleteRows = async ({ tableId, rows }) => {
|
|||
},
|
||||
})
|
||||
res.error
|
||||
? notificationStore.danger("An error has occurred")
|
||||
: notificationStore.success(`${rows.length} row(s) deleted`)
|
||||
? notificationStore.actions.error("An error has occurred")
|
||||
: notificationStore.actions.success(`${rows.length} row(s) deleted`)
|
||||
|
||||
// Refresh related datasources
|
||||
dataSourceStore.actions.invalidateDataSource(tableId)
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import Component from "./Component.svelte"
|
||||
import NotificationDisplay from "./NotificationDisplay.svelte"
|
||||
import ConfirmationDisplay from "./ConfirmationDisplay.svelte"
|
||||
import PeekScreenDisplay from "./PeekScreenDisplay.svelte"
|
||||
import Provider from "./Provider.svelte"
|
||||
import SDK from "../sdk"
|
||||
import {
|
||||
|
@ -100,6 +101,7 @@
|
|||
</div>
|
||||
<NotificationDisplay />
|
||||
<ConfirmationDisplay />
|
||||
<PeekScreenDisplay />
|
||||
<!-- Key block needs to be outside the if statement or it breaks -->
|
||||
{#key $builderStore.selectedComponentId}
|
||||
{#if $builderStore.inBuilder}
|
||||
|
|
|
@ -1,36 +1,34 @@
|
|||
<script>
|
||||
import { flip } from "svelte/animate"
|
||||
import { notificationStore } from "../store"
|
||||
import { Notification } from "@budibase/bbui"
|
||||
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>
|
||||
|
||||
<div class="notifications">
|
||||
{#each $notifications as notification (notification.id)}
|
||||
{#if $notificationStore}
|
||||
{#key $notificationStore.id}
|
||||
<div
|
||||
animate:flip
|
||||
class="toast"
|
||||
style="background: {themes[notification.type]};"
|
||||
transition:fly={{ y: -30 }}
|
||||
in:fly={{
|
||||
duration: 300,
|
||||
y: -20,
|
||||
delay: $notificationStore.delay ? 300 : 0,
|
||||
}}
|
||||
out:fly={{ y: -20, duration: 150 }}
|
||||
>
|
||||
<div class="content">{notification.message}</div>
|
||||
{#if notification.icon}<i class={notification.icon} />{/if}
|
||||
<Notification
|
||||
type={$notificationStore.type}
|
||||
message={$notificationStore.message}
|
||||
icon={$notificationStore.icon}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.notifications {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
top: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
|
@ -42,19 +40,4 @@
|
|||
align-items: center;
|
||||
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>
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
<script>
|
||||
import { peekStore, dataSourceStore, routeStore } from "../store"
|
||||
import { Modal, ModalContent, Button, Divider, Layout } from "@budibase/bbui"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
let iframe
|
||||
let fullscreen = false
|
||||
|
||||
const invalidateDataSource = event => {
|
||||
const { dataSourceId } = event.detail
|
||||
dataSourceStore.actions.invalidateDataSource(dataSourceId)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
iframe.contentWindow.removeEventListener(
|
||||
"invalidate-datasource",
|
||||
invalidateDataSource
|
||||
)
|
||||
peekStore.actions.hidePeek()
|
||||
fullscreen = false
|
||||
}
|
||||
|
||||
const navigate = () => {
|
||||
if ($peekStore.external) {
|
||||
window.location = $peekStore.href
|
||||
} else {
|
||||
routeStore.actions.navigate($peekStore.url)
|
||||
}
|
||||
peekStore.actions.hidePeek()
|
||||
}
|
||||
|
||||
$: {
|
||||
if (iframe) {
|
||||
iframe.contentWindow.addEventListener(
|
||||
"invalidate-datasource",
|
||||
invalidateDataSource
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (iframe) {
|
||||
iframe.contentWindow.removeEventListener(
|
||||
"invalidate-datasource",
|
||||
invalidateDataSource
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $peekStore.showPeek}
|
||||
<Modal fixed on:cancel={handleCancel}>
|
||||
<ModalContent
|
||||
cancelText="Close"
|
||||
showConfirmButton={false}
|
||||
size="XL"
|
||||
title="Screen Peek"
|
||||
showDivider={false}
|
||||
>
|
||||
<iframe title="Peek" bind:this={iframe} src={$peekStore.href} />
|
||||
<div slot="footer">
|
||||
<Button cta on:click={navigate}>Full screen</Button>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
margin: 0 -40px;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-top: 1px solid var(--spectrum-global-color-gray-300);
|
||||
width: calc(100% + 80px);
|
||||
height: 640px;
|
||||
transition: width 1s ease, height 1s ease, top 1s ease, left 1s ease;
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { setContext, getContext } from "svelte"
|
||||
import Router from "svelte-spa-router"
|
||||
import Router, { querystring } from "svelte-spa-router"
|
||||
import { routeStore } from "../store"
|
||||
import Screen from "./Screen.svelte"
|
||||
|
||||
|
@ -16,6 +16,18 @@
|
|||
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 => {
|
||||
let config = {}
|
||||
routes.forEach(route => {
|
||||
|
|
|
@ -15,7 +15,7 @@ import { ActionTypes } from "./constants"
|
|||
export default {
|
||||
API,
|
||||
authStore,
|
||||
notifications: notificationStore,
|
||||
notificationStore,
|
||||
routeStore,
|
||||
screenStore,
|
||||
builderStore,
|
||||
|
|
|
@ -67,12 +67,17 @@ export const createDataSourceStore = () => {
|
|||
const relatedInstances = get(store).filter(instance => {
|
||||
return instance.dataSourceId === dataSourceId
|
||||
})
|
||||
if (relatedInstances?.length) {
|
||||
notificationStore.blockNotifications(1000)
|
||||
}
|
||||
relatedInstances?.forEach(instance => {
|
||||
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 {
|
||||
|
|
|
@ -6,6 +6,7 @@ export { screenStore } from "./screens"
|
|||
export { builderStore } from "./builder"
|
||||
export { dataSourceStore } from "./dataSource"
|
||||
export { confirmationStore } from "./confirmation"
|
||||
export { peekStore } from "./peek"
|
||||
|
||||
// Context stores are layered and duplicated, so it is not a singleton
|
||||
export { createContextStore } from "./context"
|
||||
|
|
|
@ -1,56 +1,50 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { writable, get } from "svelte/store"
|
||||
import { generate } from "shortid"
|
||||
|
||||
const NOTIFICATION_TIMEOUT = 3000
|
||||
|
||||
const createNotificationStore = () => {
|
||||
const timeoutIds = new Set()
|
||||
const _notifications = writable([], () => {
|
||||
let timeout
|
||||
let block = false
|
||||
|
||||
const store = writable(null, () => {
|
||||
return () => {
|
||||
// clear all the timers
|
||||
timeoutIds.forEach(timeoutId => {
|
||||
clearTimeout(timeoutId)
|
||||
})
|
||||
_notifications.set([])
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
})
|
||||
let block = false
|
||||
|
||||
const blockNotifications = (timeout = 1000) => {
|
||||
block = true
|
||||
setTimeout(() => (block = false), timeout)
|
||||
}
|
||||
|
||||
const send = (message, type = "default") => {
|
||||
const send = (message, type = "info", icon) => {
|
||||
if (block) {
|
||||
return
|
||||
}
|
||||
let _id = id()
|
||||
_notifications.update(state => {
|
||||
return [...state, { id: _id, type, message }]
|
||||
})
|
||||
const timeoutId = setTimeout(() => {
|
||||
_notifications.update(state => {
|
||||
return state.filter(({ id }) => id !== _id)
|
||||
store.set({
|
||||
id: generate(),
|
||||
type,
|
||||
message,
|
||||
icon,
|
||||
delay: get(store) != null,
|
||||
})
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
store.set(null)
|
||||
}, NOTIFICATION_TIMEOUT)
|
||||
timeoutIds.add(timeoutId)
|
||||
}
|
||||
|
||||
const { subscribe } = _notifications
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
send,
|
||||
danger: msg => send(msg, "danger"),
|
||||
warning: msg => send(msg, "warning"),
|
||||
info: msg => send(msg, "info"),
|
||||
success: msg => send(msg, "success"),
|
||||
subscribe: store.subscribe,
|
||||
actions: {
|
||||
info: msg => send(msg, "info", "Info"),
|
||||
success: msg => send(msg, "success", "CheckmarkCircle"),
|
||||
warning: msg => send(msg, "warning", "Alert"),
|
||||
error: msg => send(msg, "error", "Alert"),
|
||||
blockNotifications,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function id() {
|
||||
return "_" + Math.random().toString(36).substr(2, 9)
|
||||
}
|
||||
|
||||
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,
|
||||
routeSessionId: Math.random(),
|
||||
routerLoaded: false,
|
||||
queryParams: {},
|
||||
}
|
||||
const store = writable(initialState)
|
||||
|
||||
|
@ -41,6 +42,12 @@ const createRouteStore = () => {
|
|||
return state
|
||||
})
|
||||
}
|
||||
const setQueryParams = queryParams => {
|
||||
store.update(state => {
|
||||
state.queryParams = queryParams
|
||||
return state
|
||||
})
|
||||
}
|
||||
const setActiveRoute = route => {
|
||||
store.update(state => {
|
||||
state.activeRoute = state.routes.find(x => x.path === route)
|
||||
|
@ -58,6 +65,7 @@ const createRouteStore = () => {
|
|||
fetchRoutes,
|
||||
navigate,
|
||||
setRouteParams,
|
||||
setQueryParams,
|
||||
setActiveRoute,
|
||||
setRouterLoaded,
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
builderStore,
|
||||
confirmationStore,
|
||||
authStore,
|
||||
peekStore,
|
||||
} from "../store"
|
||||
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
|
||||
import { ActionTypes } from "../constants"
|
||||
|
@ -39,8 +40,11 @@ const triggerAutomationHandler = async action => {
|
|||
}
|
||||
|
||||
const navigationHandler = action => {
|
||||
const { url } = action.parameters
|
||||
const { url, peek } = action.parameters
|
||||
if (url) {
|
||||
if (peek) {
|
||||
peekStore.actions.showPeek(url)
|
||||
} else {
|
||||
const external = !url.startsWith("/")
|
||||
if (external) {
|
||||
window.location.href = url
|
||||
|
@ -48,6 +52,7 @@ const navigationHandler = action => {
|
|||
routeStore.actions.navigate(action.parameters.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const queryExecutionHandler = async action => {
|
||||
|
|
|
@ -10,12 +10,12 @@
|
|||
let fieldState
|
||||
let fieldApi
|
||||
|
||||
const { API, notifications } = getContext("sdk")
|
||||
const { API, notificationStore } = getContext("sdk")
|
||||
const formContext = getContext("form")
|
||||
const BYTES_IN_MB = 1000000
|
||||
|
||||
const handleFileTooLarge = fileSizeLimit => {
|
||||
notifications.warning(
|
||||
notificationStore.actions.warning(
|
||||
`Files cannot exceed ${
|
||||
fileSizeLimit / BYTES_IN_MB
|
||||
} MB. Please try again with smaller files.`
|
||||
|
|
Loading…
Reference in New Issue