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 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 }, |
||||
|
}) |
||||
|
) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
store.set({ |
||||
|
id: generate(), |
||||
|
type, |
||||
|
message, |
||||
|
icon, |
||||
|
delay: get(store) != null, |
||||
}) |
}) |
||||
const timeoutId = setTimeout(() => { |
clearTimeout(timeout) |
||||
_notifications.update(state => { |
timeout = setTimeout(() => { |
||||
return state.filter(({ id }) => id !== _id) |
store.set(null) |
||||
}) |
|
||||
}, NOTIFICATION_TIMEOUT) |
}, NOTIFICATION_TIMEOUT) |
||||
timeoutIds.add(timeoutId) |
|
||||
} |
} |
||||
|
|
||||
const { subscribe } = _notifications |
|
||||
|
|
||||
return { |
return { |
||||
subscribe, |
subscribe: store.subscribe, |
||||
send, |
actions: { |
||||
danger: msg => send(msg, "danger"), |
send, |
||||
warning: msg => send(msg, "warning"), |
info: msg => send(msg, "info", "Info"), |
||||
info: msg => send(msg, "info"), |
success: msg => send(msg, "success", "CheckmarkCircle"), |
||||
success: msg => send(msg, "success"), |
warning: msg => send(msg, "warning", "Alert"), |
||||
blockNotifications, |
error: msg => send(msg, "error", "Alert"), |
||||
|
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() |
||||
Loading…
Reference in new issue