1
0
Fork 0
mirror of synced 2024-09-21 03:43:21 +12:00

Add automatic overflow menu popover for views that don't fit

This commit is contained in:
Andrew Kingston 2024-08-15 19:49:30 +01:00
parent d313968eaa
commit 40e7f58131
No known key found for this signature in database
3 changed files with 134 additions and 43 deletions

View file

@ -35,7 +35,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<li <li
on:click|preventDefault={disabled ? null : onClick} on:click={disabled ? null : onClick}
class="spectrum-Menu-item" class="spectrum-Menu-item"
class:is-disabled={disabled} class:is-disabled={disabled}
role="menuitem" role="menuitem"

View file

@ -9,7 +9,9 @@
let modal let modal
$: views = Object.keys(table?.views || {}).map(x => x.toLowerCase()) $: views = Object.keys(table?.views || {}).map(x => x.toLowerCase())
$: nameExists = views.includes(name?.trim().toLowerCase()) $: trimmedName = name?.trim()
$: nameExists = views.includes(trimmedName?.toLowerCase())
$: nameValid = trimmedName?.length && !nameExists
export const show = () => { export const show = () => {
name = null name = null
@ -29,15 +31,15 @@
} }
const saveView = async () => { const saveView = async () => {
name = name?.trim()
try { try {
const newView = await viewsV2.create({ const newView = await viewsV2.create({
name, name: trimmedName,
tableId: table._id, tableId: table._id,
schema: enrichSchema(table.schema), schema: enrichSchema(table.schema),
primaryDisplay: table.primaryDisplay, primaryDisplay: table.primaryDisplay,
}) })
notifications.success(`View ${name} created`) notifications.success(`View ${name} created`)
name = null
$goto(`./${newView.id}`) $goto(`./${newView.id}`)
} catch (error) { } catch (error) {
notifications.error("Error creating view") notifications.error("Error creating view")
@ -50,7 +52,7 @@
title="Create view" title="Create view"
confirmText="Create view" confirmText="Create view"
onConfirm={saveView} onConfirm={saveView}
disabled={nameExists} disabled={!nameValid}
> >
<Input <Input
label="View name" label="View name"

View file

@ -6,7 +6,7 @@
contextMenuStore, contextMenuStore,
} from "stores/builder" } from "stores/builder"
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte" import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
import { Icon, Button } from "@budibase/bbui" import { Icon, Button, Popover, ActionMenu, MenuItem } from "@budibase/bbui"
import { params, url } from "@roxi/routify" import { params, url } from "@roxi/routify"
import EditViewModal from "./EditViewModal.svelte" import EditViewModal from "./EditViewModal.svelte"
import DeleteViewModal from "./DeleteViewModal.svelte" import DeleteViewModal from "./DeleteViewModal.svelte"
@ -18,6 +18,13 @@
import { TableNames } from "constants" import { TableNames } from "constants"
import { alphabetical } from "components/backend/TableNavigator/utils" import { alphabetical } from "components/backend/TableNavigator/utils"
import CreateViewModal from "./CreateViewModal.svelte" import CreateViewModal from "./CreateViewModal.svelte"
import { onDestroy } from "svelte"
let viewContainer
let observer
let viewVisibiltyMap = {}
let overflowPopover
let anchor
// Editing table // Editing table
let createViewModal let createViewModal
@ -30,8 +37,8 @@
let deleteViewModal let deleteViewModal
$: tableId = $params.tableId $: tableId = $params.tableId
$: table = $tables.list.find(x => x._id === tableId) $: table = $tables.list.find(table => table._id === tableId)
$: datasource = $datasources.list.find(x => x._id === table?.sourceId) $: datasource = $datasources.list.find(ds => ds._id === table?.sourceId)
$: tableSelectedBy = $userSelectedResourceMap[table?._id] $: tableSelectedBy = $userSelectedResourceMap[table?._id]
$: tableEditable = table?._id !== TableNames.USERS $: tableEditable = table?._id !== TableNames.USERS
$: activeId = $params.viewId ?? $params.tableId $: activeId = $params.viewId ?? $params.tableId
@ -39,6 +46,8 @@
.filter(x => x.version === 2) .filter(x => x.version === 2)
.slice() .slice()
.sort(alphabetical) .sort(alphabetical)
$: setUpObserver(viewContainer, views)
$: overflowedViews = views.filter(view => !viewVisibiltyMap[view.id])
const openTableContextMenu = e => { const openTableContextMenu = e => {
if (!tableEditable) { if (!tableEditable) {
@ -104,9 +113,39 @@
} }
) )
} }
const setUpObserver = (viewContainer, views) => {
if (!views.length || !viewContainer) {
observer?.disconnect()
return
}
observer = new IntersectionObserver(
entries => {
let updates = {}
for (let entry of entries) {
updates[entry.target.dataset.id] = entry.isIntersecting
}
viewVisibiltyMap = {
...viewVisibiltyMap,
...updates,
}
},
{
threshold: 1,
root: viewContainer,
}
)
for (let child of viewContainer.children) {
observer.observe(child)
}
}
onDestroy(() => {
observer?.disconnect()
})
</script> </script>
<div class="view-nav-bar"> <div class="nav">
<IntegrationIcon <IntegrationIcon
integrationType={datasource.source} integrationType={datasource.source}
schema={datasource.schema} schema={datasource.schema}
@ -114,11 +153,11 @@
/> />
<a <a
href={$url(`../${tableId}`)} href={$url(`../${tableId}`)}
class="nav-bar-item" class="nav-item"
class:active={tableId === activeId} class:active={tableId === activeId}
on:contextmenu={openTableContextMenu} on:contextmenu={openTableContextMenu}
> >
<div class="title"> <div class="nav-item__title">
{table.name} {table.name}
</div> </div>
{#if tableSelectedBy} {#if tableSelectedBy}
@ -134,35 +173,66 @@
/> />
{/if} {/if}
</a> </a>
{#each views as view (view.id)} {#if views.length}
{@const selectedBy = $userSelectedResourceMap[view.id]} <div class="nav__views" bind:this={viewContainer}>
<a {#each views as view (view.id)}
href={$url(`../${tableId}/${encodeURIComponent(view.id)}`)} {@const selectedBy = $userSelectedResourceMap[view.id]}
class="nav-bar-item" <a
class:active={view.id === activeId} href={$url(`../${tableId}/${encodeURIComponent(view.id)}`)}
on:contextmenu={e => openViewContextMenu(e, view)} class="nav-item"
> class:active={view.id === activeId}
<div class="title"> class:hidden={!viewVisibiltyMap[view.id]}
{view.name} on:contextmenu={e => openViewContextMenu(e, view)}
</div> data-id={view.id}
{#if selectedBy} >
<UserAvatars size="XS" users={selectedBy} /> <div class="nav-item__title">
{/if} {view.name}
<Icon </div>
on:click={e => openViewContextMenu(e, view)} {#if selectedBy}
hoverable <UserAvatars size="XS" users={selectedBy} />
name="MoreSmallList" {/if}
color="var(--spectrum-global-color-gray-600)" <Icon
hoverColor="var(--spectrum-global-color-gray-900)" on:click={e => openViewContextMenu(e, view)}
/> hoverable
</a> name="MoreSmallList"
{/each} color="var(--spectrum-global-color-gray-600)"
hoverColor="var(--spectrum-global-color-gray-900)"
/>
</a>
{/each}
</div>
{/if}
{#if !views.length && tableEditable} {#if !views.length && tableEditable}
<Button cta on:click={createViewModal?.show}>Create a view</Button> <Button cta on:click={createViewModal?.show}>Create a view</Button>
<span> <span>
To create subsets of data, control access and more, create a view. To create subsets of data, control access and more, create a view.
</span> </span>
{/if} {/if}
{#if overflowedViews.length}
<ActionMenu align="right">
<div slot="control">
<Icon
name="ChevronDown"
size="XL"
hoverable
color="var(--spectrum-global-color-gray-600)"
hoverColor="var(--spectrum-global-color-gray-900)"
on:click={overflowPopover?.show}
/>
</div>
{#each overflowedViews as view}
<a
class="nav-overflow-item"
class:active={view.id === activeId}
href={$url(`../${tableId}/${encodeURIComponent(view.id)}`)}
>
<MenuItem>
{view.name}
</MenuItem>
</a>
{/each}
</ActionMenu>
{/if}
{#if views.length} {#if views.length}
<Icon <Icon
name="Add" name="Add"
@ -187,7 +257,8 @@
{/if} {/if}
<style> <style>
.view-nav-bar { /* Main containers */
.nav {
height: 50px; height: 50px;
border-bottom: var(--border-light); border-bottom: var(--border-light);
display: flex; display: flex;
@ -195,9 +266,21 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
padding: 0 var(--spacing-xl); padding: 0 var(--spacing-xl);
gap: var(--spacing-m); gap: 8px;
} }
.nav-bar-item { .nav__views {
width: 0;
flex: 1 1 auto;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
overflow: hidden;
gap: 8px;
}
/* Table and view items */
.nav-item {
padding: 6px 8px; padding: 6px 8px;
border-radius: 4px; border-radius: 4px;
display: flex; display: flex;
@ -208,16 +291,22 @@
transition: background 130ms ease-out, color 130ms ease-out; transition: background 130ms ease-out, color 130ms ease-out;
color: var(--spectrum-global-color-gray-600); color: var(--spectrum-global-color-gray-600);
} }
.title { .nav-item.hidden {
font-size: 16px; visibility: hidden;
} }
.nav-bar-item.active, .nav-item.active,
.nav-bar-item:hover { .nav-item:hover {
color: var(--spectrum-global-color-gray-900);
background: var(--spectrum-global-color-gray-300); background: var(--spectrum-global-color-gray-300);
cursor: pointer; cursor: pointer;
color: var(--spectrum-global-color-gray-900);
} }
.nav-bar-item:not(.active) :global(.icon) { .nav-item:not(.active) :global(.icon) {
display: none; display: none;
} }
.nav-item__title {
max-width: 150px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
</style> </style>