mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
25 changed files with 378 additions and 137 deletions
@ -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> |
|||
@ -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> |
|||
@ -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,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 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 }] |
|||
|
|||
// If peeking, pass notifications back to parent window
|
|||
if (get(routeStore).queryParams?.peek) { |
|||
window.dispatchEvent( |
|||
new CustomEvent("notification", { |
|||
detail: { message, type, icon }, |
|||
}) |
|||
) |
|||
return |
|||
} |
|||
|
|||
store.set({ |
|||
id: generate(), |
|||
type, |
|||
message, |
|||
icon, |
|||
delay: get(store) != null, |
|||
}) |
|||
const timeoutId = setTimeout(() => { |
|||
_notifications.update(state => { |
|||
return state.filter(({ id }) => id !== _id) |
|||
}) |
|||
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"), |
|||
blockNotifications, |
|||
subscribe: store.subscribe, |
|||
actions: { |
|||
send, |
|||
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() |
|||
Loading…
Reference in new issue