Add automatic overflow menu popover for views that don't fit
This commit is contained in:
parent
d313968eaa
commit
40e7f58131
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue