Merge remote-tracking branch 'origin/cheeks-lab-day-portal-redesign' into feature/user-onboarding-overlays

This commit is contained in:
Dean 2023-01-12 15:59:30 +00:00
commit 73fcbdbb6f
27 changed files with 370 additions and 360 deletions

View File

@ -1,11 +1,11 @@
const ignoredClasses = [".flatpickr-calendar", ".modal-container"] const ignoredClasses = [".flatpickr-calendar"]
let clickHandlers = [] let clickHandlers = []
/** /**
* Handle a body click event * Handle a body click event
*/ */
const handleClick = event => { const handleClick = event => {
// Ignore click if needed // Ignore click if this is an ignored class
for (let className of ignoredClasses) { for (let className of ignoredClasses) {
if (event.target.closest(className)) { if (event.target.closest(className)) {
return return
@ -14,9 +14,18 @@ const handleClick = event => {
// Process handlers // Process handlers
clickHandlers.forEach(handler => { clickHandlers.forEach(handler => {
if (!handler.element.contains(event.target)) { if (handler.element.contains(event.target)) {
handler.callback?.(event) return
} }
// Ignore clicks for modals, unless the handler is registered from a modal
const sourceInModal = handler.element.closest(".spectrum-Modal") != null
const clickInModal = event.target.closest(".spectrum-Modal") != null
if (clickInModal && !sourceInModal) {
return
}
handler.callback?.(event)
}) })
} }
document.documentElement.addEventListener("click", handleClick, true) document.documentElement.addEventListener("click", handleClick, true)

View File

@ -1,75 +1,68 @@
export default function positionDropdown(element, { anchor, align, maxWidth }) { export default function positionDropdown(
let positionSide = "top" element,
let maxHeight = 0 { anchor, align, maxWidth, useAnchorWidth }
let dimensions = getDimensions(anchor) ) {
const update = () => {
const anchorBounds = anchor.getBoundingClientRect()
const elementBounds = element.getBoundingClientRect()
let styles = {
maxHeight: null,
minWidth: null,
maxWidth,
left: null,
top: null,
}
function getDimensions() { // Determine vertical styles
const { if (window.innerHeight - anchorBounds.bottom < 100) {
bottom, styles.top = anchorBounds.top - elementBounds.height - 5
top: spaceAbove,
left,
width,
} = anchor.getBoundingClientRect()
const spaceBelow = window.innerHeight - bottom
const containerRect = element.getBoundingClientRect()
let y
if (spaceAbove > spaceBelow) {
positionSide = "bottom"
maxHeight = spaceAbove - 20
y = window.innerHeight - spaceAbove + 5
} else { } else {
positionSide = "top" styles.top = anchorBounds.bottom + 5
y = bottom + 5 styles.maxHeight = window.innerHeight - anchorBounds.bottom - 20
maxHeight = spaceBelow - 20
} }
return { // Determine horizontal styles
[positionSide]: y, if (!maxWidth && useAnchorWidth) {
left, styles.maxWidth = anchorBounds.width
width,
containerWidth: containerRect.width,
} }
if (useAnchorWidth) {
styles.minWidth = anchorBounds.width
}
if (align === "right") {
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width
} else if (align === "right-side") {
styles.left = anchorBounds.left + anchorBounds.width
} else {
styles.left = anchorBounds.left
}
// Apply styles
Object.entries(styles).forEach(([style, value]) => {
if (value) {
element.style[style] = `${value.toFixed(0)}px`
} else {
element.style[style] = null
}
})
} }
function calcLeftPosition() { // Apply initial styles which don't need to change
let left
if (align == "right") {
left = dimensions.left + dimensions.width - dimensions.containerWidth
} else if (align == "right-side") {
left = dimensions.left + dimensions.width
} else {
left = dimensions.left
}
return left
}
element.style.position = "absolute" element.style.position = "absolute"
element.style.zIndex = "9999" element.style.zIndex = "9999"
if (maxWidth) {
element.style.maxWidth = `${maxWidth}px`
}
element.style.minWidth = `${dimensions.width}px`
element.style.maxHeight = `${maxHeight.toFixed(0)}px`
element.style.transformOrigin = `center ${positionSide}`
element.style[positionSide] = `${dimensions[positionSide]}px`
element.style.left = `${calcLeftPosition(dimensions).toFixed(0)}px`
// Observe both anchor and element and resize the popover as appropriate
const resizeObserver = new ResizeObserver(entries => { const resizeObserver = new ResizeObserver(entries => {
entries.forEach(() => { entries.forEach(update)
dimensions = getDimensions()
element.style[positionSide] = `${dimensions[positionSide]}px`
element.style.left = `${calcLeftPosition(dimensions).toFixed(0)}px`
})
}) })
resizeObserver.observe(anchor) resizeObserver.observe(anchor)
resizeObserver.observe(element) resizeObserver.observe(element)
document.addEventListener("scroll", update, true)
return { return {
destroy() { destroy() {
resizeObserver.disconnect() resizeObserver.disconnect()
document.removeEventListener("scroll", update, true)
}, },
} }
} }

View File

@ -2,12 +2,12 @@
import "@spectrum-css/picker/dist/index-vars.css" import "@spectrum-css/picker/dist/index-vars.css"
import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css"
import "@spectrum-css/menu/dist/index-vars.css" import "@spectrum-css/menu/dist/index-vars.css"
import { fly } from "svelte/transition"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import clickOutside from "../../Actions/click_outside" import clickOutside from "../../Actions/click_outside"
import Search from "./Search.svelte" import Search from "./Search.svelte"
import Icon from "../../Icon/Icon.svelte" import Icon from "../../Icon/Icon.svelte"
import StatusLight from "../../StatusLight/StatusLight.svelte" import StatusLight from "../../StatusLight/StatusLight.svelte"
import Popover from "../../Popover/Popover.svelte"
export let id = null export let id = null
export let disabled = false export let disabled = false
@ -33,7 +33,10 @@
export let sort = false export let sort = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let searchTerm = null let searchTerm = null
let button
let popover
$: sortedOptions = getSortedOptions(options, getOptionLabel, sort) $: sortedOptions = getSortedOptions(options, getOptionLabel, sort)
$: filteredOptions = getFilteredOptions( $: filteredOptions = getFilteredOptions(
@ -76,77 +79,117 @@
} }
</script> </script>
<div use:clickOutside={() => (open = false)}> <button
<button {id}
{id} class="spectrum-Picker spectrum-Picker--sizeM"
class="spectrum-Picker spectrum-Picker--sizeM" class:spectrum-Picker--quiet={quiet}
class:spectrum-Picker--quiet={quiet} {disabled}
{disabled} class:is-invalid={!!error}
class:is-invalid={!!error} class:is-open={open}
class:is-open={open} aria-haspopup="listbox"
aria-haspopup="listbox" on:click={onClick}
on:click={onClick} use:clickOutside={() => (open = false)}
> bind:this={button}
{#if fieldIcon} >
<span class="option-extra icon"> {#if fieldIcon}
<Icon size="S" name={fieldIcon} /> <span class="option-extra icon">
</span> <Icon size="S" name={fieldIcon} />
{/if}
{#if fieldColour}
<span class="option-extra">
<StatusLight square color={fieldColour} />
</span>
{/if}
<span
class="spectrum-Picker-label"
class:is-placeholder={isPlaceholder}
class:auto-width={autoWidth}
>
{fieldText}
</span> </span>
{#if error} {/if}
<svg {#if fieldColour}
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon" <span class="option-extra">
focusable="false" <StatusLight square color={fieldColour} />
aria-hidden="true" </span>
aria-label="Folder" {/if}
> <span
<use xlink:href="#spectrum-icon-18-Alert" /> class="spectrum-Picker-label"
</svg> class:is-placeholder={isPlaceholder}
{/if} class:auto-width={autoWidth}
>
{fieldText}
</span>
{#if error}
<svg <svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon" class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
focusable="false" focusable="false"
aria-hidden="true" aria-hidden="true"
aria-label="Folder"
> >
<use xlink:href="#spectrum-css-icon-Chevron100" /> <use xlink:href="#spectrum-icon-18-Alert" />
</svg> </svg>
</button> {/if}
{#if open} <svg
<div class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
transition:fly|local={{ y: -20, duration: 200 }} focusable="false"
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" aria-hidden="true"
class:auto-width={autoWidth} >
> <use xlink:href="#spectrum-css-icon-Chevron100" />
{#if autocomplete} </svg>
<Search </button>
value={searchTerm}
on:change={event => (searchTerm = event.detail)} <Popover
{disabled} anchor={button}
placeholder="Search" align="left"
/> portalTarget={document.documentElement}
bind:this={popover}
{open}
on:close={() => (open = false)}
useAnchorWidth={!autoWidth}
maxWidth={autoWidth ? 400 : null}
>
<div class="popover-content" class:auto-width={autoWidth}>
{#if autocomplete}
<Search
value={searchTerm}
on:change={event => (searchTerm = event.detail)}
{disabled}
placeholder="Search"
/>
{/if}
<ul class="spectrum-Menu" role="listbox">
{#if placeholderOption}
<li
class="spectrum-Menu-item placeholder"
class:is-selected={isPlaceholder}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(null)}
>
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/if} {/if}
<ul class="spectrum-Menu" role="listbox"> {#if filteredOptions.length}
{#if placeholderOption} {#each filteredOptions as option, idx}
<li <li
class="spectrum-Menu-item placeholder" class="spectrum-Menu-item"
class:is-selected={isPlaceholder} class:is-selected={isOptionSelected(getOptionValue(option, idx))}
role="option" role="option"
aria-selected="true" aria-selected="true"
tabindex="0" tabindex="0"
on:click={() => onSelectOption(null)} on:click={() => onSelectOption(getOptionValue(option, idx))}
class:is-disabled={!isOptionEnabled(option)}
> >
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span> {#if getOptionIcon(option, idx)}
<span class="option-extra icon">
<Icon size="S" name={getOptionIcon(option, idx)} />
</span>
{/if}
{#if getOptionColour(option, idx)}
<span class="option-extra">
<StatusLight square color={getOptionColour(option, idx)} />
</span>
{/if}
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
</span>
<svg <svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon" class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false" focusable="false"
@ -155,61 +198,13 @@
<use xlink:href="#spectrum-css-icon-Checkmark100" /> <use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg> </svg>
</li> </li>
{/if} {/each}
{#if filteredOptions.length} {/if}
{#each filteredOptions as option, idx} </ul>
<li </div>
class="spectrum-Menu-item" </Popover>
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(getOptionValue(option, idx))}
class:is-disabled={!isOptionEnabled(option)}
>
{#if getOptionIcon(option, idx)}
<span class="option-extra icon">
<Icon size="S" name={getOptionIcon(option, idx)} />
</span>
{/if}
{#if getOptionColour(option, idx)}
<span class="option-extra">
<StatusLight square color={getOptionColour(option, idx)} />
</span>
{/if}
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
{/if}
</ul>
</div>
{/if}
</div>
<style> <style>
.spectrum-Popover {
max-height: 240px;
z-index: 999;
top: 100%;
}
.spectrum-Popover:not(.auto-width) {
width: 100%;
}
.spectrum-Popover.auto-width :global(.spectrum-Menu-itemLabel) {
max-width: 400px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.spectrum-Picker { .spectrum-Picker {
width: 100%; width: 100%;
box-shadow: none; box-shadow: none;
@ -229,9 +224,6 @@
.spectrum-Picker-label.auto-width.is-placeholder { .spectrum-Picker-label.auto-width.is-placeholder {
padding-right: 2px; padding-right: 2px;
} }
.auto-width .spectrum-Menu-item {
padding-right: var(--spacing-xl);
}
/* Icon and colour alignment */ /* Icon and colour alignment */
.spectrum-Menu-checkmark { .spectrum-Menu-checkmark {
@ -245,26 +237,44 @@
margin: 0 -1px; margin: 0 -1px;
} }
.spectrum-Popover :global(.spectrum-Search) { /* Popover */
.popover-content {
display: contents;
}
.popover-content.auto-width .spectrum-Menu-itemLabel {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.popover-content:not(.auto-width) .spectrum-Menu-itemLabel {
width: 0;
flex: 1 1 auto;
}
.popover-content.auto-width .spectrum-Menu-item {
padding-right: var(--spacing-xl);
}
.spectrum-Menu-item.is-disabled {
pointer-events: none;
}
/* Search styles inside popover */
.popover-content :global(.spectrum-Search) {
margin-top: -1px; margin-top: -1px;
margin-left: -1px; margin-left: -1px;
width: calc(100% + 2px); width: calc(100% + 2px);
} }
.spectrum-Popover :global(.spectrum-Search input) { .popover-content :global(.spectrum-Search input) {
height: auto; height: auto;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
padding-top: var(--spectrum-global-dimension-size-100); padding-top: var(--spectrum-global-dimension-size-100);
padding-bottom: var(--spectrum-global-dimension-size-100); padding-bottom: var(--spectrum-global-dimension-size-100);
} }
.spectrum-Popover :global(.spectrum-Search .spectrum-ClearButton) { .popover-content :global(.spectrum-Search .spectrum-ClearButton) {
right: 1px; right: 1px;
top: 2px; top: 2px;
} }
.spectrum-Popover :global(.spectrum-Search .spectrum-Textfield-icon) { .popover-content :global(.spectrum-Search .spectrum-Textfield-icon) {
top: 9px; top: 9px;
} }
.spectrum-Menu-item.is-disabled {
pointer-events: none;
}
</style> </style>

View File

@ -4,6 +4,7 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import positionDropdown from "../Actions/position_dropdown" import positionDropdown from "../Actions/position_dropdown"
import clickOutside from "../Actions/click_outside" import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -12,9 +13,10 @@
export let portalTarget export let portalTarget
export let dataCy export let dataCy
export let maxWidth export let maxWidth
export let direction = "bottom" export let direction = "bottom"
export let showTip = false export let showTip = false
export let open = false
export let useAnchorWidth = false
let tipSvg = let tipSvg =
'<svg xmlns="http://www.w3.org/svg/2000" width="23" height="12" class="spectrum-Popover-tip" > <path class="spectrum-Popover-tip-triangle" d="M 0.7071067811865476 0 L 11.414213562373096 10.707106781186548 L 22.121320343559645 0" /> </svg>' '<svg xmlns="http://www.w3.org/svg/2000" width="23" height="12" class="spectrum-Popover-tip" > <path class="spectrum-Popover-tip-triangle" d="M 0.7071067811865476 0 L 11.414213562373096 10.707106781186548 L 22.121320343559645 0" /> </svg>'
@ -35,13 +37,22 @@
const handleOutsideClick = e => { const handleOutsideClick = e => {
if (open) { if (open) {
e.stopPropagation() // Stop propagation if the source is the anchor
let node = e.target
let fromAnchor = false
while (!fromAnchor && node && node.parentNode) {
fromAnchor = node === anchor
node = node.parentNode
}
if (fromAnchor) {
e.stopPropagation()
}
// Hide the popover
hide() hide()
} }
} }
let open = null
function handleEscape(e) { function handleEscape(e) {
if (open && e.key === "Escape") { if (open && e.key === "Escape") {
hide() hide()
@ -53,12 +64,13 @@
<Portal target={portalTarget}> <Portal target={portalTarget}>
<div <div
tabindex="0" tabindex="0"
use:positionDropdown={{ anchor, align, maxWidth }} use:positionDropdown={{ anchor, align, maxWidth, useAnchorWidth }}
use:clickOutside={handleOutsideClick} use:clickOutside={handleOutsideClick}
on:keydown={handleEscape} on:keydown={handleEscape}
class={"spectrum-Popover is-open " + (tooltipClasses || "")} class={"spectrum-Popover is-open " + (tooltipClasses || "")}
role="presentation" role="presentation"
data-cy={dataCy} data-cy={dataCy}
transition:fly|local={{ y: -20, duration: 200 }}
> >
{#if showTip} {#if showTip}
{@html tipSvg} {@html tipSvg}

View File

@ -280,6 +280,9 @@
styles[field] += styles[field] +=
"border-right: 1px solid var(--spectrum-global-color-gray-200);" "border-right: 1px solid var(--spectrum-global-color-gray-200);"
} }
if (schema[field].minWidth) {
styles[field] += `min-width: ${schema[field].minWidth};`
}
}) })
return styles return styles
} }
@ -450,6 +453,7 @@
--table-bg: var(--spectrum-global-color-gray-50); --table-bg: var(--spectrum-global-color-gray-50);
--table-border: 1px solid var(--spectrum-alias-border-color-mid); --table-border: 1px solid var(--spectrum-alias-border-color-mid);
--cell-padding: var(--spectrum-global-dimension-size-250); --cell-padding: var(--spectrum-global-dimension-size-250);
overflow: auto;
} }
.wrapper--quiet { .wrapper--quiet {
--table-bg: var(--spectrum-alias-background-color-transparent); --table-bg: var(--spectrum-alias-background-color-transparent);

View File

@ -25,7 +25,6 @@
export let loading = false export let loading = false
export let hideAutocolumns export let hideAutocolumns
export let rowCount export let rowCount
export let type
export let disableSorting = false export let disableSorting = false
export let customPlaceholder = false export let customPlaceholder = false

View File

@ -141,8 +141,4 @@
gap: var(--spacing-s); gap: var(--spacing-s);
max-width: 175px; max-width: 175px;
} }
.lock-status-text {
font-weight: 400;
color: var(--spectrum-global-color-gray-800);
}
</style> </style>

View File

@ -1,6 +1,6 @@
<script> <script>
import { ModalContent } from "@budibase/bbui" import { ModalContent } from "@budibase/bbui"
import { Label, Select } from "@budibase/bbui" import { Select } from "@budibase/bbui"
import { themeStore } from "builderStore" import { themeStore } from "builderStore"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
</script> </script>

View File

@ -1,14 +1,35 @@
<script> <script>
import { Heading, Body, Button, Icon } from "@budibase/bbui" import { Heading, Body, Button, Icon, notifications } from "@budibase/bbui"
import AppLockModal from "../common/AppLockModal.svelte" import AppLockModal from "../common/AppLockModal.svelte"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
import { goto } from "@roxi/routify"
export let app export let app
export let editApp
export let appOverview const handleDefaultClick = () => {
if (window.innerWidth < 640) {
goToOverview()
} else {
goToBuilder()
}
}
const goToBuilder = () => {
if (app.lockedOther) {
notifications.error(
`App locked by ${app.lockedBy.email}. Please allow lock to expire or have them unlock this app.`
)
return
}
$goto(`../../app/${app.devId}`)
}
const goToOverview = () => {
$goto(`../overview/${app.devId}`)
}
</script> </script>
<div class="app-row" on:click={() => editApp(app)}> <div class="app-row" on:click={handleDefaultClick}>
<div class="title" data-cy={`${app.devId}`}> <div class="title" data-cy={`${app.devId}`}>
<div class="app-icon"> <div class="app-icon">
<Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} /> <Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} />
@ -35,21 +56,12 @@
<Body size="S">{app.deployed ? "Published" : "Unpublished"}</Body> <Body size="S">{app.deployed ? "Published" : "Unpublished"}</Body>
</div> </div>
<div data-cy={`row_actions_${app.appId}`}> <div class="app-row-actions" data-cy={`row_actions_${app.appId}`}>
<div class="app-row-actions"> <AppLockModal {app} buttonSize="M" />
<AppLockModal {app} buttonSize="M" /> <Button size="S" secondary on:click={goToOverview}>Manage</Button>
<Button size="S" secondary on:click={() => appOverview(app)}> <Button size="S" primary disabled={app.lockedOther} on:click={goToBuilder}>
Manage Edit
</Button> </Button>
<Button
size="S"
primary
disabled={app.lockedOther}
on:click={() => editApp(app)}
>
Edit
</Button>
</div>
</div> </div>
</div> </div>
@ -139,5 +151,8 @@
.app-row { .app-row {
padding: 20px; padding: 20px;
} }
.app-row-actions {
display: none;
}
} }
</style> </style>

View File

@ -263,6 +263,7 @@
orderMap[component.component]} orderMap[component.component]}
on:click={() => addComponent(component.component)} on:click={() => addComponent(component.component)}
on:mouseover={() => (selectedIndex = null)} on:mouseover={() => (selectedIndex = null)}
on:focus
> >
<Icon name={component.icon} /> <Icon name={component.icon} />
<Body size="XS">{component.name}</Body> <Body size="XS">{component.name}</Body>

View File

@ -20,6 +20,7 @@
class="container" class="container"
on:mouseover={() => (showTooltip = true)} on:mouseover={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)} on:mouseleave={() => (showTooltip = false)}
on:focus
style="--color: {color};" style="--color: {color};"
> >
<StatusLight square {color} /> <StatusLight square {color} />

View File

@ -8,12 +8,11 @@
Detail, Detail,
Link, Link,
TooltipWrapper, TooltipWrapper,
Page,
} from "@budibase/bbui" } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import { admin, auth, licensing } from "../../../../stores/portal" import { admin, auth, licensing } from "stores/portal"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
import { DashCard, Usage } from "../../../../components/usage" import { DashCard, Usage } from "components/usage"
let staticUsage = [] let staticUsage = []
let monthlyUsage = [] let monthlyUsage = []

View File

@ -4,7 +4,8 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
let loaded = false // Don't block loading if we've already hydrated state
let loaded = $apps.length > 0
onMount(async () => { onMount(async () => {
try { try {

View File

@ -77,31 +77,3 @@
<CreateAppModal {template} /> <CreateAppModal {template} />
</Modal> </Modal>
<AppLimitModal bind:this={appLimitModal} /> <AppLimitModal bind:this={appLimitModal} />
<style>
.title .welcome > .buttons {
padding-top: 30px;
}
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--spacing-xl);
flex-wrap: wrap;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xl);
flex-wrap: wrap;
}
@media (max-width: 640px) {
.buttons {
flex-direction: row-reverse;
justify-content: flex-end;
}
}
</style>

View File

@ -178,20 +178,6 @@
creatingApp = false creatingApp = false
} }
const appOverview = app => {
$goto(`../overview/${app.devId}`)
}
const editApp = app => {
if (app.lockedOther) {
notifications.error(
`App locked by ${app.lockedBy.email}. Please allow lock to expire or have them unlock this app.`
)
return
}
$goto(`../../app/${app.devId}`)
}
function createAppFromTemplateUrl(templateKey) { function createAppFromTemplateUrl(templateKey) {
// validate the template key just to make sure // validate the template key just to make sure
const templateParts = templateKey.split("/") const templateParts = templateKey.split("/")
@ -309,7 +295,7 @@
<div class="app-table"> <div class="app-table">
{#each filteredApps as app (app.appId)} {#each filteredApps as app (app.appId)}
<AppRow {app} {editApp} {appOverview} /> <AppRow {app} />
{/each} {/each}
</div> </div>
</Layout> </Layout>
@ -399,7 +385,7 @@
display: none; display: none;
} }
.app-actions > :global(*) { .app-actions > :global(*) {
flex: 0 0 50%; flex: 1 1 auto;
} }
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<script> <script>
import { url, isActive, params, goto } from "@roxi/routify" import { url, isActive, goto } from "@roxi/routify"
import { import {
Page, Page,
Layout, Layout,
@ -20,7 +20,7 @@
Breadcrumb, Breadcrumb,
Header, Header,
} from "components/portal/page" } from "components/portal/page"
import { apps, auth, groups, overview } from "stores/portal" import { apps, auth, overview } from "stores/portal"
import { AppStatus } from "constants" import { AppStatus } from "constants"
import analytics, { Events, EventSource } from "analytics" import analytics, { Events, EventSource } from "analytics"
import { store } from "builderStore" import { store } from "builderStore"
@ -29,7 +29,7 @@
import { API } from "api" import { API } from "api"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import ExportAppModal from "components/start/ExportAppModal.svelte" import ExportAppModal from "components/start/ExportAppModal.svelte"
import { syncURLToState } from "../../../../../helpers/urlStateSync" import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify" import * as routify from "@roxi/routify"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"

View File

@ -36,7 +36,7 @@
}, },
role: { role: {
displayName: "Access", displayName: "Access",
width: "150px", width: "160px",
borderLeft: true, borderLeft: true,
}, },
} }

View File

@ -27,7 +27,6 @@
let pageInfo = createPaginationStore() let pageInfo = createPaginationStore()
let runHistory = null let runHistory = null
let showPanel = false
let selectedHistory = null let selectedHistory = null
let automationOptions = [] let automationOptions = []
let automationId = null let automationId = null
@ -155,47 +154,47 @@
</Layout> </Layout>
<Divider /> <Divider />
<div class="search"> <div class="controls">
<div class="select"> <div class="search">
<Select <div class="select">
placeholder="All" <Select
label="Status" placeholder="All"
bind:value={status} label="Status"
options={statusOptions} bind:value={status}
/> options={statusOptions}
</div> />
<div class="select"> </div>
<Select <div class="select">
placeholder="All" <Select
label="Automation" placeholder="All"
bind:value={automationId} label="Automation"
options={automationOptions} bind:value={automationId}
/> options={automationOptions}
</div> />
<div class="select"> </div>
<Select <div class="select">
placeholder="All" <Select
label="Date range" placeholder="All"
bind:value={timeRange} label="Date range"
options={timeOptions} bind:value={timeRange}
isOptionEnabled={x => { options={timeOptions}
if (licensePlan?.type === Constants.PlanType.FREE) { isOptionEnabled={x => {
return ["1-w", "30-d", "90-d"].indexOf(x.value) < 0 if (licensePlan?.type === Constants.PlanType.FREE) {
} else if (licensePlan?.type === Constants.PlanType.TEAM) { return ["1-w", "30-d", "90-d"].indexOf(x.value) < 0
return ["90-d"].indexOf(x.value) < 0 } else if (licensePlan?.type === Constants.PlanType.TEAM) {
} else if (licensePlan?.type === Constants.PlanType.PRO) { return ["90-d"].indexOf(x.value) < 0
return ["30-d", "90-d"].indexOf(x.value) < 0 } else if (licensePlan?.type === Constants.PlanType.PRO) {
} return ["30-d", "90-d"].indexOf(x.value) < 0
return true }
}} return true
/> }}
/>
</div>
</div> </div>
{#if (licensePlan?.type !== Constants.PlanType.ENTERPRISE && $auth.user.accountPortalAccess) || !$admin.cloud} {#if (licensePlan?.type !== Constants.PlanType.ENTERPRISE && $auth.user.accountPortalAccess) || !$admin.cloud}
<div class="pro-upgrade"> <Button secondary on:click={$licensing.goToUpgradePage()}>
<Button secondary on:click={$licensing.goToUpgradePage()}> Get more history
Get more history </Button>
</Button>
</div>
{/if} {/if}
</div> </div>
@ -236,14 +235,24 @@
{/if} {/if}
<style> <style>
.controls {
display: flex;
flex-direction: row;
gap: var(--spacing-xl);
align-items: flex-end;
flex-wrap: wrap;
}
.search { .search {
display: flex; display: flex;
gap: var(--spacing-xl); gap: var(--spacing-xl);
width: 100%; align-items: flex-start;
align-items: flex-end; flex: 1 0 auto;
max-width: 100%;
} }
.select { .select {
flex-basis: 150px; flex: 1 1 0;
max-width: 150px;
min-width: 80px;
} }
.pagination { .pagination {
display: flex; display: flex;
@ -251,10 +260,4 @@
justify-content: flex-end; justify-content: flex-end;
margin-top: var(--spacing-xl); margin-top: var(--spacing-xl);
} }
.pro-upgrade {
display: flex;
align-items: center;
justify-content: flex-end;
flex: 1;
}
</style> </style>

View File

@ -14,7 +14,6 @@
Tags, Tags,
Tag, Tag,
Table, Table,
Page,
} from "@budibase/bbui" } from "@budibase/bbui"
import { backups, licensing, auth, admin, overview } from "stores/portal" import { backups, licensing, auth, admin, overview } from "stores/portal"
import { createPaginationStore } from "helpers/pagination" import { createPaginationStore } from "helpers/pagination"
@ -223,29 +222,30 @@
</div> </div>
{:else if loaded} {:else if loaded}
<Layout noPadding gap="M" alignContent="start"> <Layout noPadding gap="M" alignContent="start">
<div class="search"> <div class="controls">
<div class="select"> <div class="search">
<Select <div class="select">
placeholder="All" <Select
label="Type" placeholder="All"
options={filters} label="Type"
getOptionValue={filter => filter.value} options={filters}
getOptionLabel={filter => filter.label} getOptionValue={filter => filter.value}
bind:value={filterOpt} getOptionLabel={filter => filter.label}
bind:value={filterOpt}
/>
</div>
<DatePicker
range={true}
label="Date Range"
on:change={e => {
if (e.detail[0].length > 1) {
startDate = e.detail[0][0].toISOString()
endDate = e.detail[0][1].toISOString()
}
}}
/> />
</div> </div>
<DatePicker <div>
range={true}
label="Date Range"
on:change={e => {
if (e.detail[0].length > 1) {
startDate = e.detail[0][0].toISOString()
endDate = e.detail[0][1].toISOString()
}
}}
/>
<div class="split-buttons">
<ActionButton on:click={modal.show} icon="SaveAsFloppy"> <ActionButton on:click={modal.show} icon="SaveAsFloppy">
Create new backup Create new backup
</ActionButton> </ActionButton>
@ -291,15 +291,29 @@
gap: var(--spacing-xl); gap: var(--spacing-xl);
} }
.controls {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-end;
gap: var(--spacing-xl);
flex-wrap: wrap;
}
.search { .search {
flex: 1 1 auto;
display: flex; display: flex;
gap: var(--spacing-xl); gap: var(--spacing-xl);
width: 100%;
align-items: flex-end; align-items: flex-end;
} }
.search :global(.spectrum-InputGroup) {
min-width: 100px;
}
.select { .select {
flex-basis: 160px; flex-basis: 160px;
width: 0;
min-width: 100px;
} }
.pagination { .pagination {
@ -309,13 +323,6 @@
margin-top: var(--spacing-xl); margin-top: var(--spacing-xl);
} }
.split-buttons {
display: flex;
align-items: center;
justify-content: flex-end;
flex: 1;
}
.title { .title {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -16,15 +16,12 @@
import clientPackage from "@budibase/client/package.json" import clientPackage from "@budibase/client/package.json"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
import { users, auth, apps, groups, overview } from "stores/portal" import { users, auth, apps, groups, overview } from "stores/portal"
import { createEventDispatcher } from "svelte"
import { fetchData } from "@budibase/frontend-core" import { fetchData } from "@budibase/frontend-core"
import { API } from "api" import { API } from "api"
import GroupIcon from "../../users/groups/_components/GroupIcon.svelte" import GroupIcon from "../../users/groups/_components/GroupIcon.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { checkIncomingDeploymentStatus } from "components/deploy/utils" import { checkIncomingDeploymentStatus } from "components/deploy/utils"
const dispatch = createEventDispatcher()
let appEditor let appEditor
let unpublishModal let unpublishModal
let deployments let deployments
@ -180,7 +177,7 @@
- -
<Link <Link
on:click={() => { on:click={() => {
$goto("../version") $goto("./version")
}} }}
> >
Update Update

View File

@ -20,6 +20,7 @@
const schema = { const schema = {
name: { name: {
width: "2fr", width: "2fr",
minWidth: "200px",
}, },
version: { version: {
width: "1fr", width: "1fr",
@ -28,6 +29,7 @@
width: "1fr", width: "1fr",
displayName: "Type", displayName: "Type",
capitalise: true, capitalise: true,
minWidth: "120px",
}, },
edit: { edit: {
width: "auto", width: "auto",
@ -119,8 +121,19 @@
display: flex; display: flex;
gap: var(--spacing-xl); gap: var(--spacing-xl);
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
} }
.controls :global(.spectrum-Search) { .controls :global(.spectrum-Search) {
width: 200px; width: 200px;
} }
@media (max-width: 640px) {
.filters {
display: grid;
grid-template-columns: 1fr 1fr;
}
.controls :global(.spectrum-Search) {
width: auto;
}
}
</style> </style>

View File

@ -2,16 +2,14 @@
import { onMount, tick } from "svelte" import { onMount, tick } from "svelte"
import { import {
Button, Button,
Detail,
Heading, Heading,
ActionButton,
Body, Body,
Layout, Layout,
notifications, notifications,
Tabs, Tabs,
Tab, Tab,
} from "@budibase/bbui" } from "@budibase/bbui"
import { goto, url } from "@roxi/routify" import { url } from "@roxi/routify"
import { email } from "stores/portal" import { email } from "stores/portal"
import Editor from "components/integration/QueryEditor.svelte" import Editor from "components/integration/QueryEditor.svelte"
import TemplateBindings from "./_components/TemplateBindings.svelte" import TemplateBindings from "./_components/TemplateBindings.svelte"

View File

@ -110,12 +110,6 @@
} }
} }
const getRoleLabel = appId => {
const roleId = group?.roles?.[apps.getProdAppID(appId)]
const role = $roles.find(x => x._id === roleId)
return role?.name || "Custom role"
}
async function deleteGroup() { async function deleteGroup() {
try { try {
await groups.actions.delete(group) await groups.actions.delete(group)

View File

@ -40,7 +40,7 @@
] ]
$: schema = { $: schema = {
name: { displayName: "Group", width: "2fr" }, name: { displayName: "Group", width: "2fr", minWidth: "200px" },
users: { sortable: false, width: "1fr" }, users: { sortable: false, width: "1fr" },
roles: { sortable: false, displayName: "Apps", width: "1fr" }, roles: { sortable: false, displayName: "Apps", width: "1fr" },
} }

View File

@ -8,8 +8,6 @@
Heading, Heading,
Body, Body,
Label, Label,
List,
ListItem,
Icon, Icon,
Input, Input,
MenuItem, MenuItem,

View File

@ -57,6 +57,7 @@
email: { email: {
sortable: false, sortable: false,
width: "2fr", width: "2fr",
minWidth: "200px",
}, },
role: { role: {
sortable: false, sortable: false,
@ -296,6 +297,8 @@
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
flex-wrap: wrap;
gap: var(--spacing-xl);
} }
.controls-right { .controls-right {

View File

@ -2,7 +2,6 @@
export let title = "" export let title = ""
export let favicon = "" export let favicon = ""
export let metaImage = "" export let metaImage = ""
export let url = ""
export let clientLibPath export let clientLibPath
export let usedPlugins export let usedPlugins