Add indicators to show selected state in data section

This commit is contained in:
Andrew Kingston 2023-07-04 08:58:14 +01:00
parent 4725faf8b5
commit 7be2d6896e
14 changed files with 117 additions and 3 deletions

View File

@ -118,3 +118,16 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
x => x._id === $automationStore.selectedAutomationId x => x._id === $automationStore.selectedAutomationId
) )
}) })
// Derive map of resource IDs to other users.
// We only ever care about a single user in each resource, so if multiple users
// share the same datasource we can just overwrite them.
export const userSelectedResourceMap = derived(userStore, $userStore => {
let map = {}
$userStore.forEach(user => {
if (user.selectedResourceId) {
map[user.selectedResourceId] = user
}
})
return map
})

View File

@ -38,6 +38,7 @@ import {
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import { getComponentFieldOptions } from "helpers/formFields" import { getComponentFieldOptions } from "helpers/formFields"
import { createBuilderWebsocket } from "builderStore/websocket" import { createBuilderWebsocket } from "builderStore/websocket"
import { BuilderSocketEvent } from "@budibase/shared-core"
const INITIAL_FRONTEND_STATE = { const INITIAL_FRONTEND_STATE = {
initialised: false, initialised: false,
@ -1394,6 +1395,13 @@ export const getFrontendStore = () => {
}) })
}, },
}, },
websocket: {
selectResource: id => {
websocket.emit(BuilderSocketEvent.SelectResource, {
resourceId: id,
})
},
},
} }
return store return store

View File

@ -13,6 +13,7 @@
} from "helpers/data/utils" } from "helpers/data/utils"
import IntegrationIcon from "./IntegrationIcon.svelte" import IntegrationIcon from "./IntegrationIcon.svelte"
import { TableNames } from "constants" import { TableNames } from "constants"
import { userSelectedResourceMap } from "builderStore"
let openDataSources = [] let openDataSources = []
@ -166,6 +167,7 @@
selected={$isActive("./table/:tableId") && selected={$isActive("./table/:tableId") &&
$tables.selected?._id === TableNames.USERS} $tables.selected?._id === TableNames.USERS}
on:click={() => selectTable(TableNames.USERS)} on:click={() => selectTable(TableNames.USERS)}
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
/> />
{#each enrichedDataSources as datasource, idx} {#each enrichedDataSources as datasource, idx}
<NavItem <NavItem
@ -176,6 +178,7 @@
withArrow={true} withArrow={true}
on:click={() => selectDatasource(datasource)} on:click={() => selectDatasource(datasource)}
on:iconClick={() => toggleNode(datasource)} on:iconClick={() => toggleNode(datasource)}
selectedBy={$userSelectedResourceMap[datasource._id]}
> >
<div class="datasource-icon" slot="icon"> <div class="datasource-icon" slot="icon">
<IntegrationIcon <IntegrationIcon
@ -201,6 +204,7 @@
selected={$isActive("./query/:queryId") && selected={$isActive("./query/:queryId") &&
$queries.selectedQueryId === query._id} $queries.selectedQueryId === query._id}
on:click={() => $goto(`./query/${query._id}`)} on:click={() => $goto(`./query/${query._id}`)}
selectedBy={$userSelectedResourceMap[query._id]}
> >
<EditQueryPopover {query} /> <EditQueryPopover {query} />
</NavItem> </NavItem>

View File

@ -5,6 +5,7 @@
import EditViewPopover from "./popovers/EditViewPopover.svelte" import EditViewPopover from "./popovers/EditViewPopover.svelte"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import { goto, isActive } from "@roxi/routify" import { goto, isActive } from "@roxi/routify"
import { userSelectedResourceMap } from "builderStore"
const alphabetical = (a, b) => const alphabetical = (a, b) =>
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1 a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
@ -30,6 +31,7 @@
selected={$isActive("./table/:tableId") && selected={$isActive("./table/:tableId") &&
$tables.selected?._id === table._id} $tables.selected?._id === table._id}
on:click={() => selectTable(table._id)} on:click={() => selectTable(table._id)}
selectedBy={$userSelectedResourceMap[table._id]}
> >
{#if table._id !== TableNames.USERS} {#if table._id !== TableNames.USERS}
<EditTablePopover {table} /> <EditTablePopover {table} />
@ -42,6 +44,7 @@
text={viewName} text={viewName}
selected={$isActive("./view") && $views.selected?.name === viewName} selected={$isActive("./view") && $views.selected?.name === viewName}
on:click={() => $goto(`./view/${encodeURIComponent(viewName)}`)} on:click={() => $goto(`./view/${encodeURIComponent(viewName)}`)}
selectedBy={$userSelectedResourceMap[viewName]}
> >
<EditViewPopover <EditViewPopover
view={{ name: viewName, ...table.views[viewName] }} view={{ name: viewName, ...table.views[viewName] }}

View File

@ -1,6 +1,8 @@
<script> <script>
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 { UserAvatar } from "@budibase/frontend-core"
export let icon export let icon
export let withArrow = false export let withArrow = false
@ -18,12 +20,15 @@
export let rightAlignIcon = false export let rightAlignIcon = false
export let id export let id
export let showTooltip = false export let showTooltip = false
export let selectedBy = null
const scrollApi = getContext("scroll") const scrollApi = getContext("scroll")
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let contentRef let contentRef
$: selected && contentRef && scrollToView() $: selected && contentRef && scrollToView()
$: style = getStyle(indentLevel, selectedBy)
const onClick = () => { const onClick = () => {
scrollToView() scrollToView()
@ -42,6 +47,14 @@
const bounds = contentRef.getBoundingClientRect() const bounds = contentRef.getBoundingClientRect()
scrollApi.scrollTo(bounds) scrollApi.scrollTo(bounds)
} }
const getStyle = (indentLevel, selectedBy) => {
let style = `padding-left:calc(${indentLevel * 14}px);`
if (selectedBy) {
style += `--selected-by-color:${helpers.getUserColor(selectedBy)};`
}
return style
}
</script> </script>
<div <div
@ -51,8 +64,7 @@
class:withActions class:withActions
class:scrollable class:scrollable
class:highlighted class:highlighted
style={`padding-left: calc(${indentLevel * 14}px)`} class:selectedBy
{draggable}
on:dragend on:dragend
on:dragstart on:dragstart
on:dragover on:dragover
@ -61,6 +73,8 @@
ondragover="return false" ondragover="return false"
ondragenter="return false" ondragenter="return false"
{id} {id}
{style}
{draggable}
> >
<div class="nav-item-content" bind:this={contentRef}> <div class="nav-item-content" bind:this={contentRef}>
{#if withArrow} {#if withArrow}
@ -97,6 +111,14 @@
</div> </div>
{/if} {/if}
</div> </div>
{#if selectedBy}
<div class="selected-by-label">{helpers.getUserLabel(selectedBy)}</div>
{/if}
<!--{#if selectedBy}-->
<!-- <div class="avatar">-->
<!-- <UserAvatar size="S" user={selectedBy} tooltipDirection="left" />-->
<!-- </div>-->
<!--{/if}-->
</div> </div>
<style> <style>
@ -142,6 +164,37 @@
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;
border-radius: 2px;
}
.selected-by-label {
position: absolute;
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;
}
.avatar {
align-self: center;
}
/* 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;

View File

@ -5,6 +5,8 @@
import { isActive, goto, redirect } from "@roxi/routify" import { isActive, goto, redirect } from "@roxi/routify"
import BetaButton from "./_components/BetaButton.svelte" import BetaButton from "./_components/BetaButton.svelte"
import { datasources } from "stores/backend" import { datasources } from "stores/backend"
import { onDestroy } from "svelte"
import { store } from "builderStore"
$: { $: {
// If we ever don't have any data other than the users table, prompt the // If we ever don't have any data other than the users table, prompt the
@ -13,6 +15,10 @@
$redirect("./new") $redirect("./new")
} }
} }
onDestroy(() => {
store.actions.websocket.selectResource(null)
})
</script> </script>
<!-- routify:options index=1 --> <!-- routify:options index=1 -->

View File

@ -4,6 +4,10 @@
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"
import { store } from "builderStore"
$: datasourceId = $datasources.selectedDatasourceId
$: store.actions.websocket.selectResource(datasourceId)
const stopSyncing = syncURLToState({ const stopSyncing = syncURLToState({
urlParam: "datasourceId", urlParam: "datasourceId",

View File

@ -7,9 +7,11 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend" import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
import { TableNames } from "constants" import { TableNames } from "constants"
import { store } from "builderStore"
let modal let modal
$: store.actions.websocket.selectResource(BUDIBASE_INTERNAL_DB_ID)
$: internalTablesBySourceId = $tables.list.filter( $: internalTablesBySourceId = $tables.list.filter(
table => table =>
table.type !== "external" && table.type !== "external" &&

View File

@ -6,8 +6,11 @@
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend" import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
import { onMount } from "svelte" import { onMount } from "svelte"
import { store } from "builderStore"
let modal let modal
$: store.actions.websocket.selectResource(DEFAULT_BB_DATASOURCE_ID)
$: internalTablesBySourceId = $tables.list.filter( $: internalTablesBySourceId = $tables.list.filter(
table => table =>
table.type !== "external" && table.sourceId === DEFAULT_BB_DATASOURCE_ID table.type !== "external" && table.sourceId === DEFAULT_BB_DATASOURCE_ID

View File

@ -3,6 +3,10 @@
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"
import { store } from "builderStore"
$: queryId = $queries.selectedQueryId
$: store.actions.websocket.selectResource(queryId)
const stopSyncing = syncURLToState({ const stopSyncing = syncURLToState({
urlParam: "queryId", urlParam: "queryId",

View File

@ -3,6 +3,10 @@
import { tables } from "stores/backend" import { tables } from "stores/backend"
import * as routify from "@roxi/routify" import * as routify from "@roxi/routify"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import { store } from "builderStore"
$: tableId = $tables.selectedTableId
$: store.actions.websocket.selectResource(tableId)
const stopSyncing = syncURLToState({ const stopSyncing = syncURLToState({
urlParam: "tableId", urlParam: "tableId",

View File

@ -3,6 +3,10 @@
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"
import { store } from "builderStore"
$: viewName = $views.selectedViewName
$: store.actions.websocket.selectResource(viewName)
const stopSyncing = syncURLToState({ const stopSyncing = syncURLToState({
urlParam: "viewName", urlParam: "viewName",

View File

@ -13,7 +13,7 @@ import {
import { gridSocket } from "./index" import { gridSocket } from "./index"
import { clearLock, updateLock } from "../utilities/redis" import { clearLock, updateLock } from "../utilities/redis"
import { Socket } from "socket.io" import { Socket } from "socket.io"
import { BuilderSocketEvent } from "@budibase/shared-core" import { BuilderSocketEvent, GridSocketEvent } from "@budibase/shared-core"
export default class BuilderSocket extends BaseSocket { export default class BuilderSocket extends BaseSocket {
constructor(app: Koa, server: http.Server) { constructor(app: Koa, server: http.Server) {
@ -38,6 +38,11 @@ export default class BuilderSocket extends BaseSocket {
// Reply with all current sessions // Reply with all current sessions
callback({ users: sessions }) callback({ users: sessions })
}) })
// Handle users selecting a new cell
socket?.on(BuilderSocketEvent.SelectResource, ({ resourceId }) => {
this.updateUser(socket, { selectedResourceId: resourceId })
})
} }
async onDisconnect(socket: Socket) { async onDisconnect(socket: Socket) {

View File

@ -88,6 +88,7 @@ export enum BuilderSocketEvent {
LockTransfer = "LockTransfer", LockTransfer = "LockTransfer",
ScreenChange = "ScreenChange", ScreenChange = "ScreenChange",
AppMetadataChange = "AppMetadataChange", AppMetadataChange = "AppMetadataChange",
SelectResource = "SelectResource",
} }
export const SocketSessionTTL = 60 export const SocketSessionTTL = 60