Merge pull request #11176 from Budibase/cheeks-fixes

Grid + collab + tooltip improvements + preview in new tab
This commit is contained in:
Andrew Kingston 2023-07-17 10:17:31 +01:00 committed by GitHub
commit 0ca1850e4a
29 changed files with 484 additions and 286 deletions

View File

@ -1,6 +1,7 @@
<script> <script>
import "@spectrum-css/button/dist/index-vars.css" import "@spectrum-css/button/dist/index-vars.css"
import Tooltip from "../Tooltip/Tooltip.svelte" import AbsTooltip from "../Tooltip/AbsTooltip.svelte"
import { createEventDispatcher } from "svelte"
export let type export let type
export let disabled = false export let disabled = false
@ -16,48 +17,53 @@
export let tooltip = undefined export let tooltip = undefined
export let newStyles = true export let newStyles = true
export let id export let id
const dispatch = createEventDispatcher()
</script> </script>
<button <AbsTooltip text={tooltip}>
{id} <button
{type} {id}
class:spectrum-Button--cta={cta} {type}
class:spectrum-Button--primary={primary} class:spectrum-Button--cta={cta}
class:spectrum-Button--secondary={secondary} class:spectrum-Button--primary={primary}
class:spectrum-Button--warning={warning} class:spectrum-Button--secondary={secondary}
class:spectrum-Button--overBackground={overBackground} class:spectrum-Button--warning={warning}
class:spectrum-Button--quiet={quiet} class:spectrum-Button--overBackground={overBackground}
class:new-styles={newStyles} class:spectrum-Button--quiet={quiet}
class:active class:new-styles={newStyles}
class:disabled class:active
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}" class:is-disabled={disabled}
{disabled} class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
on:click|preventDefault on:click|preventDefault={() => {
> if (!disabled) {
{#if icon} dispatch("click")
<svg }
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}" }}
focusable="false" >
aria-hidden="true" {#if icon}
aria-label={icon} <svg
> class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
<use xlink:href="#spectrum-icon-18-{icon}" /> focusable="false"
</svg> aria-hidden="true"
{/if} aria-label={icon}
{#if $$slots} >
<span class="spectrum-Button-label"><slot /></span> <use xlink:href="#spectrum-icon-18-{icon}" />
{/if} </svg>
{#if tooltip} {/if}
<div class="tooltip"> {#if $$slots}
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} /> <span class="spectrum-Button-label"><slot /></span>
</div> {/if}
{/if} </button>
</button> </AbsTooltip>
<style> <style>
button { button {
position: relative; position: relative;
} }
button.is-disabled {
cursor: default;
}
.spectrum-Button-label { .spectrum-Button-label {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -66,23 +72,6 @@
.active { .active {
color: var(--spectrum-global-color-blue-600) !important; color: var(--spectrum-global-color-blue-600) !important;
} }
.tooltip {
position: absolute;
display: flex;
justify-content: center;
z-index: 100;
width: 160px;
text-align: center;
transform: translateX(-50%);
left: 50%;
top: 100%;
opacity: 0;
transition: opacity 130ms ease-out;
pointer-events: none;
}
button:hover .tooltip {
opacity: 1;
}
.spectrum-Button--primary.new-styles { .spectrum-Button--primary.new-styles {
background: var(--spectrum-global-color-gray-800); background: var(--spectrum-global-color-gray-800);
border-color: transparent; border-color: transparent;
@ -96,10 +85,10 @@
border-color: transparent; border-color: transparent;
color: var(--spectrum-global-color-gray-900); color: var(--spectrum-global-color-gray-900);
} }
.spectrum-Button--secondary.new-styles:not(.disabled):hover { .spectrum-Button--secondary.new-styles:not(.is-disabled):hover {
background: var(--spectrum-global-color-gray-300); background: var(--spectrum-global-color-gray-300);
} }
.spectrum-Button--secondary.new-styles.disabled { .spectrum-Button--secondary.new-styles.is-disabled {
color: var(--spectrum-global-color-gray-500); color: var(--spectrum-global-color-gray-500);
} }
</style> </style>

View File

@ -90,6 +90,6 @@
.spectrum-Popover { .spectrum-Popover {
min-width: var(--spectrum-global-dimension-size-2000); min-width: var(--spectrum-global-dimension-size-2000);
border-color: var(--spectrum-global-color-gray-300); border-color: var(--spectrum-global-color-gray-300);
overflow: visible; overflow: auto;
} }
</style> </style>

View File

@ -0,0 +1,157 @@
<script context="module">
export const TooltipPosition = {
Top: "top",
Right: "right",
Bottom: "bottom",
Left: "left",
}
export const TooltipType = {
Default: "default",
Info: "info",
Positive: "positive",
Negative: "negative",
}
</script>
<script>
import Portal from "svelte-portal"
import { fade } from "svelte/transition"
import "@spectrum-css/tooltip/dist/index-vars.css"
import { onDestroy } from "svelte"
export let position = TooltipPosition.Top
export let type = TooltipType.Default
export let text = ""
export let fixed = false
export let color = null
let wrapper
let hovered = false
let left
let top
let visible = false
let timeout
let interval
$: {
if (hovered || fixed) {
// Debounce showing by 200ms to avoid flashing tooltip
timeout = setTimeout(show, 200)
} else {
hide()
}
}
$: tooltipStyle = color ? `background:${color};` : null
$: tipStyle = color ? `border-top-color:${color};` : null
// Computes the position of the tooltip
const updateTooltipPosition = () => {
const node = wrapper?.children?.[0]
if (!node) {
left = null
top = null
return
}
const bounds = node.getBoundingClientRect()
// Determine where to render tooltip based on position prop
if (position === TooltipPosition.Top) {
left = bounds.left + bounds.width / 2
top = bounds.top
} else if (position === TooltipPosition.Right) {
left = bounds.left + bounds.width
top = bounds.top + bounds.height / 2
} else if (position === TooltipPosition.Bottom) {
left = bounds.left + bounds.width / 2
top = bounds.top + bounds.height
} else if (position === TooltipPosition.Left) {
left = bounds.left
top = bounds.top + bounds.height / 2
}
}
// Computes the position of the tooltip then shows it.
// We set up a poll to frequently update the position of the tooltip in case
// the target moves.
const show = () => {
updateTooltipPosition()
interval = setInterval(updateTooltipPosition, 100)
visible = true
}
// Hides the tooltip
const hide = () => {
clearTimeout(timeout)
clearInterval(interval)
visible = false
}
// Ensure we clean up interval and timeout
onDestroy(hide)
</script>
<div
bind:this={wrapper}
class="abs-tooltip"
on:focus={null}
on:mouseover={() => (hovered = true)}
on:mouseleave={() => (hovered = false)}
>
<slot />
</div>
{#if visible && text && left != null && top != null}
<Portal target=".spectrum">
<span
class="spectrum-Tooltip spectrum-Tooltip--{type} spectrum-Tooltip--{position} is-open"
style={`left:${left}px;top:${top}px;${tooltipStyle}`}
transition:fade|local={{ duration: 130 }}
>
<span class="spectrum-Tooltip-label">{text}</span>
<span class="spectrum-Tooltip-tip" style={tipStyle} />
</span>
</Portal>
{/if}
<style>
.abs-tooltip {
display: contents;
}
.spectrum-Tooltip {
position: absolute;
z-index: 9999;
pointer-events: none;
margin: 0;
max-width: 280px;
transition: top 130ms ease-out, left 130ms ease-out;
}
.spectrum-Tooltip-label {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
font-size: 12px;
font-weight: 600;
}
/* Colour overrides for default type */
.spectrum-Tooltip--default {
background: var(--spectrum-global-color-gray-500);
}
.spectrum-Tooltip--default .spectrum-Tooltip-tip {
border-top-color: var(--spectrum-global-color-gray-500);
}
/* Position styles */
.spectrum-Tooltip--top {
transform: translateX(-50%) translateY(calc(-100% - 8px));
}
.spectrum-Tooltip--right {
transform: translateX(8px) translateY(-50%);
}
.spectrum-Tooltip--bottom {
transform: translateX(-50%) translateY(8px);
}
.spectrum-Tooltip--left {
transform: translateX(calc(-100% - 8px)) translateY(-50%);
}
</style>

View File

@ -0,0 +1,39 @@
<script>
import AbsTooltip from "./AbsTooltip.svelte"
import { onDestroy } from "svelte"
export let text = null
export let condition = true
export let duration = 3000
export let position
export let type
let visible = false
let timeout
$: {
if (condition) {
showTooltip()
} else {
hideTooltip()
}
}
const showTooltip = () => {
visible = true
timeout = setTimeout(() => {
visible = false
}, duration)
}
const hideTooltip = () => {
visible = false
clearTimeout(timeout)
}
onDestroy(hideTooltip)
</script>
<AbsTooltip {position} {type} text={visible ? text : null} fixed={visible}>
<slot />
</AbsTooltip>

View File

@ -36,6 +36,12 @@ export { default as Layout } from "./Layout/Layout.svelte"
export { default as Page } from "./Layout/Page.svelte" export { default as Page } from "./Layout/Page.svelte"
export { default as Link } from "./Link/Link.svelte" export { default as Link } from "./Link/Link.svelte"
export { default as Tooltip } from "./Tooltip/Tooltip.svelte" export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
export { default as TempTooltip } from "./Tooltip/TempTooltip.svelte"
export {
default as AbsTooltip,
TooltipPosition,
TooltipType,
} from "./Tooltip/AbsTooltip.svelte"
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte" export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
export { default as Menu } from "./Menu/Menu.svelte" export { default as Menu } from "./Menu/Menu.svelte"
export { default as MenuSection } from "./Menu/Section.svelte" export { default as MenuSection } from "./Menu/Section.svelte"

View File

@ -127,8 +127,12 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
export const userSelectedResourceMap = derived(userStore, $userStore => { export const userSelectedResourceMap = derived(userStore, $userStore => {
let map = {} let map = {}
$userStore.forEach(user => { $userStore.forEach(user => {
if (user.builderMetadata?.selectedResourceId) { const resource = user.builderMetadata?.selectedResourceId
map[user.builderMetadata?.selectedResourceId] = user if (resource) {
if (!map[resource]) {
map[resource] = []
}
map[resource].push(user)
} }
}) })
return map return map

View File

@ -2,6 +2,7 @@
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { createEventDispatcher, getContext } from "svelte" import { createEventDispatcher, getContext } from "svelte"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
import UserAvatars from "../../pages/builder/app/[application]/_components/UserAvatars.svelte"
export let icon export let icon
export let withArrow = false export let withArrow = false
@ -98,21 +99,25 @@
<Icon color={iconColor} size="S" name={icon} /> <Icon color={iconColor} size="S" name={icon} />
</div> </div>
{/if} {/if}
<div class="text" title={showTooltip ? text : null}>{text}</div> <div class="text" title={showTooltip ? text : null}>
{text}
{#if selectedBy}
<UserAvatars size="XS" users={selectedBy} />
{/if}
</div>
{#if withActions} {#if withActions}
<div class="actions"> <div class="actions">
<slot /> <slot />
</div> </div>
{/if} {/if}
{#if $$slots.right} {#if $$slots.right}
<div class="right"> <div class="right">
<slot name="right" /> <slot name="right" />
</div> </div>
{/if} {/if}
</div> </div>
{#if selectedBy}
<div class="selected-by-label">{helpers.getUserLabel(selectedBy)}</div>
{/if}
</div> </div>
<style> <style>
@ -136,13 +141,16 @@
} }
.nav-item.highlighted { .nav-item.highlighted {
background-color: var(--spectrum-global-color-gray-200); background-color: var(--spectrum-global-color-gray-200);
--avatars-background: var(--spectrum-global-color-gray-200);
} }
.nav-item.selected { .nav-item.selected {
background-color: var(--spectrum-global-color-gray-300); background-color: var(--spectrum-global-color-gray-300);
--avatars-background: var(--spectrum-global-color-gray-300);
color: var(--ink); color: var(--ink);
} }
.nav-item:hover { .nav-item:hover {
background-color: var(--spectrum-global-color-gray-300); background-color: var(--spectrum-global-color-gray-300);
--avatars-background: var(--spectrum-global-color-gray-300);
} }
.nav-item:hover .actions { .nav-item:hover .actions {
visibility: visible; visibility: visible;
@ -159,37 +167,6 @@
padding-left: var(--spacing-l); padding-left: var(--spacing-l);
} }
/* Selected user styles */
.nav-item.selectedBy:after {
content: "";
position: absolute;
width: calc(100% - 4px);
height: 28px;
border: 2px solid var(--selected-by-color);
left: 0;
top: 0;
border-radius: 2px;
pointer-events: none;
}
.selected-by-label {
position: absolute;
top: 0;
right: 0;
background: var(--selected-by-color);
padding: 2px 4px;
font-size: 12px;
color: white;
transform: translateY(calc(1px - 100%));
border-top-right-radius: 2px;
border-top-left-radius: 2px;
pointer-events: none;
opacity: 0;
transition: opacity 130ms ease-out;
}
.nav-item.selectedBy:hover .selected-by-label {
opacity: 1;
}
/* Needed to fully display the actions icon */ /* Needed to fully display the actions icon */
.nav-item.scrollable .nav-item-content { .nav-item.scrollable .nav-item-content {
padding-right: 1px; padding-right: 1px;
@ -245,6 +222,9 @@
color: var(--spectrum-global-color-gray-900); color: var(--spectrum-global-color-gray-900);
order: 2; order: 2;
width: 0; width: 0;
display: flex;
align-items: center;
gap: 8px;
} }
.scrollable .text { .scrollable .text {
flex: 0 0 auto; flex: 0 0 auto;

View File

@ -10,6 +10,7 @@
Link, Link,
Modal, Modal,
StatusLight, StatusLight,
AbsTooltip,
} from "@budibase/bbui" } from "@budibase/bbui"
import RevertModal from "components/deploy/RevertModal.svelte" import RevertModal from "components/deploy/RevertModal.svelte"
import VersionModal from "components/deploy/VersionModal.svelte" import VersionModal from "components/deploy/VersionModal.svelte"
@ -250,15 +251,20 @@
<Link quiet on:click={unpublishApp}>Unpublish</Link> <Link quiet on:click={unpublishApp}>Unpublish</Link>
</span> </span>
<span class="revert-link"> <span class="revert-link">
<Link <AbsTooltip
disabled={!$isOnlyUser} text={$isOnlyUser
quiet ? null
secondary : "Unavailable - another user is editing this app"}
on:click={revertApp}
tooltip="Unavailable - another user is editing this app"
> >
Revert <Link
</Link> disabled={!$isOnlyUser}
quiet
secondary
on:click={revertApp}
>
Revert
</Link>
</AbsTooltip>
</span> </span>
{:else} {:else}
<span class="status-text unpublished">Not published</span> <span class="status-text unpublished">Not published</span>

View File

@ -1,11 +1,8 @@
<script> <script>
import { Tooltip } from "@budibase/bbui"
export let text export let text
export let url export let url
export let active = false export let active = false
export let disabled = false export let disabled = false
export let tooltip = null
</script> </script>
<div class="side-nav-item"> <div class="side-nav-item">
@ -18,11 +15,6 @@
{text || ""} {text || ""}
</div> </div>
{/if} {/if}
{#if tooltip}
<div class="tooltip">
<Tooltip textWrapping direction="right" text={tooltip} />
</div>
{/if}
</div> </div>
<style> <style>
@ -45,17 +37,4 @@
pointer-events: none; pointer-events: none;
color: var(--spectrum-global-color-gray-500) !important; color: var(--spectrum-global-color-gray-500) !important;
} }
.tooltip {
position: absolute;
transform: translateY(-50%);
left: 100%;
top: 50%;
opacity: 0;
pointer-events: none;
transition: opacity 130ms ease-out;
z-index: 100;
}
.side-nav-item:hover .tooltip {
opacity: 1;
}
</style> </style>

View File

@ -15,6 +15,7 @@
} }
onMount(() => { onMount(() => {
window.isBuilder = true
window.closePreview = () => { window.closePreview = () => {
store.update(state => ({ store.update(state => ({
...state, ...state,

View File

@ -1,9 +1,14 @@
<script> <script>
import { UserAvatar } from "@budibase/frontend-core" import { UserAvatar } from "@budibase/frontend-core"
import { TooltipPosition, Avatar } from "@budibase/bbui"
export let users = [] export let users = []
export let order = "ltr"
export let size = "S"
export let tooltipPosition = TooltipPosition.Top
$: uniqueUsers = unique(users) $: uniqueUsers = unique(users, order)
$: avatars = getAvatars(uniqueUsers, order)
const unique = users => { const unique = users => {
let uniqueUsers = {} let uniqueUsers = {}
@ -12,17 +17,51 @@
}) })
return Object.values(uniqueUsers) return Object.values(uniqueUsers)
} }
const getAvatars = (users, order) => {
const avatars = users.slice(0, 3)
if (users.length > 3) {
const overflow = {
_id: "overflow",
label: `+${users.length - 3}`,
}
if (order === "ltr") {
avatars.push(overflow)
} else {
avatars.unshift(overflow)
}
}
return avatars.map((user, idx) => ({
...user,
zIndex: order === "ltr" ? idx : uniqueUsers.length - idx,
}))
}
</script> </script>
<div class="avatars"> <div class="avatars">
{#each uniqueUsers as user} {#each avatars as user}
<UserAvatar {user} tooltipDirection="bottom" /> <span style="z-index:{user.zIndex};">
{#if user._id === "overflow"}
<Avatar
{size}
initials={user.label}
color="var(--spectrum-global-color-gray-500)"
/>
{:else}
<UserAvatar {size} {user} {tooltipPosition} />
{/if}
</span>
{/each} {/each}
</div> </div>
<style> <style>
.avatars { .avatars {
display: flex; display: flex;
gap: 4px; }
span:not(:first-of-type) {
margin-left: -6px;
}
.avatars :global(.spectrum-Avatar) {
border: 2px solid var(--avatars-background, var(--background));
} }
</style> </style>

View File

@ -15,6 +15,7 @@
Heading, Heading,
Modal, Modal,
notifications, notifications,
TooltipPosition,
} from "@budibase/bbui" } from "@budibase/bbui"
import AppActions from "components/deploy/AppActions.svelte" import AppActions from "components/deploy/AppActions.svelte"
import { API } from "api" import { API } from "api"
@ -172,7 +173,11 @@
</div> </div>
<div class="toprightnav"> <div class="toprightnav">
<span> <span>
<UserAvatars users={$userStore} /> <UserAvatars
users={$userStore}
order="rtl"
tooltipPosition={TooltipPosition.Bottom}
/>
</span> </span>
<AppActions {application} {loaded} /> <AppActions {application} {loaded} />
</div> </div>

View File

@ -7,7 +7,7 @@
} from "stores/backend" } from "stores/backend"
import { hasData } from "stores/selectors" import { hasData } from "stores/selectors"
import { notifications, Body } from "@budibase/bbui" import { notifications, Body, Icon, AbsTooltip } from "@budibase/bbui"
import { params, goto } from "@roxi/routify" import { params, goto } from "@roxi/routify"
import CreateExternalDatasourceModal from "./_components/CreateExternalDatasourceModal/index.svelte" import CreateExternalDatasourceModal from "./_components/CreateExternalDatasourceModal/index.svelte"
import CreateInternalTableModal from "./_components/CreateInternalTableModal.svelte" import CreateInternalTableModal from "./_components/CreateInternalTableModal.svelte"
@ -15,7 +15,6 @@
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte" import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
import CreationPage from "components/common/CreationPage.svelte" import CreationPage from "components/common/CreationPage.svelte"
import ICONS from "components/backend/DatasourceNavigator/icons/index.js" import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
let internalTableModal let internalTableModal
let externalDatasourceModal let externalDatasourceModal
@ -54,13 +53,9 @@
> >
<div class="subHeading"> <div class="subHeading">
<Body>Get started with our Budibase DB</Body> <Body>Get started with our Budibase DB</Body>
<div <AbsTooltip text="Budibase DB is built with CouchDB">
role="tooltip" <Icon name="Info" size="S" />
title="Budibase DB is built with CouchDB" </AbsTooltip>
class="tooltip"
>
<FontAwesomeIcon name="fa-solid fa-circle-info" />
</div>
</div> </div>
<div class="options"> <div class="options">
@ -116,13 +111,12 @@
display: flex; display: flex;
align-items: center; align-items: center;
margin-top: 12px; margin-top: 12px;
margin-bottom: 24px; margin-bottom: 36px;
gap: 8px;
} }
.subHeading :global(p) {
.tooltip { color: var(--spectrum-global-color-gray-600) !important;
margin-left: 6px;
} }
.options { .options {
width: 100%; width: 100%;
display: grid; display: grid;

View File

@ -19,7 +19,7 @@
heading={hasScreens ? "Create new screen" : "Create your first screen"} heading={hasScreens ? "Create new screen" : "Create your first screen"}
> >
<div class="subHeading"> <div class="subHeading">
<Body size="L">Start from scratch or create screens from your data</Body> <Body>Start from scratch or create screens from your data</Body>
</div> </div>
<div class="cards"> <div class="cards">
@ -56,18 +56,18 @@
.subHeading :global(p) { .subHeading :global(p) {
text-align: center; text-align: center;
margin-top: 12px; margin-top: 12px;
margin-bottom: 24px; margin-bottom: 36px;
color: var(--grey-6); color: var(--spectrum-global-color-gray-600);
} }
.cards { .cards {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 24px;
} }
.card { .card {
margin: 12px;
max-width: 235px; max-width: 235px;
transition: filter 150ms; transition: filter 150ms;
} }

View File

@ -1,6 +1,6 @@
<script> <script>
import { Content, SideNav, SideNavItem } from "components/portal/page" import { Content, SideNav, SideNavItem } from "components/portal/page"
import { Page, Layout } from "@budibase/bbui" import { Page, Layout, AbsTooltip, TooltipPosition } from "@budibase/bbui"
import { url, isActive } from "@roxi/routify" import { url, isActive } from "@roxi/routify"
import DeleteModal from "components/deploy/DeleteModal.svelte" import DeleteModal from "components/deploy/DeleteModal.svelte"
import { isOnlyUser } from "builderStore" import { isOnlyUser } from "builderStore"
@ -45,16 +45,20 @@
active={$isActive("./version")} active={$isActive("./version")}
/> />
<div class="delete-action"> <div class="delete-action">
<SideNavItem <AbsTooltip
text="Delete app" position={TooltipPosition.Bottom}
on:click={() => { text={$isOnlyUser
deleteModal.show()
}}
disabled={!$isOnlyUser}
tooltip={$isOnlyUser
? null ? null
: "Unavailable - another user is editing this app"} : "Unavailable - another user is editing this app"}
/> >
<SideNavItem
text="Delete app"
disabled={!$isOnlyUser}
on:click={() => {
deleteModal.show()
}}
/>
</AbsTooltip>
</div> </div>
</SideNav> </SideNav>
<slot /> <slot />

View File

@ -6,6 +6,8 @@
Heading, Heading,
Body, Body,
Modal, Modal,
AbsTooltip,
TooltipPosition,
} from "@budibase/bbui" } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import CreateRestoreModal from "./CreateRestoreModal.svelte" import CreateRestoreModal from "./CreateRestoreModal.svelte"
@ -46,16 +48,18 @@
</div> </div>
{#if row.type !== "restore"} {#if row.type !== "restore"}
<MenuItem <AbsTooltip
on:click={restoreDialog.show} position={TooltipPosition.Left}
icon="Revert" text="Unavailable - another user is editing this app"
disabled={!$isOnlyUser}
tooltip={$isOnlyUser
? null
: "Unavailable - another user is editing this app"}
> >
Restore <MenuItem
</MenuItem> on:click={restoreDialog.show}
icon="Revert"
disabled={!$isOnlyUser}
>
Restore
</MenuItem>
</AbsTooltip>
<MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem> <MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem>
<MenuItem on:click={downloadExport} icon="Download">Download</MenuItem> <MenuItem on:click={downloadExport} icon="Download">Download</MenuItem>
{/if} {/if}

View File

@ -1,6 +1,6 @@
<script> <script>
import { Heading, Select, ActionButton } from "@budibase/bbui" import { Heading, Select, ActionButton } from "@budibase/bbui"
import { devToolsStore } from "../../stores" import { devToolsStore, appStore } from "../../stores"
import { getContext } from "svelte" import { getContext } from "svelte"
const context = getContext("context") const context = getContext("context")
@ -45,27 +45,41 @@
icon="Code" icon="Code"
on:click={() => devToolsStore.actions.setVisible(!$devToolsStore.visible)} on:click={() => devToolsStore.actions.setVisible(!$devToolsStore.visible)}
> >
{$devToolsStore.visible ? "Close" : "Open"} DevTools DevTools
</ActionButton>
{/if}
{#if window.parent.isBuilder}
<ActionButton
quiet
icon="LinkOut"
on:click={() => {
window.parent.closePreview?.()
window.open(`/${$appStore.appId}`, "_blank")
}}
>
Fullscreen
</ActionButton>
<ActionButton
quiet
icon="Close"
on:click={() => window.parent.closePreview?.()}
>
Close
</ActionButton> </ActionButton>
{/if} {/if}
<ActionButton
quiet
icon="Close"
on:click={() => window.parent.closePreview?.()}
>
Close preview
</ActionButton>
</div> </div>
<style> <style>
.dev-preview-header { .dev-preview-header {
flex: 0 0 60px; flex: 0 0 60px;
display: grid;
align-items: center;
background-color: black; background-color: black;
padding: 0 var(--spacing-xl); padding: 0 var(--spacing-xl);
grid-template-columns: 1fr auto auto auto; display: flex;
grid-gap: var(--spacing-xl); align-items: center;
gap: var(--spacing-xl);
}
.dev-preview-header :global(.spectrum-Heading) {
flex: 1 1 auto;
} }
.dev-preview-header.mobile { .dev-preview-header.mobile {
grid-template-columns: 1fr auto auto; grid-template-columns: 1fr auto auto;

View File

@ -1,60 +1,23 @@
<script> <script>
import { Avatar, Tooltip } from "@budibase/bbui" import { Avatar, AbsTooltip, TooltipPosition } from "@budibase/bbui"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
export let user export let user
export let size export let size = "S"
export let tooltipDirection = "top" export let tooltipPosition = TooltipPosition.Top
export let showTooltip = true export let showTooltip = true
$: tooltipStyle = getTooltipStyle(tooltipDirection)
const getTooltipStyle = direction => {
if (!direction) {
return ""
}
if (direction === "top") {
return "transform: translateX(-50%) translateY(-100%);"
} else if (direction === "bottom") {
return "transform: translateX(-50%) translateY(100%);"
}
}
</script> </script>
{#if user} {#if user}
<div class="user-avatar"> <AbsTooltip
text={showTooltip ? helpers.getUserLabel(user) : null}
position={tooltipPosition}
color={helpers.getUserColor(user)}
>
<Avatar <Avatar
{size} {size}
initials={helpers.getUserInitials(user)} initials={helpers.getUserInitials(user)}
color={helpers.getUserColor(user)} color={helpers.getUserColor(user)}
/> />
{#if showTooltip} </AbsTooltip>
<div class="tooltip" style={tooltipStyle}>
<Tooltip
direction={tooltipDirection}
textWrapping
text={helpers.getUserLabel(user)}
size="S"
/>
</div>
{/if}
</div>
{/if} {/if}
<style>
.user-avatar {
position: relative;
}
.tooltip {
position: absolute;
top: 0;
left: 50%;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 130ms ease-out;
}
.user-avatar:hover .tooltip {
opacity: 1;
}
</style>

View File

@ -1,16 +0,0 @@
<script>
import { ActionButton } from "@budibase/bbui"
import { getContext } from "svelte"
const { config, dispatch } = getContext("grid")
</script>
<ActionButton
icon="TableColumnAddRight"
quiet
size="M"
on:click={() => dispatch("add-column")}
disabled={!$config.allowSchemaChanges}
>
Add column
</ActionButton>

View File

@ -1,18 +0,0 @@
<script>
import { ActionButton } from "@budibase/bbui"
import { getContext } from "svelte"
const { dispatch, columns, stickyColumn, config, loaded } = getContext("grid")
</script>
<ActionButton
icon="TableRowAddBottom"
quiet
size="M"
on:click={() => dispatch("add-row-inline")}
disabled={!loaded ||
!$config.allowAddRows ||
(!$columns.length && !$stickyColumn)}
>
Add row
</ActionButton>

View File

@ -71,6 +71,7 @@
contentLines, contentLines,
gridFocused, gridFocused,
error, error,
canAddRows,
} = context } = context
// Keep config store up to date with props // Keep config store up to date with props
@ -143,7 +144,7 @@
<HeaderRow /> <HeaderRow />
<GridBody /> <GridBody />
</div> </div>
{#if allowAddRows} {#if $canAddRows}
<NewRow /> <NewRow />
{/if} {/if}
<div class="overlays"> <div class="overlays">

View File

@ -9,7 +9,7 @@
renderedRows, renderedRows,
renderedColumns, renderedColumns,
rowVerticalInversionIndex, rowVerticalInversionIndex,
config, canAddRows,
hoveredRowId, hoveredRowId,
dispatch, dispatch,
isDragging, isDragging,
@ -43,7 +43,7 @@
invertY={idx >= $rowVerticalInversionIndex} invertY={idx >= $rowVerticalInversionIndex}
/> />
{/each} {/each}
{#if $config.allowAddRows && $renderedColumns.length} {#if $canAddRows}
<div <div
class="blank" class="blank"
class:highlighted={$hoveredRowId === BlankRowID} class:highlighted={$hoveredRowId === BlankRowID}

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import GridScrollWrapper from "./GridScrollWrapper.svelte" import GridScrollWrapper from "./GridScrollWrapper.svelte"
import HeaderCell from "../cells/HeaderCell.svelte" import HeaderCell from "../cells/HeaderCell.svelte"
import { Icon } from "@budibase/bbui" import { Icon, TempTooltip, TooltipType } from "@budibase/bbui"
const { const {
renderedColumns, renderedColumns,
@ -11,10 +11,13 @@
hiddenColumnsWidth, hiddenColumnsWidth,
width, width,
config, config,
hasNonAutoColumn,
tableId,
loading,
} = getContext("grid") } = getContext("grid")
$: columnsWidth = $renderedColumns.reduce( $: columnsWidth = $renderedColumns.reduce(
(total, col) => (total += col.width), (total, col) => total + col.width,
0 0
) )
$: end = $hiddenColumnsWidth + columnsWidth - 1 - $scroll.left $: end = $hiddenColumnsWidth + columnsWidth - 1 - $scroll.left
@ -30,13 +33,21 @@
</div> </div>
</GridScrollWrapper> </GridScrollWrapper>
{#if $config.allowSchemaChanges} {#if $config.allowSchemaChanges}
<div {#key $tableId}
class="add" <TempTooltip
style="left:{left}px" text="Click here to create your first column"
on:click={() => dispatch("add-column")} type={TooltipType.Info}
> condition={!$hasNonAutoColumn && !$loading}
<Icon name="Add" /> >
</div> <div
class="add"
style="left:{left}px;"
on:click={() => dispatch("add-column")}
>
<Icon name="Add" />
</div>
</TempTooltip>
{/key}
{/if} {/if}
</div> </div>

View File

@ -1,6 +1,6 @@
<script> <script>
import { getContext, onDestroy, onMount, tick } from "svelte" import { getContext, onDestroy, onMount, tick } from "svelte"
import { Icon, Button } from "@budibase/bbui" import { Icon, Button, TempTooltip, TooltipType } from "@budibase/bbui"
import GridScrollWrapper from "./GridScrollWrapper.svelte" import GridScrollWrapper from "./GridScrollWrapper.svelte"
import DataCell from "../cells/DataCell.svelte" import DataCell from "../cells/DataCell.svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
@ -27,7 +27,8 @@
rowVerticalInversionIndex, rowVerticalInversionIndex,
columnHorizontalInversionIndex, columnHorizontalInversionIndex,
selectedRows, selectedRows,
config, loading,
canAddRows,
} = getContext("grid") } = getContext("grid")
let visible = false let visible = false
@ -40,6 +41,7 @@
$: $tableId, (visible = false) $: $tableId, (visible = false)
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows) $: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
$: selectedRowCount = Object.values($selectedRows).length $: selectedRowCount = Object.values($selectedRows).length
$: hasNoRows = !$rows.length
const shouldInvertY = (offset, inversionIndex, rows) => { const shouldInvertY = (offset, inversionIndex, rows) => {
if (offset === 0) { if (offset === 0) {
@ -147,16 +149,22 @@
</script> </script>
<!-- New row FAB --> <!-- New row FAB -->
{#if !visible && !selectedRowCount && $config.allowAddRows && firstColumn} <TempTooltip
<div text="Click here to create your first row"
class="new-row-fab" condition={hasNoRows && !$loading}
on:click={() => dispatch("add-row-inline")} type={TooltipType.Info}
transition:fade|local={{ duration: 130 }} >
class:offset={!$stickyColumn} {#if !visible && !selectedRowCount && $canAddRows}
> <div
<Icon name="Add" size="S" /> class="new-row-fab"
</div> on:click={() => dispatch("add-row-inline")}
{/if} transition:fade|local={{ duration: 130 }}
class:offset={!$stickyColumn}
>
<Icon name="Add" size="S" />
</div>
{/if}
</TempTooltip>
<!-- Only show new row functionality if we have any columns --> <!-- Only show new row functionality if we have any columns -->
{#if visible} {#if visible}

View File

@ -13,11 +13,10 @@
rows, rows,
selectedRows, selectedRows,
stickyColumn, stickyColumn,
renderedColumns,
renderedRows, renderedRows,
focusedCellId, focusedCellId,
hoveredRowId, hoveredRowId,
config, canAddRows,
selectedCellMap, selectedCellMap,
focusedRow, focusedRow,
scrollLeft, scrollLeft,
@ -93,7 +92,7 @@
{/if} {/if}
</div> </div>
{/each} {/each}
{#if $config.allowAddRows && ($renderedColumns.length || $stickyColumn)} {#if $canAddRows}
<div <div
class="row new" class="row new"
on:mouseenter={$isDragging on:mouseenter={$isDragging

View File

@ -16,6 +16,7 @@
config, config,
menu, menu,
gridFocused, gridFocused,
canAddRows,
} = getContext("grid") } = getContext("grid")
const ignoredOriginSelectors = [ const ignoredOriginSelectors = [
@ -45,7 +46,7 @@
e.preventDefault() e.preventDefault()
focusFirstCell() focusFirstCell()
} else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { } else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
if ($config.allowAddRows) { if ($canAddRows) {
e.preventDefault() e.preventDefault()
dispatch("add-row-inline") dispatch("add-row-inline")
} }
@ -99,7 +100,7 @@
} }
break break
case "Enter": case "Enter":
if ($config.allowAddRows) { if ($canAddRows) {
dispatch("add-row-inline") dispatch("add-row-inline")
} }
} }

View File

@ -17,6 +17,7 @@
focusedCellAPI, focusedCellAPI,
focusedRowId, focusedRowId,
notifications, notifications,
canAddRows,
} = getContext("grid") } = getContext("grid")
$: style = makeStyle($menu) $: style = makeStyle($menu)
@ -93,7 +94,7 @@
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon="Duplicate" icon="Duplicate"
disabled={isNewRow || !$config.allowAddRows} disabled={isNewRow || !$canAddRows}
on:click={duplicate} on:click={duplicate}
> >
Duplicate row Duplicate row

View File

@ -83,6 +83,21 @@ export const deriveStores = context => {
await saveChanges() await saveChanges()
} }
// Derive if we have any normal columns
const hasNonAutoColumn = derived(
[columns, stickyColumn],
([$columns, $stickyColumn]) => {
let allCols = $columns || []
if ($stickyColumn) {
allCols = [...allCols, $stickyColumn]
}
const normalCols = allCols.filter(column => {
return !column.schema?.autocolumn
})
return normalCols.length > 0
}
)
// Persists column changes by saving metadata against table schema // Persists column changes by saving metadata against table schema
const saveChanges = async () => { const saveChanges = async () => {
const $columns = get(columns) const $columns = get(columns)
@ -128,6 +143,7 @@ export const deriveStores = context => {
} }
return { return {
hasNonAutoColumn,
columns: { columns: {
...columns, ...columns,
actions: { actions: {

View File

@ -70,6 +70,8 @@ export const deriveStores = context => {
rowHeight, rowHeight,
stickyColumn, stickyColumn,
width, width,
hasNonAutoColumn,
config,
} = context } = context
// Derive the row that contains the selected cell // Derive the row that contains the selected cell
@ -112,7 +114,16 @@ export const deriveStores = context => {
return ($stickyColumn?.width || 0) + $width + GutterWidth < 1100 return ($stickyColumn?.width || 0) + $width + GutterWidth < 1100
}) })
// Derive if we're able to add rows
const canAddRows = derived(
[config, hasNonAutoColumn],
([$config, $hasNonAutoColumn]) => {
return $config.allowAddRows && $hasNonAutoColumn
}
)
return { return {
canAddRows,
focusedRow, focusedRow,
contentLines, contentLines,
compact, compact,