commit
325818748e
|
@ -44,13 +44,15 @@
|
||||||
<Grid
|
<Grid
|
||||||
{API}
|
{API}
|
||||||
tableId={id}
|
tableId={id}
|
||||||
tableType={$tables.selected?.type}
|
|
||||||
allowAddRows={!isUsersTable}
|
allowAddRows={!isUsersTable}
|
||||||
allowDeleteRows={!isUsersTable}
|
allowDeleteRows={!isUsersTable}
|
||||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||||
showAvatars={false}
|
showAvatars={false}
|
||||||
on:updatetable={handleGridTableUpdate}
|
on:updatetable={handleGridTableUpdate}
|
||||||
>
|
>
|
||||||
|
<svelte:fragment slot="filter">
|
||||||
|
<GridFilterButton />
|
||||||
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="controls">
|
<svelte:fragment slot="controls">
|
||||||
{#if isInternal}
|
{#if isInternal}
|
||||||
<GridCreateViewButton />
|
<GridCreateViewButton />
|
||||||
|
@ -65,7 +67,6 @@
|
||||||
<GridImportButton />
|
<GridImportButton />
|
||||||
{/if}
|
{/if}
|
||||||
<GridExportButton />
|
<GridExportButton />
|
||||||
<GridFilterButton />
|
|
||||||
<GridAddColumnModal />
|
<GridAddColumnModal />
|
||||||
<GridEditColumnModal />
|
<GridEditColumnModal />
|
||||||
{#if isUsersTable}
|
{#if isUsersTable}
|
||||||
|
|
|
@ -14,6 +14,12 @@
|
||||||
|
|
||||||
$: tempValue = filters || []
|
$: tempValue = filters || []
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = Object.values(schema || {})
|
||||||
|
$: text = getText(filters)
|
||||||
|
|
||||||
|
const getText = filters => {
|
||||||
|
const count = filters?.length
|
||||||
|
return count ? `Filter (${count})` : "Filter"
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
@ -23,7 +29,7 @@
|
||||||
on:click={modal.show}
|
on:click={modal.show}
|
||||||
selected={tempValue?.length > 0}
|
selected={tempValue?.length > 0}
|
||||||
>
|
>
|
||||||
Filter
|
{text}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
|
|
||||||
const { columns, tableId, filter, table } = getContext("grid")
|
const { columns, tableId, filter, table } = getContext("grid")
|
||||||
|
|
||||||
|
// Wipe filter whenever table ID changes to avoid using stale filters
|
||||||
|
$: $tableId, filter.set([])
|
||||||
|
|
||||||
const onFilter = e => {
|
const onFilter = e => {
|
||||||
filter.set(e.detail || [])
|
filter.set(e.detail || [])
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
|
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
|
||||||
const { rows, tableId, tableType } = getContext("grid")
|
const { rows, tableId, table } = getContext("grid")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ImportButton
|
<ImportButton
|
||||||
{disabled}
|
{disabled}
|
||||||
tableId={$tableId}
|
tableId={$tableId}
|
||||||
{tableType}
|
tableType={$table?.type}
|
||||||
on:importrows={rows.actions.refreshData}
|
on:importrows={rows.actions.refreshData}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
export let tableId
|
export let tableId
|
||||||
export let tableType
|
export let tableType
|
||||||
|
|
||||||
let rows = []
|
let rows = []
|
||||||
let allValid = false
|
let allValid = false
|
||||||
let displayColumn = null
|
let displayColumn = null
|
||||||
|
|
|
@ -2,9 +2,4 @@
|
||||||
import ColumnEditor from "./ColumnEditor.svelte"
|
import ColumnEditor from "./ColumnEditor.svelte"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ColumnEditor
|
<ColumnEditor {...$$props} on:change allowCellEditing={false} />
|
||||||
{...$$props}
|
|
||||||
on:change
|
|
||||||
allowCellEditing={false}
|
|
||||||
subject="Dynamic Filter"
|
|
||||||
/>
|
|
||||||
|
|
|
@ -142,10 +142,10 @@
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="wide">
|
<div class="wide">
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
By default, all table columns will automatically be shown.
|
By default, all columns will automatically be shown.
|
||||||
<br />
|
<br />
|
||||||
You can manually control which columns are included in your table,
|
You can manually control which columns are included by adding them
|
||||||
and their appearance, by adding them below.
|
below.
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let value = []
|
export let value = []
|
||||||
export let allowCellEditing = true
|
export let allowCellEditing = true
|
||||||
export let subject = "Table"
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -75,11 +74,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton on:click={open}>Configure columns</ActionButton>
|
<div class="column-editor">
|
||||||
<Drawer bind:this={drawer} title="{subject} Columns">
|
<ActionButton on:click={open}>Configure columns</ActionButton>
|
||||||
<svelte:fragment slot="description">
|
</div>
|
||||||
Configure the columns in your {subject.toLowerCase()}.
|
<Drawer bind:this={drawer} title="Columns">
|
||||||
</svelte:fragment>
|
|
||||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||||
<ColumnDrawer
|
<ColumnDrawer
|
||||||
slot="body"
|
slot="body"
|
||||||
|
@ -89,3 +87,9 @@
|
||||||
{allowCellEditing}
|
{allowCellEditing}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.column-editor :global(.spectrum-ActionButton) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, ActionButton, Drawer } from "@budibase/bbui"
|
import { Button, ActionButton, Drawer } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import ColumnDrawer from "./ColumnDrawer.svelte"
|
import ColumnDrawer from "./ColumnEditor/ColumnDrawer.svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import {
|
import {
|
||||||
getDatasourceForProvider,
|
getDatasourceForProvider,
|
||||||
|
|
|
@ -20,15 +20,26 @@
|
||||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: schema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
$: schema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = Object.values(schema || {})
|
||||||
|
$: text = getText(value)
|
||||||
|
|
||||||
async function saveFilter() {
|
async function saveFilter() {
|
||||||
dispatch("change", tempValue)
|
dispatch("change", tempValue)
|
||||||
notifications.success("Filters saved")
|
notifications.success("Filters saved")
|
||||||
drawer.hide()
|
drawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getText = filters => {
|
||||||
|
if (!filters?.length) {
|
||||||
|
return "No filters set"
|
||||||
|
} else {
|
||||||
|
return `${filters.length} filter${filters.length === 1 ? "" : "s"} set`
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
<div class="filter-editor">
|
||||||
|
<ActionButton on:click={drawer.show}>{text}</ActionButton>
|
||||||
|
</div>
|
||||||
<Drawer bind:this={drawer} title="Filtering">
|
<Drawer bind:this={drawer} title="Filtering">
|
||||||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
||||||
<FilterDrawer
|
<FilterDrawer
|
||||||
|
@ -40,3 +51,9 @@
|
||||||
on:change={e => (tempValue = e.detail)}
|
on:change={e => (tempValue = e.detail)}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.filter-editor :global(.spectrum-ActionButton) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"name": "Blocks",
|
"name": "Blocks",
|
||||||
"icon": "Article",
|
"icon": "Article",
|
||||||
"children": [
|
"children": [
|
||||||
|
"gridblock",
|
||||||
"tableblock",
|
"tableblock",
|
||||||
"cardsblock",
|
"cardsblock",
|
||||||
"repeaterblock",
|
"repeaterblock",
|
||||||
|
|
|
@ -5230,5 +5230,91 @@
|
||||||
"type": "schema",
|
"type": "schema",
|
||||||
"suffix": "repeater"
|
"suffix": "repeater"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"gridblock": {
|
||||||
|
"name": "Grid block",
|
||||||
|
"icon": "Table",
|
||||||
|
"styles": ["size"],
|
||||||
|
"size": {
|
||||||
|
"width": 600,
|
||||||
|
"height": 400
|
||||||
|
},
|
||||||
|
"info": "Grid Blocks are only compatible with internal or SQL tables",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"label": "Table",
|
||||||
|
"key": "table",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "columns/basic",
|
||||||
|
"label": "Columns",
|
||||||
|
"key": "columns",
|
||||||
|
"dependsOn": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "filter",
|
||||||
|
"label": "Filtering",
|
||||||
|
"key": "initialFilter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field/sortable",
|
||||||
|
"label": "Sort column",
|
||||||
|
"key": "initialSortColumn",
|
||||||
|
"placeholder": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Sort order",
|
||||||
|
"key": "initialSortOrder",
|
||||||
|
"options": ["Ascending", "Descending"],
|
||||||
|
"defaultValue": "Ascending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Row height",
|
||||||
|
"key": "initialRowHeight",
|
||||||
|
"placeholder": "Default",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Small",
|
||||||
|
"value": 36
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Medium",
|
||||||
|
"value": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Large",
|
||||||
|
"value": 92
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Add rows",
|
||||||
|
"key": "allowAddRows",
|
||||||
|
"defaultValue": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Edit rows",
|
||||||
|
"key": "allowEditRows",
|
||||||
|
"defaultValue": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Delete rows",
|
||||||
|
"key": "allowDeleteRows",
|
||||||
|
"defaultValue": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "High contrast",
|
||||||
|
"key": "stripeRows",
|
||||||
|
"defaultValue": false
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,8 @@ export const API = createAPIClient({
|
||||||
// We could also log these to sentry.
|
// We could also log these to sentry.
|
||||||
// Or we could check error.status and redirect to login on a 403 etc.
|
// Or we could check error.status and redirect to login on a 403 etc.
|
||||||
onError: error => {
|
onError: error => {
|
||||||
const { status, method, url, message, handled } = error || {}
|
const { status, method, url, message, handled, suppressErrors } =
|
||||||
|
error || {}
|
||||||
const ignoreErrorUrls = [
|
const ignoreErrorUrls = [
|
||||||
"bbtel",
|
"bbtel",
|
||||||
"/api/global/self",
|
"/api/global/self",
|
||||||
|
@ -49,7 +50,7 @@ export const API = createAPIClient({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify all errors
|
// Notify all errors
|
||||||
if (message) {
|
if (message && !suppressErrors) {
|
||||||
// Don't notify if the URL contains the word analytics as it may be
|
// Don't notify if the URL contains the word analytics as it may be
|
||||||
// blocked by browser extensions
|
// blocked by browser extensions
|
||||||
let ignore = false
|
let ignore = false
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script>
|
||||||
|
// NOTE: this is not a block - it's just named as such to avoid confusing users,
|
||||||
|
// because it functions similarly to one
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import { Grid } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
export let table
|
||||||
|
export let allowAddRows = true
|
||||||
|
export let allowEditRows = true
|
||||||
|
export let allowDeleteRows = true
|
||||||
|
export let stripeRows = false
|
||||||
|
export let initialFilter = null
|
||||||
|
export let initialSortColumn = null
|
||||||
|
export let initialSortOrder = null
|
||||||
|
export let initialRowHeight = null
|
||||||
|
export let columns = null
|
||||||
|
|
||||||
|
const component = getContext("component")
|
||||||
|
const { styleable, API, builderStore } = getContext("sdk")
|
||||||
|
|
||||||
|
$: columnWhitelist = columns?.map(col => col.name)
|
||||||
|
$: schemaOverrides = getSchemaOverrides(columns)
|
||||||
|
|
||||||
|
const getSchemaOverrides = columns => {
|
||||||
|
let overrides = {}
|
||||||
|
columns?.forEach(column => {
|
||||||
|
overrides[column.name] = {
|
||||||
|
displayName: column.displayName || column.name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return overrides
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
use:styleable={$component.styles}
|
||||||
|
class:in-builder={$builderStore.inBuilder}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
tableId={table?.tableId}
|
||||||
|
{API}
|
||||||
|
{allowAddRows}
|
||||||
|
{allowEditRows}
|
||||||
|
{allowDeleteRows}
|
||||||
|
{stripeRows}
|
||||||
|
{initialFilter}
|
||||||
|
{initialSortColumn}
|
||||||
|
{initialSortOrder}
|
||||||
|
{initialRowHeight}
|
||||||
|
{columnWhitelist}
|
||||||
|
{schemaOverrides}
|
||||||
|
showControls={false}
|
||||||
|
allowExpandRows={false}
|
||||||
|
allowSchemaChanges={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 410px;
|
||||||
|
}
|
||||||
|
div.in-builder :global(*) {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -36,6 +36,7 @@ export { default as markdownviewer } from "./MarkdownViewer.svelte"
|
||||||
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
||||||
export { default as grid } from "./Grid.svelte"
|
export { default as grid } from "./Grid.svelte"
|
||||||
export { default as sidepanel } from "./SidePanel.svelte"
|
export { default as sidepanel } from "./SidePanel.svelte"
|
||||||
|
export { default as gridblock } from "./GridBlock.svelte"
|
||||||
export * from "./charts"
|
export * from "./charts"
|
||||||
export * from "./forms"
|
export * from "./forms"
|
||||||
export * from "./table"
|
export * from "./table"
|
||||||
|
|
|
@ -75,7 +75,11 @@ export const createAPIClient = config => {
|
||||||
let cache = {}
|
let cache = {}
|
||||||
|
|
||||||
// Generates an error object from an API response
|
// Generates an error object from an API response
|
||||||
const makeErrorFromResponse = async (response, method) => {
|
const makeErrorFromResponse = async (
|
||||||
|
response,
|
||||||
|
method,
|
||||||
|
suppressErrors = false
|
||||||
|
) => {
|
||||||
// Try to read a message from the error
|
// Try to read a message from the error
|
||||||
let message = response.statusText
|
let message = response.statusText
|
||||||
let json = null
|
let json = null
|
||||||
|
@ -96,6 +100,7 @@ export const createAPIClient = config => {
|
||||||
url: response.url,
|
url: response.url,
|
||||||
method,
|
method,
|
||||||
handled: true,
|
handled: true,
|
||||||
|
suppressErrors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +124,7 @@ export const createAPIClient = config => {
|
||||||
json = true,
|
json = true,
|
||||||
external = false,
|
external = false,
|
||||||
parseResponse,
|
parseResponse,
|
||||||
|
suppressErrors = false,
|
||||||
}) => {
|
}) => {
|
||||||
// Ensure we don't do JSON processing if sending a GET request
|
// Ensure we don't do JSON processing if sending a GET request
|
||||||
json = json && method !== "GET"
|
json = json && method !== "GET"
|
||||||
|
@ -174,7 +180,7 @@ export const createAPIClient = config => {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
delete cache[url]
|
delete cache[url]
|
||||||
throw await makeErrorFromResponse(response, method)
|
throw await makeErrorFromResponse(response, method, suppressErrors)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,6 +234,14 @@ export const createAPIClient = config => {
|
||||||
invalidateCache: () => {
|
invalidateCache: () => {
|
||||||
cache = {}
|
cache = {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Generic utility to extract the current app ID. Assumes that any client
|
||||||
|
// that exists in an app context will be attaching our app ID header.
|
||||||
|
getAppID: () => {
|
||||||
|
let headers = {}
|
||||||
|
config?.attachHeaders(headers)
|
||||||
|
return headers?.["x-budibase-app-id"]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach all endpoints
|
// Attach all endpoints
|
||||||
|
|
|
@ -16,14 +16,16 @@ export const buildRowEndpoints = API => ({
|
||||||
/**
|
/**
|
||||||
* Creates or updates a row in a table.
|
* Creates or updates a row in a table.
|
||||||
* @param row the row to save
|
* @param row the row to save
|
||||||
|
* @param suppressErrors whether or not to suppress error notifications
|
||||||
*/
|
*/
|
||||||
saveRow: async row => {
|
saveRow: async (row, suppressErrors = false) => {
|
||||||
if (!row?.tableId) {
|
if (!row?.tableId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: `/api/${row.tableId}/rows`,
|
url: `/api/${row.tableId}/rows`,
|
||||||
body: row,
|
body: row,
|
||||||
|
suppressErrors,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -138,10 +138,12 @@
|
||||||
top: 100%;
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
background: var(--background);
|
background: var(--grid-background-alt);
|
||||||
border: var(--cell-border);
|
border: var(--cell-border);
|
||||||
padding: var(--cell-padding);
|
padding: var(--cell-padding);
|
||||||
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
}
|
}
|
||||||
.dropzone.invertX {
|
.dropzone.invertX {
|
||||||
left: auto;
|
left: auto;
|
||||||
|
|
|
@ -132,7 +132,7 @@
|
||||||
--cell-color: var(--user-color);
|
--cell-color: var(--user-color);
|
||||||
}
|
}
|
||||||
.cell.focused {
|
.cell.focused {
|
||||||
--cell-color: var(--spectrum-global-color-blue-400);
|
--cell-color: var(--accent-color);
|
||||||
}
|
}
|
||||||
.cell.error {
|
.cell.error {
|
||||||
--cell-color: var(--spectrum-global-color-red-500);
|
--cell-color: var(--spectrum-global-color-red-500);
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
export let rowFocused = false
|
export let rowFocused = false
|
||||||
export let rowHovered = false
|
export let rowHovered = false
|
||||||
export let rowSelected = false
|
export let rowSelected = false
|
||||||
export let disableExpand = false
|
export let expandable = false
|
||||||
export let disableNumber = false
|
export let disableNumber = false
|
||||||
export let defaultHeight = false
|
export let defaultHeight = false
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
@ -24,13 +24,6 @@
|
||||||
selectedRows.actions.toggleRow(id)
|
selectedRows.actions.toggleRow(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const expand = () => {
|
|
||||||
svelteDispatch("expand")
|
|
||||||
if (row) {
|
|
||||||
dispatch("edit-row", row)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<GridCell
|
<GridCell
|
||||||
|
@ -70,12 +63,14 @@
|
||||||
color="var(--spectrum-global-color-red-400)"
|
color="var(--spectrum-global-color-red-400)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if $config.allowExpandRows}
|
{:else}
|
||||||
<div
|
<div class="expand" class:visible={$config.allowExpandRows && expandable}>
|
||||||
class="expand"
|
<Icon
|
||||||
class:visible={!disableExpand && (rowFocused || rowHovered)}
|
size="S"
|
||||||
>
|
name="Maximize"
|
||||||
<Icon name="Maximize" hoverable size="S" on:click={expand} />
|
hoverable
|
||||||
|
on:click={() => svelteDispatch("expand")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -163,7 +163,7 @@
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="Edit"
|
icon="Edit"
|
||||||
on:click={editColumn}
|
on:click={editColumn}
|
||||||
disabled={!$config.allowEditColumns || column.schema.disabled}
|
disabled={!$config.allowSchemaChanges || column.schema.disabled}
|
||||||
>
|
>
|
||||||
Edit column
|
Edit column
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -171,7 +171,7 @@
|
||||||
icon="Label"
|
icon="Label"
|
||||||
on:click={makeDisplayColumn}
|
on:click={makeDisplayColumn}
|
||||||
disabled={idx === "sticky" ||
|
disabled={idx === "sticky" ||
|
||||||
!$config.allowEditColumns ||
|
!$config.allowSchemaChanges ||
|
||||||
bannedDisplayColumnTypes.includes(column.schema.type)}
|
bannedDisplayColumnTypes.includes(column.schema.type)}
|
||||||
>
|
>
|
||||||
Use as display column
|
Use as display column
|
||||||
|
@ -197,10 +197,12 @@
|
||||||
Move right
|
Move right
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={idx === "sticky"}
|
disabled={idx === "sticky" || !$config.showControls}
|
||||||
icon="VisibilityOff"
|
icon="VisibilityOff"
|
||||||
on:click={hideColumn}>Hide column</MenuItem
|
on:click={hideColumn}
|
||||||
>
|
>
|
||||||
|
Hide column
|
||||||
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
|
@ -218,7 +220,7 @@
|
||||||
.header-cell :global(.cell) {
|
.header-cell :global(.cell) {
|
||||||
padding: 0 var(--cell-padding);
|
padding: 0 var(--cell-padding);
|
||||||
gap: calc(2 * var(--cell-spacing));
|
gap: calc(2 * var(--cell-spacing));
|
||||||
background: var(--spectrum-global-color-gray-100);
|
background: var(--grid-background-alt);
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
|
|
|
@ -102,7 +102,7 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: calc(100% + var(--max-cell-render-width-overflow));
|
width: calc(100% + var(--max-cell-render-width-overflow));
|
||||||
height: var(--max-cell-render-height);
|
height: calc(var(--row-height) + var(--max-cell-render-height));
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
resize: none;
|
resize: none;
|
||||||
|
|
|
@ -132,10 +132,7 @@
|
||||||
{option}
|
{option}
|
||||||
</div>
|
</div>
|
||||||
{#if values.includes(option)}
|
{#if values.includes(option)}
|
||||||
<Icon
|
<Icon name="Checkmark" color="var(--accent-color)" />
|
||||||
name="Checkmark"
|
|
||||||
color="var(--spectrum-global-color-blue-400)"
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -223,6 +220,8 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: var(--cell-border);
|
border: var(--cell-border);
|
||||||
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
}
|
}
|
||||||
.options.invertX {
|
.options.invertX {
|
||||||
left: auto;
|
left: auto;
|
||||||
|
@ -240,7 +239,7 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--cell-spacing);
|
gap: var(--cell-spacing);
|
||||||
background-color: var(--background);
|
background-color: var(--grid-background-alt);
|
||||||
}
|
}
|
||||||
.option:hover,
|
.option:hover,
|
||||||
.option.focused {
|
.option.focused {
|
||||||
|
|
|
@ -42,6 +42,8 @@
|
||||||
let candidateIndex
|
let candidateIndex
|
||||||
let lastSearchId
|
let lastSearchId
|
||||||
let searching = false
|
let searching = false
|
||||||
|
let valuesHeight = 0
|
||||||
|
let container
|
||||||
|
|
||||||
$: oneRowOnly = schema?.relationshipType === "one-to-many"
|
$: oneRowOnly = schema?.relationshipType === "one-to-many"
|
||||||
$: editable = focused && !readonly
|
$: editable = focused && !readonly
|
||||||
|
@ -138,6 +140,7 @@
|
||||||
|
|
||||||
const open = async () => {
|
const open = async () => {
|
||||||
isOpen = true
|
isOpen = true
|
||||||
|
valuesHeight = container.getBoundingClientRect().height
|
||||||
|
|
||||||
// Find the primary display for the related table
|
// Find the primary display for the related table
|
||||||
if (!primaryDisplay) {
|
if (!primaryDisplay) {
|
||||||
|
@ -242,8 +245,14 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper" class:editable class:focused style="--color:{color};">
|
<div
|
||||||
<div class="container">
|
class="wrapper"
|
||||||
|
class:editable
|
||||||
|
class:focused
|
||||||
|
class:invertY
|
||||||
|
style="--color:{color};"
|
||||||
|
>
|
||||||
|
<div class="container" bind:this={container}>
|
||||||
<div
|
<div
|
||||||
class="values"
|
class="values"
|
||||||
class:wrap={editable || contentLines > 1}
|
class:wrap={editable || contentLines > 1}
|
||||||
|
@ -290,6 +299,7 @@
|
||||||
class:invertY
|
class:invertY
|
||||||
on:wheel|stopPropagation
|
on:wheel|stopPropagation
|
||||||
use:clickOutside={close}
|
use:clickOutside={close}
|
||||||
|
style="--values-height:{valuesHeight}px;"
|
||||||
>
|
>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<Input
|
<Input
|
||||||
|
@ -319,11 +329,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{#if isRowSelected(row)}
|
{#if isRowSelected(row)}
|
||||||
<Icon
|
<Icon size="S" name="Checkmark" color="var(--accent-color)" />
|
||||||
size="S"
|
|
||||||
name="Checkmark"
|
|
||||||
color="var(--spectrum-global-color-blue-400)"
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -340,7 +346,7 @@
|
||||||
min-height: var(--row-height);
|
min-height: var(--row-height);
|
||||||
max-height: var(--row-height);
|
max-height: var(--row-height);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
--max-relationship-height: 120px;
|
--max-relationship-height: 96px;
|
||||||
}
|
}
|
||||||
.wrapper.focused {
|
.wrapper.focused {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -352,6 +358,10 @@
|
||||||
max-height: none;
|
max-height: none;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
.wrapper.invertY {
|
||||||
|
top: auto;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
min-height: var(--row-height);
|
min-height: var(--row-height);
|
||||||
|
@ -450,16 +460,17 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: calc(
|
max-height: calc(
|
||||||
var(--max-cell-render-height) + var(--row-height) -
|
var(--max-cell-render-height) + var(--row-height) - var(--values-height)
|
||||||
var(--max-relationship-height)
|
|
||||||
);
|
);
|
||||||
background: var(--background);
|
background: var(--grid-background-alt);
|
||||||
border: var(--cell-border);
|
border: var(--cell-border);
|
||||||
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
padding: 0 0 8px 0;
|
padding: 0 0 8px 0;
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
}
|
}
|
||||||
.dropdown.invertY {
|
.dropdown.invertY {
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
quiet
|
quiet
|
||||||
size="M"
|
size="M"
|
||||||
on:click={() => dispatch("add-column")}
|
on:click={() => dispatch("add-column")}
|
||||||
disabled={!$config.allowAddColumns}
|
disabled={!$config.allowSchemaChanges}
|
||||||
>
|
>
|
||||||
Add column
|
Add column
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
|
@ -6,15 +6,9 @@
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
$: selectedRowCount = Object.values($selectedRows).length
|
||||||
$: rowsToDelete = Object.entries($selectedRows)
|
$: rowsToDelete = Object.entries($selectedRows)
|
||||||
.map(entry => {
|
.map(entry => $rows.find(x => x._id === entry[0]))
|
||||||
if (entry[1] === true) {
|
|
||||||
return $rows.find(x => x._id === entry[0])
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(x => x != null)
|
.filter(x => x != null)
|
||||||
|
|
||||||
// Deletion callback when confirmed
|
// Deletion callback when confirmed
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { ActionButton, Popover } from "@budibase/bbui"
|
|
||||||
import { DefaultColumnWidth } from "../lib/constants"
|
|
||||||
|
|
||||||
const { stickyColumn, columns, compact } = getContext("grid")
|
|
||||||
const smallSize = 120
|
|
||||||
const mediumSize = DefaultColumnWidth
|
|
||||||
const largeSize = DefaultColumnWidth * 1.5
|
|
||||||
|
|
||||||
let open = false
|
|
||||||
let anchor
|
|
||||||
|
|
||||||
$: allCols = $columns.concat($stickyColumn ? [$stickyColumn] : [])
|
|
||||||
$: allSmall = allCols.every(col => col.width === smallSize)
|
|
||||||
$: allMedium = allCols.every(col => col.width === mediumSize)
|
|
||||||
$: allLarge = allCols.every(col => col.width === largeSize)
|
|
||||||
$: custom = !allSmall && !allMedium && !allLarge
|
|
||||||
$: sizeOptions = [
|
|
||||||
{
|
|
||||||
label: "Small",
|
|
||||||
size: smallSize,
|
|
||||||
selected: allSmall,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Medium",
|
|
||||||
size: mediumSize,
|
|
||||||
selected: allMedium,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Large",
|
|
||||||
size: largeSize,
|
|
||||||
selected: allLarge,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const changeColumnWidth = async width => {
|
|
||||||
columns.update(state => {
|
|
||||||
state.forEach(column => {
|
|
||||||
column.width = width
|
|
||||||
})
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
if ($stickyColumn) {
|
|
||||||
stickyColumn.update(state => ({
|
|
||||||
...state,
|
|
||||||
width,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
await columns.actions.saveChanges()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
|
||||||
<ActionButton
|
|
||||||
icon="MoveLeftRight"
|
|
||||||
quiet
|
|
||||||
size="M"
|
|
||||||
on:click={() => (open = !open)}
|
|
||||||
selected={open}
|
|
||||||
disabled={!allCols.length}
|
|
||||||
tooltip={$compact ? "Width" : null}
|
|
||||||
>
|
|
||||||
{$compact ? "" : "Width"}
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Popover bind:open {anchor} align={$compact ? "right" : "left"}>
|
|
||||||
<div class="content">
|
|
||||||
{#each sizeOptions as option}
|
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
on:click={() => changeColumnWidth(option.size)}
|
|
||||||
selected={option.selected}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</ActionButton>
|
|
||||||
{/each}
|
|
||||||
{#if custom}
|
|
||||||
<ActionButton selected={custom} quiet>Custom</ActionButton>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.content {
|
|
||||||
padding: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -3,12 +3,13 @@
|
||||||
import { ActionButton, Popover, Toggle, Icon } from "@budibase/bbui"
|
import { ActionButton, Popover, Toggle, Icon } from "@budibase/bbui"
|
||||||
import { getColumnIcon } from "../lib/utils"
|
import { getColumnIcon } from "../lib/utils"
|
||||||
|
|
||||||
const { columns, stickyColumn, compact } = getContext("grid")
|
const { columns, stickyColumn } = getContext("grid")
|
||||||
|
|
||||||
let open = false
|
let open = false
|
||||||
let anchor
|
let anchor
|
||||||
|
|
||||||
$: anyHidden = $columns.some(col => !col.visible)
|
$: anyHidden = $columns.some(col => !col.visible)
|
||||||
|
$: text = getText($columns)
|
||||||
|
|
||||||
const toggleVisibility = (column, visible) => {
|
const toggleVisibility = (column, visible) => {
|
||||||
columns.update(state => {
|
columns.update(state => {
|
||||||
|
@ -38,6 +39,11 @@
|
||||||
})
|
})
|
||||||
columns.actions.saveChanges()
|
columns.actions.saveChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getText = columns => {
|
||||||
|
const hidden = columns.filter(col => !col.visible).length
|
||||||
|
return hidden ? `Hide columns (${hidden})` : "Hide columns"
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
<div bind:this={anchor}>
|
||||||
|
@ -48,13 +54,12 @@
|
||||||
on:click={() => (open = !open)}
|
on:click={() => (open = !open)}
|
||||||
selected={open || anyHidden}
|
selected={open || anyHidden}
|
||||||
disabled={!$columns.length}
|
disabled={!$columns.length}
|
||||||
tooltip={$compact ? "Columns" : ""}
|
|
||||||
>
|
>
|
||||||
{$compact ? "" : "Columns"}
|
{text}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Popover bind:open {anchor} align={$compact ? "right" : "left"}>
|
<Popover bind:open {anchor} align="left">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
{#if $stickyColumn}
|
{#if $stickyColumn}
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { ActionButton, Popover } from "@budibase/bbui"
|
|
||||||
import {
|
|
||||||
LargeRowHeight,
|
|
||||||
MediumRowHeight,
|
|
||||||
SmallRowHeight,
|
|
||||||
} from "../lib/constants"
|
|
||||||
|
|
||||||
const { rowHeight, columns, table, compact } = getContext("grid")
|
|
||||||
const sizeOptions = [
|
|
||||||
{
|
|
||||||
label: "Small",
|
|
||||||
size: SmallRowHeight,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Medium",
|
|
||||||
size: MediumRowHeight,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Large",
|
|
||||||
size: LargeRowHeight,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
let open = false
|
|
||||||
let anchor
|
|
||||||
|
|
||||||
const changeRowHeight = height => {
|
|
||||||
columns.actions.saveTable({
|
|
||||||
...$table,
|
|
||||||
rowHeight: height,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
|
||||||
<ActionButton
|
|
||||||
icon="MoveUpDown"
|
|
||||||
quiet
|
|
||||||
size="M"
|
|
||||||
on:click={() => (open = !open)}
|
|
||||||
selected={open}
|
|
||||||
tooltip={$compact ? "Height" : null}
|
|
||||||
>
|
|
||||||
{$compact ? "" : "Height"}
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Popover bind:open {anchor} align={$compact ? "right" : "left"}>
|
|
||||||
<div class="content">
|
|
||||||
{#each sizeOptions as option}
|
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
selected={$rowHeight === option.size}
|
|
||||||
on:click={() => changeRowHeight(option.size)}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</ActionButton>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.content {
|
|
||||||
padding: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import { ActionButton, Popover, Label } from "@budibase/bbui"
|
||||||
|
import {
|
||||||
|
DefaultColumnWidth,
|
||||||
|
LargeRowHeight,
|
||||||
|
MediumRowHeight,
|
||||||
|
SmallRowHeight,
|
||||||
|
} from "../lib/constants"
|
||||||
|
|
||||||
|
const { stickyColumn, columns, rowHeight, table } = getContext("grid")
|
||||||
|
|
||||||
|
// Some constants for column width options
|
||||||
|
const smallColSize = 120
|
||||||
|
const mediumColSize = DefaultColumnWidth
|
||||||
|
const largeColSize = DefaultColumnWidth * 1.5
|
||||||
|
|
||||||
|
// Row height sizes
|
||||||
|
const rowSizeOptions = [
|
||||||
|
{
|
||||||
|
label: "Small",
|
||||||
|
size: SmallRowHeight,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Medium",
|
||||||
|
size: MediumRowHeight,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Large",
|
||||||
|
size: LargeRowHeight,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
let open = false
|
||||||
|
let anchor
|
||||||
|
|
||||||
|
// Column width sizes
|
||||||
|
$: allCols = $columns.concat($stickyColumn ? [$stickyColumn] : [])
|
||||||
|
$: allSmall = allCols.every(col => col.width === smallColSize)
|
||||||
|
$: allMedium = allCols.every(col => col.width === mediumColSize)
|
||||||
|
$: allLarge = allCols.every(col => col.width === largeColSize)
|
||||||
|
$: custom = !allSmall && !allMedium && !allLarge
|
||||||
|
$: columnSizeOptions = [
|
||||||
|
{
|
||||||
|
label: "Small",
|
||||||
|
size: smallColSize,
|
||||||
|
selected: allSmall,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Medium",
|
||||||
|
size: mediumColSize,
|
||||||
|
selected: allMedium,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Large",
|
||||||
|
size: largeColSize,
|
||||||
|
selected: allLarge,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const changeRowHeight = height => {
|
||||||
|
columns.actions.saveTable({
|
||||||
|
...$table,
|
||||||
|
rowHeight: height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={anchor}>
|
||||||
|
<ActionButton
|
||||||
|
icon="MoveUpDown"
|
||||||
|
quiet
|
||||||
|
size="M"
|
||||||
|
on:click={() => (open = !open)}
|
||||||
|
selected={open}
|
||||||
|
disabled={!allCols.length}
|
||||||
|
>
|
||||||
|
Size
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popover bind:open {anchor} align="left">
|
||||||
|
<div class="content">
|
||||||
|
<div class="size">
|
||||||
|
<Label>Row height</Label>
|
||||||
|
<div class="options">
|
||||||
|
{#each rowSizeOptions as option}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
selected={$rowHeight === option.size}
|
||||||
|
on:click={() => changeRowHeight(option.size)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</ActionButton>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="size">
|
||||||
|
<Label>Column width</Label>
|
||||||
|
<div class="options">
|
||||||
|
{#each columnSizeOptions as option}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
on:click={() => columns.actions.changeAllColumnWidths(option.size)}
|
||||||
|
selected={option.selected}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</ActionButton>
|
||||||
|
{/each}
|
||||||
|
{#if custom}
|
||||||
|
<ActionButton selected={custom} quiet>Custom</ActionButton>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.size {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.size:first-child {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,7 +2,7 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { ActionButton, Popover, Select } from "@budibase/bbui"
|
import { ActionButton, Popover, Select } from "@budibase/bbui"
|
||||||
|
|
||||||
const { sort, columns, stickyColumn, compact } = getContext("grid")
|
const { sort, columns, stickyColumn } = getContext("grid")
|
||||||
|
|
||||||
let open = false
|
let open = false
|
||||||
let anchor
|
let anchor
|
||||||
|
@ -90,13 +90,12 @@
|
||||||
on:click={() => (open = !open)}
|
on:click={() => (open = !open)}
|
||||||
selected={open}
|
selected={open}
|
||||||
disabled={!columnOptions.length}
|
disabled={!columnOptions.length}
|
||||||
tooltip={$compact ? "Sort" : ""}
|
|
||||||
>
|
>
|
||||||
{$compact ? "" : "Sort"}
|
Sort
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Popover bind:open {anchor} align={$compact ? "right" : "left"}>
|
<Popover bind:open {anchor} align="left">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<Select
|
<Select
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { setContext, onMount } from "svelte"
|
import { setContext, onMount } from "svelte"
|
||||||
import { writable } from "svelte/store"
|
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import { clickOutside, ProgressCircle } from "@budibase/bbui"
|
import { clickOutside, ProgressCircle } from "@budibase/bbui"
|
||||||
import { createEventManagers } from "../lib/events"
|
import { createEventManagers } from "../lib/events"
|
||||||
|
@ -17,11 +16,8 @@
|
||||||
import UserAvatars from "./UserAvatars.svelte"
|
import UserAvatars from "./UserAvatars.svelte"
|
||||||
import KeyboardManager from "../overlays/KeyboardManager.svelte"
|
import KeyboardManager from "../overlays/KeyboardManager.svelte"
|
||||||
import SortButton from "../controls/SortButton.svelte"
|
import SortButton from "../controls/SortButton.svelte"
|
||||||
import AddColumnButton from "../controls/AddColumnButton.svelte"
|
|
||||||
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
|
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
|
||||||
import AddRowButton from "../controls/AddRowButton.svelte"
|
import SizeButton from "../controls/SizeButton.svelte"
|
||||||
import RowHeightButton from "../controls/RowHeightButton.svelte"
|
|
||||||
import ColumnWidthButton from "../controls/ColumnWidthButton.svelte"
|
|
||||||
import NewRow from "./NewRow.svelte"
|
import NewRow from "./NewRow.svelte"
|
||||||
import { createGridWebsocket } from "../lib/websocket"
|
import { createGridWebsocket } from "../lib/websocket"
|
||||||
import {
|
import {
|
||||||
|
@ -33,48 +29,37 @@
|
||||||
|
|
||||||
export let API = null
|
export let API = null
|
||||||
export let tableId = null
|
export let tableId = null
|
||||||
export let tableType = null
|
|
||||||
export let schemaOverrides = null
|
export let schemaOverrides = null
|
||||||
|
export let columnWhitelist = null
|
||||||
export let allowAddRows = true
|
export let allowAddRows = true
|
||||||
export let allowAddColumns = true
|
|
||||||
export let allowEditColumns = true
|
|
||||||
export let allowExpandRows = true
|
export let allowExpandRows = true
|
||||||
export let allowEditRows = true
|
export let allowEditRows = true
|
||||||
export let allowDeleteRows = true
|
export let allowDeleteRows = true
|
||||||
|
export let allowSchemaChanges = true
|
||||||
export let stripeRows = false
|
export let stripeRows = false
|
||||||
export let collaboration = true
|
export let collaboration = true
|
||||||
export let showAvatars = true
|
export let showAvatars = true
|
||||||
|
export let showControls = true
|
||||||
|
export let initialFilter = null
|
||||||
|
export let initialSortColumn = null
|
||||||
|
export let initialSortOrder = null
|
||||||
|
export let initialRowHeight = null
|
||||||
|
|
||||||
// Unique identifier for DOM nodes inside this instance
|
// Unique identifier for DOM nodes inside this instance
|
||||||
const rand = Math.random()
|
const rand = Math.random()
|
||||||
|
|
||||||
// State stores
|
|
||||||
const tableIdStore = writable(tableId)
|
|
||||||
const schemaOverridesStore = writable(schemaOverrides)
|
|
||||||
const config = writable({
|
|
||||||
allowAddRows,
|
|
||||||
allowAddColumns,
|
|
||||||
allowEditColumns,
|
|
||||||
allowExpandRows,
|
|
||||||
allowEditRows,
|
|
||||||
allowDeleteRows,
|
|
||||||
stripeRows,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Build up context
|
// Build up context
|
||||||
let context = {
|
let context = {
|
||||||
API: API || createAPIClient(),
|
API: API || createAPIClient(),
|
||||||
rand,
|
rand,
|
||||||
config,
|
props: $$props,
|
||||||
tableId: tableIdStore,
|
|
||||||
tableType,
|
|
||||||
schemaOverrides: schemaOverridesStore,
|
|
||||||
}
|
}
|
||||||
context = { ...context, ...createEventManagers() }
|
context = { ...context, ...createEventManagers() }
|
||||||
context = attachStores(context)
|
context = attachStores(context)
|
||||||
|
|
||||||
// Reference some stores for local use
|
// Reference some stores for local use
|
||||||
const {
|
const {
|
||||||
|
config,
|
||||||
isResizing,
|
isResizing,
|
||||||
isReordering,
|
isReordering,
|
||||||
ui,
|
ui,
|
||||||
|
@ -82,19 +67,27 @@
|
||||||
loading,
|
loading,
|
||||||
rowHeight,
|
rowHeight,
|
||||||
contentLines,
|
contentLines,
|
||||||
|
gridFocused,
|
||||||
} = context
|
} = context
|
||||||
|
|
||||||
// Keep stores up to date
|
// Keep config store up to date with props
|
||||||
$: tableIdStore.set(tableId)
|
|
||||||
$: schemaOverridesStore.set(schemaOverrides)
|
|
||||||
$: config.set({
|
$: config.set({
|
||||||
|
tableId,
|
||||||
|
schemaOverrides,
|
||||||
|
columnWhitelist,
|
||||||
allowAddRows,
|
allowAddRows,
|
||||||
allowAddColumns,
|
|
||||||
allowEditColumns,
|
|
||||||
allowExpandRows,
|
allowExpandRows,
|
||||||
allowEditRows,
|
allowEditRows,
|
||||||
allowDeleteRows,
|
allowDeleteRows,
|
||||||
|
allowSchemaChanges,
|
||||||
stripeRows,
|
stripeRows,
|
||||||
|
collaboration,
|
||||||
|
showAvatars,
|
||||||
|
showControls,
|
||||||
|
initialFilter,
|
||||||
|
initialSortColumn,
|
||||||
|
initialSortOrder,
|
||||||
|
initialRowHeight,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set context for children to consume
|
// Set context for children to consume
|
||||||
|
@ -116,25 +109,27 @@
|
||||||
id="grid-{rand}"
|
id="grid-{rand}"
|
||||||
class:is-resizing={$isResizing}
|
class:is-resizing={$isResizing}
|
||||||
class:is-reordering={$isReordering}
|
class:is-reordering={$isReordering}
|
||||||
class:stripe={$config.stripeRows}
|
class:stripe={stripeRows}
|
||||||
|
on:mouseenter={() => gridFocused.set(true)}
|
||||||
|
on:mouseleave={() => gridFocused.set(false)}
|
||||||
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};"
|
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};"
|
||||||
>
|
>
|
||||||
<div class="controls">
|
{#if showControls}
|
||||||
<div class="controls-left">
|
<div class="controls">
|
||||||
<AddRowButton />
|
<div class="controls-left">
|
||||||
<AddColumnButton />
|
<slot name="filter" />
|
||||||
<slot name="controls" />
|
<SortButton />
|
||||||
<SortButton />
|
<HideColumnsButton />
|
||||||
<HideColumnsButton />
|
<SizeButton />
|
||||||
<ColumnWidthButton />
|
<slot name="controls" />
|
||||||
<RowHeightButton />
|
</div>
|
||||||
|
<div class="controls-right">
|
||||||
|
{#if showAvatars}
|
||||||
|
<UserAvatars />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-right">
|
{/if}
|
||||||
{#if showAvatars}
|
|
||||||
<UserAvatars />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if $loaded}
|
{#if $loaded}
|
||||||
<div class="grid-data-outer" use:clickOutside={ui.actions.blur}>
|
<div class="grid-data-outer" use:clickOutside={ui.actions.blur}>
|
||||||
<div class="grid-data-inner">
|
<div class="grid-data-inner">
|
||||||
|
@ -167,7 +162,20 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* Core grid */
|
||||||
.grid {
|
.grid {
|
||||||
|
/* Variables */
|
||||||
|
--accent-color: var(--primaryColor, var(--spectrum-global-color-blue-400));
|
||||||
|
--grid-background: var(--spectrum-global-color-gray-50);
|
||||||
|
--grid-background-alt: var(--spectrum-global-color-gray-100);
|
||||||
|
--cell-background: var(--grid-background);
|
||||||
|
--cell-background-hover: var(--grid-background-alt);
|
||||||
|
--cell-background-alt: var(--cell-background);
|
||||||
|
--cell-padding: 8px;
|
||||||
|
--cell-spacing: 4px;
|
||||||
|
--cell-border: 1px solid var(--spectrum-global-color-gray-200);
|
||||||
|
--cell-font-size: 14px;
|
||||||
|
--controls-height: 50px;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -175,17 +183,7 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--cell-background);
|
background: var(--grid-background);
|
||||||
|
|
||||||
/* Variables */
|
|
||||||
--cell-background: var(--spectrum-global-color-gray-50);
|
|
||||||
--cell-background-hover: var(--spectrum-global-color-gray-100);
|
|
||||||
--cell-background-alt: var(--cell-background);
|
|
||||||
--cell-padding: 8px;
|
|
||||||
--cell-spacing: 4px;
|
|
||||||
--cell-border: 1px solid var(--spectrum-global-color-gray-200);
|
|
||||||
--cell-font-size: 14px;
|
|
||||||
--controls-height: 50px;
|
|
||||||
}
|
}
|
||||||
.grid,
|
.grid,
|
||||||
.grid :global(*) {
|
.grid :global(*) {
|
||||||
|
@ -201,6 +199,7 @@
|
||||||
--cell-background-alt: var(--spectrum-global-color-gray-75);
|
--cell-background-alt: var(--spectrum-global-color-gray-75);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Data layers */
|
||||||
.grid-data-outer,
|
.grid-data-outer,
|
||||||
.grid-data-inner {
|
.grid-data-inner {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
@ -234,7 +233,7 @@
|
||||||
border-bottom: 2px solid var(--spectrum-global-color-gray-200);
|
border-bottom: 2px solid var(--spectrum-global-color-gray-200);
|
||||||
padding: var(--cell-padding);
|
padding: var(--cell-padding);
|
||||||
gap: var(--cell-spacing);
|
gap: var(--cell-spacing);
|
||||||
background: var(--background);
|
background: var(--grid-background-alt);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.controls-left,
|
.controls-left,
|
||||||
|
@ -270,7 +269,15 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--background);
|
background: var(--grid-background-alt);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Disable checkbox animation anywhere in the grid data */
|
||||||
|
.grid-data-outer :global(.spectrum-Checkbox-box:before),
|
||||||
|
.grid-data-outer :global(.spectrum-Checkbox-box:after),
|
||||||
|
.grid-data-outer :global(.spectrum-Checkbox-checkmark),
|
||||||
|
.grid-data-outer :global(.spectrum-Checkbox-partialCheckmark) {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
config,
|
config,
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
isDragging,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
let body
|
let body
|
||||||
|
@ -47,8 +48,8 @@
|
||||||
class="blank"
|
class="blank"
|
||||||
class:highlighted={$hoveredRowId === BlankRowID}
|
class:highlighted={$hoveredRowId === BlankRowID}
|
||||||
style="width:{renderColumnsWidth}px"
|
style="width:{renderColumnsWidth}px"
|
||||||
on:mouseenter={() => ($hoveredRowId = BlankRowID)}
|
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = BlankRowID)}
|
||||||
on:mouseleave={() => ($hoveredRowId = null)}
|
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||||
on:click={() => dispatch("add-row-inline")}
|
on:click={() => dispatch("add-row-inline")}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
focusedRow,
|
focusedRow,
|
||||||
columnHorizontalInversionIndex,
|
columnHorizontalInversionIndex,
|
||||||
contentLines,
|
contentLines,
|
||||||
|
isDragging,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
$: rowSelected = !!$selectedRows[row._id]
|
$: rowSelected = !!$selectedRows[row._id]
|
||||||
|
@ -27,8 +28,8 @@
|
||||||
<div
|
<div
|
||||||
class="row"
|
class="row"
|
||||||
on:focus
|
on:focus
|
||||||
on:mouseenter={() => ($hoveredRowId = row._id)}
|
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||||
on:mouseleave={() => ($hoveredRowId = null)}
|
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||||
>
|
>
|
||||||
{#each $renderedColumns as column, columnIdx (column.name)}
|
{#each $renderedColumns as column, columnIdx (column.name)}
|
||||||
{@const cellId = `${row._id}-${column.name}`}
|
{@const cellId = `${row._id}-${column.name}`}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
bounds,
|
bounds,
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
hiddenColumnsWidth,
|
hiddenColumnsWidth,
|
||||||
|
menu,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
export let scrollVertically = false
|
export let scrollVertically = false
|
||||||
|
@ -30,6 +31,11 @@
|
||||||
const handleWheel = e => {
|
const handleWheel = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
debouncedHandleWheel(e.deltaX, e.deltaY, e.clientY)
|
debouncedHandleWheel(e.deltaX, e.deltaY, e.clientY)
|
||||||
|
|
||||||
|
// If a context menu was visible, hide it
|
||||||
|
if ($menu.visible) {
|
||||||
|
menu.actions.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
|
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
|
||||||
const { top, left } = $scroll
|
const { top, left } = $scroll
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</GridScrollWrapper>
|
</GridScrollWrapper>
|
||||||
{#if $config.allowAddColumns}
|
{#if $config.allowSchemaChanges}
|
||||||
<div
|
<div
|
||||||
class="add"
|
class="add"
|
||||||
style="left:{left}px"
|
style="left:{left}px"
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.header {
|
.header {
|
||||||
background: var(--background);
|
background: var(--grid-background-alt);
|
||||||
border-bottom: var(--cell-border);
|
border-bottom: var(--cell-border);
|
||||||
position: relative;
|
position: relative;
|
||||||
height: var(--default-row-height);
|
height: var(--default-row-height);
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
border-left: var(--cell-border);
|
border-left: var(--cell-border);
|
||||||
border-right: var(--cell-border);
|
border-right: var(--cell-border);
|
||||||
border-bottom: var(--cell-border);
|
border-bottom: var(--cell-border);
|
||||||
background: var(--spectrum-global-color-gray-100);
|
background: var(--grid-background-alt);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.add:hover {
|
.add:hover {
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
maxScrollTop,
|
maxScrollTop,
|
||||||
rowVerticalInversionIndex,
|
rowVerticalInversionIndex,
|
||||||
columnHorizontalInversionIndex,
|
columnHorizontalInversionIndex,
|
||||||
|
selectedRows,
|
||||||
|
config,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
let visible = false
|
let visible = false
|
||||||
|
@ -37,6 +39,7 @@
|
||||||
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
||||||
$: $tableId, (visible = false)
|
$: $tableId, (visible = false)
|
||||||
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
|
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
|
||||||
|
$: selectedRowCount = Object.values($selectedRows).length
|
||||||
|
|
||||||
const shouldInvertY = (offset, inversionIndex, rows) => {
|
const shouldInvertY = (offset, inversionIndex, rows) => {
|
||||||
if (offset === 0) {
|
if (offset === 0) {
|
||||||
|
@ -75,7 +78,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const startAdding = async () => {
|
const startAdding = async () => {
|
||||||
if (visible) {
|
if (visible || !firstColumn) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,9 +132,6 @@
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
clear()
|
clear()
|
||||||
}
|
}
|
||||||
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
||||||
e.preventDefault()
|
|
||||||
addRow()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,6 +141,18 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- New row FAB -->
|
||||||
|
{#if !visible && !selectedRowCount && $config.allowAddRows && firstColumn}
|
||||||
|
<div
|
||||||
|
class="new-row-fab"
|
||||||
|
on:click={() => dispatch("add-row-inline")}
|
||||||
|
transition:fade|local={{ duration: 130 }}
|
||||||
|
class:offset={!$stickyColumn}
|
||||||
|
>
|
||||||
|
<Icon name="Add" size="S" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Only show new row functionality if we have any columns -->
|
<!-- Only show new row functionality if we have any columns -->
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<div
|
<div
|
||||||
|
@ -151,7 +163,7 @@
|
||||||
<div class="underlay sticky" transition:fade|local={{ duration: 130 }} />
|
<div class="underlay sticky" transition:fade|local={{ duration: 130 }} />
|
||||||
<div class="underlay" transition:fade|local={{ duration: 130 }} />
|
<div class="underlay" transition:fade|local={{ duration: 130 }} />
|
||||||
<div class="sticky-column" transition:fade|local={{ duration: 130 }}>
|
<div class="sticky-column" transition:fade|local={{ duration: 130 }}>
|
||||||
<GutterCell on:expand={addViaModal} rowHovered>
|
<GutterCell expandable on:expand={addViaModal} rowHovered>
|
||||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||||
{#if isAdding}
|
{#if isAdding}
|
||||||
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
||||||
|
@ -227,6 +239,26 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* New row FAB */
|
||||||
|
.new-row-fab {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--default-row-height);
|
||||||
|
left: calc(var(--gutter-width) / 2);
|
||||||
|
transform: translateX(6px) translateY(-50%);
|
||||||
|
background: var(--cell-background);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: var(--cell-border);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.new-row-fab:hover {
|
||||||
|
background: var(--cell-background-hover);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.new-row-fab.offset {
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--default-row-height);
|
top: var(--default-row-height);
|
||||||
|
|
|
@ -23,10 +23,11 @@
|
||||||
scrollLeft,
|
scrollLeft,
|
||||||
dispatch,
|
dispatch,
|
||||||
contentLines,
|
contentLines,
|
||||||
|
isDragging,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
$: rowCount = $rows.length
|
$: rowCount = $rows.length
|
||||||
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
$: selectedRowCount = Object.values($selectedRows).length
|
||||||
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
||||||
|
|
||||||
const selectAll = () => {
|
const selectAll = () => {
|
||||||
|
@ -50,7 +51,6 @@
|
||||||
>
|
>
|
||||||
<div class="header row">
|
<div class="header row">
|
||||||
<GutterCell
|
<GutterCell
|
||||||
disableExpand
|
|
||||||
disableNumber
|
disableNumber
|
||||||
on:select={selectAll}
|
on:select={selectAll}
|
||||||
defaultHeight
|
defaultHeight
|
||||||
|
@ -71,8 +71,8 @@
|
||||||
{@const cellId = `${row._id}-${$stickyColumn?.name}`}
|
{@const cellId = `${row._id}-${$stickyColumn?.name}`}
|
||||||
<div
|
<div
|
||||||
class="row"
|
class="row"
|
||||||
on:mouseenter={() => ($hoveredRowId = row._id)}
|
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||||
on:mouseleave={() => ($hoveredRowId = null)}
|
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||||
>
|
>
|
||||||
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
||||||
{#if $stickyColumn}
|
{#if $stickyColumn}
|
||||||
|
@ -96,11 +96,13 @@
|
||||||
{#if $config.allowAddRows && ($renderedColumns.length || $stickyColumn)}
|
{#if $config.allowAddRows && ($renderedColumns.length || $stickyColumn)}
|
||||||
<div
|
<div
|
||||||
class="row new"
|
class="row new"
|
||||||
on:mouseenter={() => ($hoveredRowId = BlankRowID)}
|
on:mouseenter={$isDragging
|
||||||
on:mouseleave={() => ($hoveredRowId = null)}
|
? null
|
||||||
|
: () => ($hoveredRowId = BlankRowID)}
|
||||||
|
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||||
on:click={() => dispatch("add-row-inline")}
|
on:click={() => dispatch("add-row-inline")}
|
||||||
>
|
>
|
||||||
<GutterCell disableExpand rowHovered={$hoveredRowId === BlankRowID}>
|
<GutterCell rowHovered={$hoveredRowId === BlankRowID}>
|
||||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||||
</GutterCell>
|
</GutterCell>
|
||||||
{#if $stickyColumn}
|
{#if $stickyColumn}
|
||||||
|
@ -159,7 +161,7 @@
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.header :global(.cell) {
|
.header :global(.cell) {
|
||||||
background: var(--spectrum-global-color-gray-100);
|
background: var(--grid-background-alt);
|
||||||
}
|
}
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
export const Padding = 256
|
export const Padding = 246
|
||||||
export const MaxCellRenderHeight = 252
|
export const MaxCellRenderHeight = 222
|
||||||
export const MaxCellRenderWidthOverflow = 200
|
|
||||||
export const ScrollBarSize = 8
|
export const ScrollBarSize = 8
|
||||||
export const GutterWidth = 72
|
export const GutterWidth = 72
|
||||||
export const DefaultColumnWidth = 200
|
export const DefaultColumnWidth = 200
|
||||||
|
@ -12,3 +11,5 @@ export const DefaultRowHeight = SmallRowHeight
|
||||||
export const NewRowID = "new"
|
export const NewRowID = "new"
|
||||||
export const BlankRowID = "blank"
|
export const BlankRowID = "blank"
|
||||||
export const RowPageSize = 100
|
export const RowPageSize = 100
|
||||||
|
export const FocusedCellMinOffset = 48
|
||||||
|
export const MaxCellRenderWidthOverflow = Padding - 3 * ScrollBarSize
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { createWebsocket } from "../../../utils"
|
||||||
import { SocketEvent, GridSocketEvent } from "@budibase/shared-core"
|
import { SocketEvent, GridSocketEvent } from "@budibase/shared-core"
|
||||||
|
|
||||||
export const createGridWebsocket = context => {
|
export const createGridWebsocket = context => {
|
||||||
const { rows, tableId, users, focusedCellId, table } = context
|
const { rows, tableId, users, focusedCellId, table, API } = context
|
||||||
const socket = createWebsocket("/socket/grid")
|
const socket = createWebsocket("/socket/grid")
|
||||||
|
|
||||||
const connectToTable = tableId => {
|
const connectToTable = tableId => {
|
||||||
|
@ -11,9 +11,10 @@ export const createGridWebsocket = context => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Identify which table we are editing
|
// Identify which table we are editing
|
||||||
|
const appId = API.getAppID()
|
||||||
socket.emit(
|
socket.emit(
|
||||||
GridSocketEvent.SelectTable,
|
GridSocketEvent.SelectTable,
|
||||||
{ tableId },
|
{ tableId, appId },
|
||||||
({ users: gridUsers }) => {
|
({ users: gridUsers }) => {
|
||||||
users.set(gridUsers)
|
users.set(gridUsers)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
selectedRows,
|
selectedRows,
|
||||||
config,
|
config,
|
||||||
menu,
|
menu,
|
||||||
|
gridFocused,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
const ignoredOriginSelectors = [
|
const ignoredOriginSelectors = [
|
||||||
|
@ -24,6 +25,11 @@
|
||||||
|
|
||||||
// Global key listener which intercepts all key events
|
// Global key listener which intercepts all key events
|
||||||
const handleKeyDown = e => {
|
const handleKeyDown = e => {
|
||||||
|
// Ignore completely if the grid is not focused
|
||||||
|
if (!$gridFocused) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Avoid processing events sourced from certain origins
|
// Avoid processing events sourced from certain origins
|
||||||
if (e.target?.closest) {
|
if (e.target?.closest) {
|
||||||
for (let selector of ignoredOriginSelectors) {
|
for (let selector of ignoredOriginSelectors) {
|
||||||
|
|
|
@ -72,7 +72,9 @@
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="Maximize"
|
icon="Maximize"
|
||||||
disabled={isNewRow || !$config.allowEditRows}
|
disabled={isNewRow ||
|
||||||
|
!$config.allowEditRows ||
|
||||||
|
!$config.allowExpandRows}
|
||||||
on:click={() => dispatch("edit-row", $focusedRow)}
|
on:click={() => dispatch("edit-row", $focusedRow)}
|
||||||
on:click={menu.actions.close}
|
on:click={menu.actions.close}
|
||||||
>
|
>
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
background: var(--spectrum-global-color-blue-400);
|
background: var(--accent-color);
|
||||||
margin-left: -2px;
|
margin-left: -2px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -65,6 +65,6 @@
|
||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--spectrum-global-color-blue-400);
|
background: var(--accent-color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -15,11 +15,18 @@
|
||||||
scrollLeft,
|
scrollLeft,
|
||||||
scrollTop,
|
scrollTop,
|
||||||
height,
|
height,
|
||||||
|
isDragging,
|
||||||
|
menu,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
// State for dragging bars
|
// State for dragging bars
|
||||||
let initialMouse
|
let initialMouse
|
||||||
let initialScroll
|
let initialScroll
|
||||||
|
let isDraggingV = false
|
||||||
|
let isDraggingH = false
|
||||||
|
|
||||||
|
// Update state to reflect if we are dragging
|
||||||
|
$: isDragging.set(isDraggingV || isDraggingH)
|
||||||
|
|
||||||
// Calculate V scrollbar size and offset
|
// Calculate V scrollbar size and offset
|
||||||
// Terminology is the same for both axes:
|
// Terminology is the same for both axes:
|
||||||
|
@ -39,6 +46,13 @@
|
||||||
$: availWidth = renderWidth - barWidth
|
$: availWidth = renderWidth - barWidth
|
||||||
$: barLeft = ScrollBarSize + availWidth * ($scrollLeft / $maxScrollLeft)
|
$: barLeft = ScrollBarSize + availWidth * ($scrollLeft / $maxScrollLeft)
|
||||||
|
|
||||||
|
// Helper to close the context menu if it's open
|
||||||
|
const closeMenu = () => {
|
||||||
|
if ($menu.visible) {
|
||||||
|
menu.actions.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// V scrollbar drag handlers
|
// V scrollbar drag handlers
|
||||||
const startVDragging = e => {
|
const startVDragging = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
@ -46,6 +60,8 @@
|
||||||
initialScroll = $scrollTop
|
initialScroll = $scrollTop
|
||||||
document.addEventListener("mousemove", moveVDragging)
|
document.addEventListener("mousemove", moveVDragging)
|
||||||
document.addEventListener("mouseup", stopVDragging)
|
document.addEventListener("mouseup", stopVDragging)
|
||||||
|
isDraggingV = true
|
||||||
|
closeMenu()
|
||||||
}
|
}
|
||||||
const moveVDragging = domDebounce(e => {
|
const moveVDragging = domDebounce(e => {
|
||||||
const delta = e.clientY - initialMouse
|
const delta = e.clientY - initialMouse
|
||||||
|
@ -59,6 +75,7 @@
|
||||||
const stopVDragging = () => {
|
const stopVDragging = () => {
|
||||||
document.removeEventListener("mousemove", moveVDragging)
|
document.removeEventListener("mousemove", moveVDragging)
|
||||||
document.removeEventListener("mouseup", stopVDragging)
|
document.removeEventListener("mouseup", stopVDragging)
|
||||||
|
isDraggingV = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// H scrollbar drag handlers
|
// H scrollbar drag handlers
|
||||||
|
@ -68,6 +85,8 @@
|
||||||
initialScroll = $scrollLeft
|
initialScroll = $scrollLeft
|
||||||
document.addEventListener("mousemove", moveHDragging)
|
document.addEventListener("mousemove", moveHDragging)
|
||||||
document.addEventListener("mouseup", stopHDragging)
|
document.addEventListener("mouseup", stopHDragging)
|
||||||
|
isDraggingH = true
|
||||||
|
closeMenu()
|
||||||
}
|
}
|
||||||
const moveHDragging = domDebounce(e => {
|
const moveHDragging = domDebounce(e => {
|
||||||
const delta = e.clientX - initialMouse
|
const delta = e.clientX - initialMouse
|
||||||
|
@ -81,6 +100,7 @@
|
||||||
const stopHDragging = () => {
|
const stopHDragging = () => {
|
||||||
document.removeEventListener("mousemove", moveHDragging)
|
document.removeEventListener("mousemove", moveHDragging)
|
||||||
document.removeEventListener("mouseup", stopHDragging)
|
document.removeEventListener("mouseup", stopHDragging)
|
||||||
|
isDraggingH = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -89,6 +109,7 @@
|
||||||
class="v-scrollbar"
|
class="v-scrollbar"
|
||||||
style="--size:{ScrollBarSize}px; top:{barTop}px; height:{barHeight}px;"
|
style="--size:{ScrollBarSize}px; top:{barTop}px; height:{barHeight}px;"
|
||||||
on:mousedown={startVDragging}
|
on:mousedown={startVDragging}
|
||||||
|
class:dragging={isDraggingV}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $showHScrollbar}
|
{#if $showHScrollbar}
|
||||||
|
@ -96,6 +117,7 @@
|
||||||
class="h-scrollbar"
|
class="h-scrollbar"
|
||||||
style="--size:{ScrollBarSize}px; left:{barLeft}px; width:{barWidth}px;"
|
style="--size:{ScrollBarSize}px; left:{barLeft}px; width:{barWidth}px;"
|
||||||
on:mousedown={startHDragging}
|
on:mousedown={startHDragging}
|
||||||
|
class:dragging={isDraggingH}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -103,11 +125,12 @@
|
||||||
div {
|
div {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: var(--spectrum-global-color-gray-500);
|
background: var(--spectrum-global-color-gray-500);
|
||||||
opacity: 0.7;
|
opacity: 0.5;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: opacity 130ms ease-out;
|
transition: opacity 130ms ease-out;
|
||||||
}
|
}
|
||||||
div:hover {
|
div:hover,
|
||||||
|
div.dragging {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.v-scrollbar {
|
.v-scrollbar {
|
||||||
|
|
|
@ -46,7 +46,7 @@ export const createStores = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deriveStores = context => {
|
export const deriveStores = context => {
|
||||||
const { table, columns, stickyColumn, API, dispatch } = context
|
const { table, columns, stickyColumn, API, dispatch, config } = context
|
||||||
|
|
||||||
// Updates the tables primary display column
|
// Updates the tables primary display column
|
||||||
const changePrimaryDisplay = async column => {
|
const changePrimaryDisplay = async column => {
|
||||||
|
@ -56,6 +56,23 @@ export const deriveStores = context => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updates the width of all columns
|
||||||
|
const changeAllColumnWidths = async width => {
|
||||||
|
columns.update(state => {
|
||||||
|
return state.map(col => ({
|
||||||
|
...col,
|
||||||
|
width,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
if (get(stickyColumn)) {
|
||||||
|
stickyColumn.update(state => ({
|
||||||
|
...state,
|
||||||
|
width,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
await saveChanges()
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
|
@ -91,7 +108,9 @@ export const deriveStores = context => {
|
||||||
table.set(newTable)
|
table.set(newTable)
|
||||||
|
|
||||||
// Update server
|
// Update server
|
||||||
await API.saveTable(newTable)
|
if (get(config).allowSchemaChanges) {
|
||||||
|
await API.saveTable(newTable)
|
||||||
|
}
|
||||||
|
|
||||||
// Broadcast change to external state can be updated, as this change
|
// Broadcast change to external state can be updated, as this change
|
||||||
// will not be received by the builder websocket because we caused it ourselves
|
// will not be received by the builder websocket because we caused it ourselves
|
||||||
|
@ -105,17 +124,19 @@ export const deriveStores = context => {
|
||||||
saveChanges,
|
saveChanges,
|
||||||
saveTable,
|
saveTable,
|
||||||
changePrimaryDisplay,
|
changePrimaryDisplay,
|
||||||
|
changeAllColumnWidths,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialise = context => {
|
export const initialise = context => {
|
||||||
const { table, columns, stickyColumn, schemaOverrides } = context
|
const { table, columns, stickyColumn, schemaOverrides, columnWhitelist } =
|
||||||
|
context
|
||||||
|
|
||||||
const schema = derived(
|
const schema = derived(
|
||||||
[table, schemaOverrides],
|
[table, schemaOverrides, columnWhitelist],
|
||||||
([$table, $schemaOverrides]) => {
|
([$table, $schemaOverrides, $columnWhitelist]) => {
|
||||||
if (!$table?.schema) {
|
if (!$table?.schema) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -142,6 +163,16 @@ export const initialise = context => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Apply whitelist if specified
|
||||||
|
if ($columnWhitelist?.length) {
|
||||||
|
Object.keys(newSchema).forEach(key => {
|
||||||
|
if (!$columnWhitelist.includes(key)) {
|
||||||
|
delete newSchema[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return newSchema
|
return newSchema
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -209,7 +240,7 @@ export const initialise = context => {
|
||||||
}
|
}
|
||||||
stickyColumn.set({
|
stickyColumn.set({
|
||||||
name: primaryDisplay,
|
name: primaryDisplay,
|
||||||
label: $schema[primaryDisplay].name || primaryDisplay,
|
label: $schema[primaryDisplay].displayName || primaryDisplay,
|
||||||
schema: $schema[primaryDisplay],
|
schema: $schema[primaryDisplay],
|
||||||
width: $schema[primaryDisplay].width || DefaultColumnWidth,
|
width: $schema[primaryDisplay].width || DefaultColumnWidth,
|
||||||
visible: true,
|
visible: true,
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { derivedMemo } from "../../../utils"
|
||||||
|
|
||||||
|
export const createStores = context => {
|
||||||
|
const config = writable(context.props)
|
||||||
|
const getProp = prop => derivedMemo(config, $config => $config[prop])
|
||||||
|
|
||||||
|
// Derive and memoize some props so that we can react to them in isolation
|
||||||
|
const tableId = getProp("tableId")
|
||||||
|
const initialSortColumn = getProp("initialSortColumn")
|
||||||
|
const initialSortOrder = getProp("initialSortOrder")
|
||||||
|
const initialFilter = getProp("initialFilter")
|
||||||
|
const initialRowHeight = getProp("initialRowHeight")
|
||||||
|
const schemaOverrides = getProp("schemaOverrides")
|
||||||
|
const columnWhitelist = getProp("columnWhitelist")
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
tableId,
|
||||||
|
initialSortColumn,
|
||||||
|
initialSortOrder,
|
||||||
|
initialFilter,
|
||||||
|
initialRowHeight,
|
||||||
|
schemaOverrides,
|
||||||
|
columnWhitelist,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
export const createStores = context => {
|
||||||
|
const { props } = context
|
||||||
|
|
||||||
|
// Initialise to default props
|
||||||
|
const filter = writable(props.initialFilter)
|
||||||
|
|
||||||
|
return {
|
||||||
|
filter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialise = context => {
|
||||||
|
const { filter, initialFilter } = context
|
||||||
|
|
||||||
|
// Reset filter when initial filter prop changes
|
||||||
|
initialFilter.subscribe(filter.set)
|
||||||
|
}
|
|
@ -11,8 +11,14 @@ import * as Users from "./users"
|
||||||
import * as Validation from "./validation"
|
import * as Validation from "./validation"
|
||||||
import * as Viewport from "./viewport"
|
import * as Viewport from "./viewport"
|
||||||
import * as Clipboard from "./clipboard"
|
import * as Clipboard from "./clipboard"
|
||||||
|
import * as Config from "./config"
|
||||||
|
import * as Sort from "./sort"
|
||||||
|
import * as Filter from "./filter"
|
||||||
|
|
||||||
const DependencyOrderedStores = [
|
const DependencyOrderedStores = [
|
||||||
|
Config,
|
||||||
|
Sort,
|
||||||
|
Filter,
|
||||||
Bounds,
|
Bounds,
|
||||||
Scroll,
|
Scroll,
|
||||||
Rows,
|
Rows,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import { GutterWidth } from "../lib/constants"
|
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
const menu = writable({
|
const menu = writable({
|
||||||
|
@ -14,18 +13,25 @@ export const createStores = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deriveStores = context => {
|
export const deriveStores = context => {
|
||||||
const { menu, bounds, focusedCellId, stickyColumn, rowHeight } = context
|
const { menu, focusedCellId, rand } = context
|
||||||
|
|
||||||
const open = (cellId, e) => {
|
const open = (cellId, e) => {
|
||||||
const $bounds = get(bounds)
|
|
||||||
const $stickyColumn = get(stickyColumn)
|
|
||||||
const $rowHeight = get(rowHeight)
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Get DOM node for grid data wrapper to compute relative position to
|
||||||
|
const gridNode = document.getElementById(`grid-${rand}`)
|
||||||
|
const dataNode = gridNode?.getElementsByClassName("grid-data-outer")?.[0]
|
||||||
|
if (!dataNode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute bounds of cell relative to outer data node
|
||||||
|
const targetBounds = e.target.getBoundingClientRect()
|
||||||
|
const dataBounds = dataNode.getBoundingClientRect()
|
||||||
focusedCellId.set(cellId)
|
focusedCellId.set(cellId)
|
||||||
menu.set({
|
menu.set({
|
||||||
left:
|
left: targetBounds.left - dataBounds.left + e.offsetX,
|
||||||
e.clientX - $bounds.left + GutterWidth + ($stickyColumn?.width || 0),
|
top: targetBounds.top - dataBounds.top + e.offsetY,
|
||||||
top: e.clientY - $bounds.top + $rowHeight,
|
|
||||||
visible: true,
|
visible: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,18 +4,13 @@ import { notifications } from "@budibase/bbui"
|
||||||
import { NewRowID, RowPageSize } from "../lib/constants"
|
import { NewRowID, RowPageSize } from "../lib/constants"
|
||||||
import { tick } from "svelte"
|
import { tick } from "svelte"
|
||||||
|
|
||||||
const initialSortState = {
|
const SuppressErrors = true
|
||||||
column: null,
|
|
||||||
order: "ascending",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
const rows = writable([])
|
const rows = writable([])
|
||||||
const table = writable(null)
|
const table = writable(null)
|
||||||
const filter = writable([])
|
|
||||||
const loading = writable(false)
|
const loading = writable(false)
|
||||||
const loaded = writable(false)
|
const loaded = writable(false)
|
||||||
const sort = writable(initialSortState)
|
|
||||||
const rowChangeCache = writable({})
|
const rowChangeCache = writable({})
|
||||||
const inProgressChanges = writable({})
|
const inProgressChanges = writable({})
|
||||||
const hasNextPage = writable(false)
|
const hasNextPage = writable(false)
|
||||||
|
@ -47,10 +42,8 @@ export const createStores = () => {
|
||||||
rows,
|
rows,
|
||||||
rowLookupMap,
|
rowLookupMap,
|
||||||
table,
|
table,
|
||||||
filter,
|
|
||||||
loaded,
|
loaded,
|
||||||
loading,
|
loading,
|
||||||
sort,
|
|
||||||
rowChangeCache,
|
rowChangeCache,
|
||||||
inProgressChanges,
|
inProgressChanges,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
|
@ -98,15 +91,18 @@ export const deriveStores = context => {
|
||||||
// Reset everything when table ID changes
|
// Reset everything when table ID changes
|
||||||
let unsubscribe = null
|
let unsubscribe = null
|
||||||
let lastResetKey = null
|
let lastResetKey = null
|
||||||
tableId.subscribe($tableId => {
|
tableId.subscribe(async $tableId => {
|
||||||
// Unsub from previous fetch if one exists
|
// Unsub from previous fetch if one exists
|
||||||
unsubscribe?.()
|
unsubscribe?.()
|
||||||
fetch.set(null)
|
fetch.set(null)
|
||||||
instanceLoaded.set(false)
|
instanceLoaded.set(false)
|
||||||
loading.set(true)
|
loading.set(true)
|
||||||
|
|
||||||
// Reset state
|
// Tick to allow other reactive logic to update stores when table ID changes
|
||||||
filter.set([])
|
// before proceeding. This allows us to wipe filters etc if needed.
|
||||||
|
await tick()
|
||||||
|
const $filter = get(filter)
|
||||||
|
const $sort = get(sort)
|
||||||
|
|
||||||
// Create new fetch model
|
// Create new fetch model
|
||||||
const newFetch = fetchData({
|
const newFetch = fetchData({
|
||||||
|
@ -116,9 +112,9 @@ export const deriveStores = context => {
|
||||||
tableId: $tableId,
|
tableId: $tableId,
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
filter: [],
|
filter: $filter,
|
||||||
sortColumn: initialSortState.column,
|
sortColumn: $sort.column,
|
||||||
sortOrder: initialSortState.order,
|
sortOrder: $sort.order,
|
||||||
limit: RowPageSize,
|
limit: RowPageSize,
|
||||||
paginate: true,
|
paginate: true,
|
||||||
},
|
},
|
||||||
|
@ -224,7 +220,10 @@ export const deriveStores = context => {
|
||||||
const addRow = async (row, idx, bubble = false) => {
|
const addRow = async (row, idx, bubble = false) => {
|
||||||
try {
|
try {
|
||||||
// Create row
|
// Create row
|
||||||
const newRow = await API.saveRow({ ...row, tableId: get(tableId) })
|
const newRow = await API.saveRow(
|
||||||
|
{ ...row, tableId: get(tableId) },
|
||||||
|
SuppressErrors
|
||||||
|
)
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
if (idx != null) {
|
if (idx != null) {
|
||||||
|
@ -351,7 +350,10 @@ export const deriveStores = context => {
|
||||||
...state,
|
...state,
|
||||||
[rowId]: true,
|
[rowId]: true,
|
||||||
}))
|
}))
|
||||||
const saved = await API.saveRow({ ...row, ...get(rowChangeCache)[rowId] })
|
const saved = await API.saveRow(
|
||||||
|
{ ...row, ...get(rowChangeCache)[rowId] },
|
||||||
|
SuppressErrors
|
||||||
|
)
|
||||||
|
|
||||||
// Update state after a successful change
|
// Update state after a successful change
|
||||||
if (saved?._id) {
|
if (saved?._id) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { writable, derived, get } from "svelte/store"
|
import { writable, derived, get } from "svelte/store"
|
||||||
import { tick } from "svelte"
|
import { tick } from "svelte"
|
||||||
import { Padding, GutterWidth } from "../lib/constants"
|
import { Padding, GutterWidth, FocusedCellMinOffset } from "../lib/constants"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
const scroll = writable({
|
const scroll = writable({
|
||||||
|
@ -138,14 +138,13 @@ export const initialise = context => {
|
||||||
const $scroll = get(scroll)
|
const $scroll = get(scroll)
|
||||||
const $bounds = get(bounds)
|
const $bounds = get(bounds)
|
||||||
const $rowHeight = get(rowHeight)
|
const $rowHeight = get(rowHeight)
|
||||||
const verticalOffset = 60
|
|
||||||
|
|
||||||
// Ensure vertical position is viewable
|
// Ensure vertical position is viewable
|
||||||
if ($focusedRow) {
|
if ($focusedRow) {
|
||||||
// Ensure row is not below bottom of screen
|
// Ensure row is not below bottom of screen
|
||||||
const rowYPos = $focusedRow.__idx * $rowHeight
|
const rowYPos = $focusedRow.__idx * $rowHeight
|
||||||
const bottomCutoff =
|
const bottomCutoff =
|
||||||
$scroll.top + $bounds.height - $rowHeight - verticalOffset
|
$scroll.top + $bounds.height - $rowHeight - FocusedCellMinOffset
|
||||||
let delta = rowYPos - bottomCutoff
|
let delta = rowYPos - bottomCutoff
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
scroll.update(state => ({
|
scroll.update(state => ({
|
||||||
|
@ -156,7 +155,7 @@ export const initialise = context => {
|
||||||
|
|
||||||
// Ensure row is not above top of screen
|
// Ensure row is not above top of screen
|
||||||
else {
|
else {
|
||||||
const delta = $scroll.top - rowYPos + verticalOffset
|
const delta = $scroll.top - rowYPos + FocusedCellMinOffset
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
scroll.update(state => ({
|
scroll.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
|
@ -171,13 +170,12 @@ export const initialise = context => {
|
||||||
const $visibleColumns = get(visibleColumns)
|
const $visibleColumns = get(visibleColumns)
|
||||||
const columnName = $focusedCellId?.split("-")[1]
|
const columnName = $focusedCellId?.split("-")[1]
|
||||||
const column = $visibleColumns.find(col => col.name === columnName)
|
const column = $visibleColumns.find(col => col.name === columnName)
|
||||||
const horizontalOffset = 50
|
|
||||||
if (!column) {
|
if (!column) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure column is not cutoff on left edge
|
// Ensure column is not cutoff on left edge
|
||||||
let delta = $scroll.left - column.left + horizontalOffset
|
let delta = $scroll.left - column.left + FocusedCellMinOffset
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
scroll.update(state => ({
|
scroll.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
|
@ -188,7 +186,7 @@ export const initialise = context => {
|
||||||
// Ensure column is not cutoff on right edge
|
// Ensure column is not cutoff on right edge
|
||||||
else {
|
else {
|
||||||
const rightEdge = column.left + column.width
|
const rightEdge = column.left + column.width
|
||||||
const rightBound = $bounds.width + $scroll.left - horizontalOffset
|
const rightBound = $bounds.width + $scroll.left - FocusedCellMinOffset
|
||||||
delta = rightEdge - rightBound
|
delta = rightEdge - rightBound
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
scroll.update(state => ({
|
scroll.update(state => ({
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
export const createStores = context => {
|
||||||
|
const { props } = context
|
||||||
|
|
||||||
|
// Initialise to default props
|
||||||
|
const sort = writable({
|
||||||
|
column: props.initialSortColumn,
|
||||||
|
order: props.initialSortOrder || "ascending",
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
sort,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialise = context => {
|
||||||
|
const { sort, initialSortColumn, initialSortOrder } = context
|
||||||
|
|
||||||
|
// Reset sort when initial sort props change
|
||||||
|
initialSortColumn.subscribe(newSortColumn => {
|
||||||
|
sort.update(state => ({ ...state, column: newSortColumn }))
|
||||||
|
})
|
||||||
|
initialSortOrder.subscribe(newSortOrder => {
|
||||||
|
sort.update(state => ({ ...state, order: newSortOrder }))
|
||||||
|
})
|
||||||
|
}
|
|
@ -8,13 +8,16 @@ import {
|
||||||
NewRowID,
|
NewRowID,
|
||||||
} from "../lib/constants"
|
} from "../lib/constants"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = context => {
|
||||||
|
const { props } = context
|
||||||
const focusedCellId = writable(null)
|
const focusedCellId = writable(null)
|
||||||
const focusedCellAPI = writable(null)
|
const focusedCellAPI = writable(null)
|
||||||
const selectedRows = writable({})
|
const selectedRows = writable({})
|
||||||
const hoveredRowId = writable(null)
|
const hoveredRowId = writable(null)
|
||||||
const rowHeight = writable(DefaultRowHeight)
|
const rowHeight = writable(props.initialRowHeight || DefaultRowHeight)
|
||||||
const previousFocusedRowId = writable(null)
|
const previousFocusedRowId = writable(null)
|
||||||
|
const gridFocused = writable(false)
|
||||||
|
const isDragging = writable(false)
|
||||||
|
|
||||||
// Derive the current focused row ID
|
// Derive the current focused row ID
|
||||||
const focusedRowId = derived(
|
const focusedRowId = derived(
|
||||||
|
@ -46,6 +49,8 @@ export const createStores = () => {
|
||||||
previousFocusedRowId,
|
previousFocusedRowId,
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
rowHeight,
|
rowHeight,
|
||||||
|
gridFocused,
|
||||||
|
isDragging,
|
||||||
selectedRows: {
|
selectedRows: {
|
||||||
...selectedRows,
|
...selectedRows,
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -94,9 +99,9 @@ export const deriveStores = context => {
|
||||||
|
|
||||||
// Derive the amount of content lines to show in cells depending on row height
|
// Derive the amount of content lines to show in cells depending on row height
|
||||||
const contentLines = derived(rowHeight, $rowHeight => {
|
const contentLines = derived(rowHeight, $rowHeight => {
|
||||||
if ($rowHeight === LargeRowHeight) {
|
if ($rowHeight >= LargeRowHeight) {
|
||||||
return 3
|
return 3
|
||||||
} else if ($rowHeight === MediumRowHeight) {
|
} else if ($rowHeight >= MediumRowHeight) {
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
|
@ -129,6 +134,7 @@ export const initialise = context => {
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
table,
|
table,
|
||||||
rowHeight,
|
rowHeight,
|
||||||
|
initialRowHeight,
|
||||||
} = context
|
} = context
|
||||||
|
|
||||||
// Ensure we clear invalid rows from state if they disappear
|
// Ensure we clear invalid rows from state if they disappear
|
||||||
|
@ -185,4 +191,13 @@ export const initialise = context => {
|
||||||
table.subscribe($table => {
|
table.subscribe($table => {
|
||||||
rowHeight.set($table?.rowHeight || DefaultRowHeight)
|
rowHeight.set($table?.rowHeight || DefaultRowHeight)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Reset row height when initial row height prop changes
|
||||||
|
initialRowHeight.subscribe(height => {
|
||||||
|
if (height) {
|
||||||
|
rowHeight.set(height)
|
||||||
|
} else {
|
||||||
|
rowHeight.set(get(table)?.rowHeight || DefaultRowHeight)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,11 +108,22 @@ export const deriveStores = context => {
|
||||||
// Determine the row index at which we should start vertically inverting cell
|
// Determine the row index at which we should start vertically inverting cell
|
||||||
// dropdowns
|
// dropdowns
|
||||||
const rowVerticalInversionIndex = derived(
|
const rowVerticalInversionIndex = derived(
|
||||||
[visualRowCapacity, rowHeight],
|
[height, rowHeight, scrollTop],
|
||||||
([$visualRowCapacity, $rowHeight]) => {
|
([$height, $rowHeight, $scrollTop]) => {
|
||||||
return (
|
const offset = $scrollTop % $rowHeight
|
||||||
$visualRowCapacity - Math.ceil(MaxCellRenderHeight / $rowHeight) - 2
|
|
||||||
)
|
// Compute the last row index with space to render popovers below it
|
||||||
|
const minBottom =
|
||||||
|
$height - ScrollBarSize * 3 - MaxCellRenderHeight + offset
|
||||||
|
const lastIdx = Math.floor(minBottom / $rowHeight)
|
||||||
|
|
||||||
|
// Compute the first row index with space to render popovers above it
|
||||||
|
const minTop = MaxCellRenderHeight + offset
|
||||||
|
const firstIdx = Math.ceil(minTop / $rowHeight)
|
||||||
|
|
||||||
|
// Use the greater of the two indices so that we prefer content below,
|
||||||
|
// unless there is room to render the entire popover above
|
||||||
|
return Math.max(lastIdx, firstIdx)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -125,7 +136,7 @@ export const deriveStores = context => {
|
||||||
let inversionIdx = $renderedColumns.length
|
let inversionIdx = $renderedColumns.length
|
||||||
for (let i = $renderedColumns.length - 1; i >= 0; i--, inversionIdx--) {
|
for (let i = $renderedColumns.length - 1; i >= 0; i--, inversionIdx--) {
|
||||||
const rightEdge = $renderedColumns[i].left + $renderedColumns[i].width
|
const rightEdge = $renderedColumns[i].left + $renderedColumns[i].width
|
||||||
if (rightEdge + MaxCellRenderWidthOverflow < cutoff) {
|
if (rightEdge + MaxCellRenderWidthOverflow <= cutoff) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,8 +136,10 @@ export default class DataFetch {
|
||||||
this.options.sortOrder = "ascending"
|
this.options.sortOrder = "ascending"
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no sort column, use the primary display and fallback to first column
|
// If no sort column, or an invalid sort column is provided, use the primary
|
||||||
if (!this.options.sortColumn) {
|
// display and fallback to first column
|
||||||
|
const sortValid = this.options.sortColumn && schema[this.options.sortColumn]
|
||||||
|
if (!sortValid) {
|
||||||
let newSortColumn
|
let newSortColumn
|
||||||
if (definition?.primaryDisplay && schema[definition.primaryDisplay]) {
|
if (definition?.primaryDisplay && schema[definition.primaryDisplay]) {
|
||||||
newSortColumn = definition.primaryDisplay
|
newSortColumn = definition.primaryDisplay
|
||||||
|
|
|
@ -3,4 +3,5 @@ export * as JSONUtils from "./json"
|
||||||
export * as CookieUtils from "./cookies"
|
export * as CookieUtils from "./cookies"
|
||||||
export * as RoleUtils from "./roles"
|
export * as RoleUtils from "./roles"
|
||||||
export * as Utils from "./utils"
|
export * as Utils from "./utils"
|
||||||
|
export { memo, derivedMemo } from "./memo"
|
||||||
export { createWebsocket } from "./websocket"
|
export { createWebsocket } from "./websocket"
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { writable, get, derived } from "svelte/store"
|
||||||
|
|
||||||
|
// A simple svelte store which deeply compares all changes and ensures that
|
||||||
|
// subscribed children will only fire when a new value is actually set
|
||||||
|
export const memo = initialValue => {
|
||||||
|
const store = writable(initialValue)
|
||||||
|
|
||||||
|
const tryUpdateValue = (newValue, currentValue) => {
|
||||||
|
// Sanity check for primitive equality
|
||||||
|
if (currentValue === newValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise deep compare via JSON stringify
|
||||||
|
const currentString = JSON.stringify(currentValue)
|
||||||
|
const newString = JSON.stringify(newValue)
|
||||||
|
if (currentString !== newString) {
|
||||||
|
store.set(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
set: newValue => {
|
||||||
|
const currentValue = get(store)
|
||||||
|
tryUpdateValue(newValue, currentValue)
|
||||||
|
},
|
||||||
|
update: updateFn => {
|
||||||
|
const currentValue = get(store)
|
||||||
|
let mutableCurrentValue = JSON.parse(JSON.stringify(currentValue))
|
||||||
|
const newValue = updateFn(mutableCurrentValue)
|
||||||
|
tryUpdateValue(newValue, currentValue)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enriched version of svelte's derived store which returns a memo
|
||||||
|
export const derivedMemo = (store, derivation) => {
|
||||||
|
const derivedStore = derived(store, derivation)
|
||||||
|
const memoStore = memo(get(derivedStore))
|
||||||
|
derivedStore.subscribe(memoStore.set)
|
||||||
|
return memoStore
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import authorized from "../middleware/authorized"
|
import authorized from "../middleware/authorized"
|
||||||
import { BaseSocket } from "./websocket"
|
import { BaseSocket } from "./websocket"
|
||||||
import { permissions } from "@budibase/backend-core"
|
import { context, permissions } from "@budibase/backend-core"
|
||||||
import http from "http"
|
import http from "http"
|
||||||
import Koa from "koa"
|
import Koa from "koa"
|
||||||
import { getTableId } from "../api/controllers/row/utils"
|
import { getTableId } from "../api/controllers/row/utils"
|
||||||
|
@ -8,20 +8,56 @@ import { Row, Table } from "@budibase/types"
|
||||||
import { Socket } from "socket.io"
|
import { Socket } from "socket.io"
|
||||||
import { GridSocketEvent } from "@budibase/shared-core"
|
import { GridSocketEvent } from "@budibase/shared-core"
|
||||||
|
|
||||||
|
const { PermissionType, PermissionLevel } = permissions
|
||||||
|
|
||||||
export default class GridSocket extends BaseSocket {
|
export default class GridSocket extends BaseSocket {
|
||||||
constructor(app: Koa, server: http.Server) {
|
constructor(app: Koa, server: http.Server) {
|
||||||
super(app, server, "/socket/grid", [authorized(permissions.BUILDER)])
|
super(app, server, "/socket/grid")
|
||||||
}
|
}
|
||||||
|
|
||||||
async onConnect(socket: Socket) {
|
async onConnect(socket: Socket) {
|
||||||
// Initial identification of connected spreadsheet
|
// Initial identification of connected spreadsheet
|
||||||
socket.on(GridSocketEvent.SelectTable, async ({ tableId }, callback) => {
|
socket.on(
|
||||||
await this.joinRoom(socket, tableId)
|
GridSocketEvent.SelectTable,
|
||||||
|
async ({ tableId, appId }, callback) => {
|
||||||
|
// Ignore if no table or app specified
|
||||||
|
if (!tableId || !appId) {
|
||||||
|
socket.disconnect(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Reply with all users in current room
|
// Check if the user has permission to read this resource
|
||||||
const sessions = await this.getRoomSessions(tableId)
|
const middleware = authorized(
|
||||||
callback({ users: sessions })
|
PermissionType.TABLE,
|
||||||
})
|
PermissionLevel.READ
|
||||||
|
)
|
||||||
|
const ctx = {
|
||||||
|
appId,
|
||||||
|
resourceId: tableId,
|
||||||
|
roleId: socket.data.roleId,
|
||||||
|
user: { _id: socket.data._id },
|
||||||
|
isAuthenticated: socket.data.isAuthenticated,
|
||||||
|
request: {
|
||||||
|
url: "/fake",
|
||||||
|
},
|
||||||
|
get: () => null,
|
||||||
|
throw: () => {
|
||||||
|
// If they don't have access, immediately disconnect them
|
||||||
|
socket.disconnect(true)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await context.doInAppContext(appId, async () => {
|
||||||
|
await middleware(ctx, async () => {
|
||||||
|
const room = `${appId}-${tableId}`
|
||||||
|
await this.joinRoom(socket, room)
|
||||||
|
|
||||||
|
// Reply with all users in current room
|
||||||
|
const sessions = await this.getRoomSessions(room)
|
||||||
|
callback({ users: sessions })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Handle users selecting a new cell
|
// Handle users selecting a new cell
|
||||||
socket.on(GridSocketEvent.SelectCell, ({ cellId }) => {
|
socket.on(GridSocketEvent.SelectCell, ({ cellId }) => {
|
||||||
|
@ -31,7 +67,8 @@ export default class GridSocket extends BaseSocket {
|
||||||
|
|
||||||
emitRowUpdate(ctx: any, row: Row) {
|
emitRowUpdate(ctx: any, row: Row) {
|
||||||
const tableId = getTableId(ctx)
|
const tableId = getTableId(ctx)
|
||||||
this.emitToRoom(ctx, tableId, GridSocketEvent.RowChange, {
|
const room = `${ctx.appId}-${tableId}`
|
||||||
|
this.emitToRoom(ctx, room, GridSocketEvent.RowChange, {
|
||||||
id: row._id,
|
id: row._id,
|
||||||
row,
|
row,
|
||||||
})
|
})
|
||||||
|
@ -39,17 +76,20 @@ export default class GridSocket extends BaseSocket {
|
||||||
|
|
||||||
emitRowDeletion(ctx: any, id: string) {
|
emitRowDeletion(ctx: any, id: string) {
|
||||||
const tableId = getTableId(ctx)
|
const tableId = getTableId(ctx)
|
||||||
this.emitToRoom(ctx, tableId, GridSocketEvent.RowChange, { id, row: null })
|
const room = `${ctx.appId}-${tableId}`
|
||||||
|
this.emitToRoom(ctx, room, GridSocketEvent.RowChange, { id, row: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
emitTableUpdate(ctx: any, table: Table) {
|
emitTableUpdate(ctx: any, table: Table) {
|
||||||
this.emitToRoom(ctx, table._id!, GridSocketEvent.TableChange, {
|
const room = `${ctx.appId}-${table._id}`
|
||||||
|
this.emitToRoom(ctx, room, GridSocketEvent.TableChange, {
|
||||||
id: table._id,
|
id: table._id,
|
||||||
table,
|
table,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
emitTableDeletion(ctx: any, id: string) {
|
emitTableDeletion(ctx: any, id: string) {
|
||||||
this.emitToRoom(ctx, id, GridSocketEvent.TableChange, { id, table: null })
|
const room = `${ctx.appId}-${id}`
|
||||||
|
this.emitToRoom(ctx, room, GridSocketEvent.TableChange, { id, table: null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,18 @@ import Koa from "koa"
|
||||||
import Cookies from "cookies"
|
import Cookies from "cookies"
|
||||||
import { userAgent } from "koa-useragent"
|
import { userAgent } from "koa-useragent"
|
||||||
import { auth, Header, redis } from "@budibase/backend-core"
|
import { auth, Header, redis } from "@budibase/backend-core"
|
||||||
import currentApp from "../middleware/currentapp"
|
|
||||||
import { createAdapter } from "@socket.io/redis-adapter"
|
import { createAdapter } from "@socket.io/redis-adapter"
|
||||||
import { Socket } from "socket.io"
|
import { Socket } from "socket.io"
|
||||||
import { getSocketPubSubClients } from "../utilities/redis"
|
import { getSocketPubSubClients } from "../utilities/redis"
|
||||||
import { SocketEvent, SocketSessionTTL } from "@budibase/shared-core"
|
import { SocketEvent, SocketSessionTTL } from "@budibase/shared-core"
|
||||||
import { SocketSession } from "@budibase/types"
|
import { SocketSession } from "@budibase/types"
|
||||||
|
import { v4 as uuid } from "uuid"
|
||||||
|
|
||||||
|
const anonUser = () => ({
|
||||||
|
_id: uuid(),
|
||||||
|
email: "user@mail.com",
|
||||||
|
firstName: "Anonymous",
|
||||||
|
})
|
||||||
|
|
||||||
export class BaseSocket {
|
export class BaseSocket {
|
||||||
io: Server
|
io: Server
|
||||||
|
@ -34,7 +40,6 @@ export class BaseSocket {
|
||||||
const middlewares = [
|
const middlewares = [
|
||||||
userAgent,
|
userAgent,
|
||||||
authenticate,
|
authenticate,
|
||||||
currentApp,
|
|
||||||
...(additionalMiddlewares || []),
|
...(additionalMiddlewares || []),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -70,7 +75,8 @@ export class BaseSocket {
|
||||||
// Middlewares are finished
|
// Middlewares are finished
|
||||||
// Extract some data from our enriched koa context to persist
|
// Extract some data from our enriched koa context to persist
|
||||||
// as metadata for the socket
|
// as metadata for the socket
|
||||||
const { _id, email, firstName, lastName } = ctx.user
|
const user = ctx.user?._id ? ctx.user : anonUser()
|
||||||
|
const { _id, email, firstName, lastName } = user
|
||||||
socket.data = {
|
socket.data = {
|
||||||
_id,
|
_id,
|
||||||
email,
|
email,
|
||||||
|
@ -78,6 +84,8 @@ export class BaseSocket {
|
||||||
lastName,
|
lastName,
|
||||||
sessionId: socket.id,
|
sessionId: socket.id,
|
||||||
connectedAt: Date.now(),
|
connectedAt: Date.now(),
|
||||||
|
isAuthenticated: ctx.isAuthenticated,
|
||||||
|
roleId: ctx.roleId,
|
||||||
}
|
}
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue