Merge pull request #4638 from Budibase/feature/table-row-selection
Allow selection of rows from table component
This commit is contained in:
commit
ffe35bc5ec
16 changed files with 257 additions and 80 deletions
|
@ -47,7 +47,9 @@
|
|||
<use xlink:href="#spectrum-css-icon-Dash100" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="spectrum-Checkbox-label">{text || ""}</span>
|
||||
{#if text}
|
||||
<span class="spectrum-Checkbox-label">{text}</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -8,9 +8,21 @@
|
|||
export let allowEditRows = false
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if allowSelectRows}
|
||||
<Checkbox value={selected} />
|
||||
{/if}
|
||||
{#if allowEditRows}
|
||||
<ActionButton size="S" on:click={onEdit}>Edit</ActionButton>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
||||
import { cloneDeep, deepGet } from "../helpers"
|
||||
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
|
||||
import Checkbox from "../Form/Checkbox.svelte"
|
||||
|
||||
/**
|
||||
* The expected schema is our normal couch schemas for our tables.
|
||||
|
@ -31,7 +32,6 @@
|
|||
export let allowEditRows = true
|
||||
export let allowEditColumns = true
|
||||
export let selectedRows = []
|
||||
export let editColumnTitle = "Edit"
|
||||
export let customRenderers = []
|
||||
export let disableSorting = false
|
||||
export let autoSortColumns = true
|
||||
|
@ -50,6 +50,8 @@
|
|||
// Table state
|
||||
let height = 0
|
||||
let loaded = false
|
||||
let checkboxStatus = false
|
||||
|
||||
$: schema = fixSchema(schema)
|
||||
$: if (!loading) loaded = true
|
||||
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
|
||||
|
@ -67,6 +69,16 @@
|
|||
$: showEditColumn = allowEditRows || allowSelectRows
|
||||
$: cellStyles = computeCellStyles(schema)
|
||||
|
||||
// Deselect the "select all" checkbox when the user navigates to a new page
|
||||
$: {
|
||||
let checkRowCount = rows.filter(o1 =>
|
||||
selectedRows.some(o2 => o1._id === o2._id)
|
||||
)
|
||||
if (checkRowCount.length === 0) {
|
||||
checkboxStatus = false
|
||||
}
|
||||
}
|
||||
|
||||
const fixSchema = schema => {
|
||||
let fixedSchema = {}
|
||||
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
|
||||
|
@ -197,13 +209,32 @@
|
|||
if (!allowSelectRows) {
|
||||
return
|
||||
}
|
||||
if (selectedRows.includes(row)) {
|
||||
selectedRows = selectedRows.filter(selectedRow => selectedRow !== row)
|
||||
if (selectedRows.some(selectedRow => selectedRow._id === row._id)) {
|
||||
selectedRows = selectedRows.filter(
|
||||
selectedRow => selectedRow._id !== row._id
|
||||
)
|
||||
} else {
|
||||
selectedRows = [...selectedRows, row]
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSelectAll = e => {
|
||||
const select = !!e.detail
|
||||
if (select) {
|
||||
// Add any rows which are not already in selected rows
|
||||
rows.forEach(row => {
|
||||
if (selectedRows.findIndex(x => x._id === row._id) === -1) {
|
||||
selectedRows.push(row)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Remove any rows from selected rows that are in the current data set
|
||||
selectedRows = selectedRows.filter(el =>
|
||||
rows.every(f => f._id !== el._id)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const computeCellStyles = schema => {
|
||||
let styles = {}
|
||||
Object.keys(schema || {}).forEach(field => {
|
||||
|
@ -244,7 +275,14 @@
|
|||
<div
|
||||
class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit"
|
||||
>
|
||||
{editColumnTitle || ""}
|
||||
{#if allowSelectRows}
|
||||
<Checkbox
|
||||
bind:value={checkboxStatus}
|
||||
on:change={toggleSelectAll}
|
||||
/>
|
||||
{:else}
|
||||
Edit
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#each fields as field}
|
||||
|
@ -302,11 +340,16 @@
|
|||
{#if showEditColumn}
|
||||
<div
|
||||
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
|
||||
on:click={e => {
|
||||
toggleSelectRow(row)
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<SelectEditRenderer
|
||||
data={row}
|
||||
selected={selectedRows.includes(row)}
|
||||
onToggleSelection={() => toggleSelectRow(row)}
|
||||
selected={selectedRows.findIndex(
|
||||
selectedRow => selectedRow._id === row._id
|
||||
) !== -1}
|
||||
onEdit={e => editRow(e, row)}
|
||||
{allowSelectRows}
|
||||
{allowEditRows}
|
||||
|
|
|
@ -53,10 +53,10 @@
|
|||
to-gfm-code-block "^0.1.1"
|
||||
year "^0.2.1"
|
||||
|
||||
"@budibase/string-templates@^1.0.66-alpha.0":
|
||||
version "1.0.72"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.72.tgz#acc154e402cce98ea30eedde9c6124183ee9b37c"
|
||||
integrity sha512-w715TjgO6NUHkZNqoOEo8lAKJ/PQ4b00ATWSX5VB523SAu7y/uOiqKqV1E3fgwxq1o8L+Ff7rn9FTkiYtjkV/g==
|
||||
"@budibase/string-templates@^1.0.72-alpha.0":
|
||||
version "1.0.75"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.75.tgz#5b4061f1a626160ec092f32f036541376298100c"
|
||||
integrity sha512-hPgr6n5cpSCGFEha5DS/P+rtRXOLc72M6y4J/scl59JvUi/ZUJkjRgJdpQPdBLu04CNKp89V59+rAqAuDjOC0g==
|
||||
dependencies:
|
||||
"@budibase/handlebars-helpers" "^0.11.7"
|
||||
dayjs "^1.10.4"
|
||||
|
|
|
@ -27,10 +27,13 @@ filterTests(["smoke", "all"], () => {
|
|||
it("updates a column on the table", () => {
|
||||
cy.get(".title").click()
|
||||
cy.get(".spectrum-Table-editIcon > use").click()
|
||||
cy.get("input").eq(1).type("updated", { force: true })
|
||||
cy.get(".modal-inner-wrapper").within(() => {
|
||||
|
||||
cy.get("input").eq(0).type("updated", { force: true })
|
||||
// Unset table display column
|
||||
cy.get(".spectrum-Switch-input").eq(1).click()
|
||||
cy.contains("Save Column").click()
|
||||
})
|
||||
cy.contains("nameupdated ").should("contain", "nameupdated")
|
||||
})
|
||||
|
||||
|
|
|
@ -172,6 +172,7 @@ Cypress.Commands.add("addRow", values => {
|
|||
|
||||
Cypress.Commands.add("addRowMultiValue", values => {
|
||||
cy.contains("Create row").click()
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get(".spectrum-Form-itemField")
|
||||
.click()
|
||||
.then(() => {
|
||||
|
@ -184,6 +185,7 @@ Cypress.Commands.add("addRowMultiValue", values => {
|
|||
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("createUser", email => {
|
||||
// quick hacky recorded way to create a user
|
||||
|
|
|
@ -32,12 +32,14 @@ export const getBindableProperties = (asset, componentId) => {
|
|||
const urlBindings = getUrlBindings(asset)
|
||||
const deviceBindings = getDeviceBindings()
|
||||
const stateBindings = getStateBindings()
|
||||
const selectedRowsBindings = getSelectedRowsBindings(asset)
|
||||
return [
|
||||
...contextBindings,
|
||||
...urlBindings,
|
||||
...stateBindings,
|
||||
...userBindings,
|
||||
...deviceBindings,
|
||||
...selectedRowsBindings,
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -315,6 +317,40 @@ const getDeviceBindings = () => {
|
|||
return bindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all selected rows bindings for tables in the current asset.
|
||||
*/
|
||||
const getSelectedRowsBindings = asset => {
|
||||
let bindings = []
|
||||
if (get(store).clientFeatures?.rowSelection) {
|
||||
// Add bindings for table components
|
||||
let tables = findAllMatchingComponents(asset?.props, component =>
|
||||
component._component.endsWith("table")
|
||||
)
|
||||
const safeState = makePropSafe("rowSelection")
|
||||
bindings = bindings.concat(
|
||||
tables.map(table => ({
|
||||
type: "context",
|
||||
runtimeBinding: `${safeState}.${makePropSafe(table._id)}`,
|
||||
readableBinding: `${table._instanceName}.Selected rows`,
|
||||
}))
|
||||
)
|
||||
|
||||
// Add bindings for table blocks
|
||||
let tableBlocks = findAllMatchingComponents(asset?.props, component =>
|
||||
component._component.endsWith("tableblock")
|
||||
)
|
||||
bindings = bindings.concat(
|
||||
tableBlocks.map(block => ({
|
||||
type: "context",
|
||||
runtimeBinding: `${safeState}.${makePropSafe(block._id + "-table")}`,
|
||||
readableBinding: `${block._instanceName}.Selected rows`,
|
||||
}))
|
||||
)
|
||||
}
|
||||
return bindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all state bindings that are globally available.
|
||||
*/
|
||||
|
@ -597,14 +633,9 @@ const buildFormSchema = component => {
|
|||
* in the app.
|
||||
*/
|
||||
export const getAllStateVariables = () => {
|
||||
// Get all component containing assets
|
||||
let allAssets = []
|
||||
allAssets = allAssets.concat(get(store).layouts || [])
|
||||
allAssets = allAssets.concat(get(store).screens || [])
|
||||
|
||||
// Find all button action settings in all components
|
||||
let eventSettings = []
|
||||
allAssets.forEach(asset => {
|
||||
getAllAssets().forEach(asset => {
|
||||
findAllMatchingComponents(asset.props, component => {
|
||||
const settings = getComponentSettings(component._component)
|
||||
settings
|
||||
|
@ -635,6 +666,15 @@ export const getAllStateVariables = () => {
|
|||
return Array.from(bindingSet)
|
||||
}
|
||||
|
||||
export const getAllAssets = () => {
|
||||
// Get all component containing assets
|
||||
let allAssets = []
|
||||
allAssets = allAssets.concat(get(store).layouts || [])
|
||||
allAssets = allAssets.concat(get(store).screens || [])
|
||||
|
||||
return allAssets
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurses the input object to remove any instances of bindings.
|
||||
*/
|
||||
|
|
|
@ -41,6 +41,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
intelligentLoading: false,
|
||||
deviceAwareness: false,
|
||||
state: false,
|
||||
rowSelection: false,
|
||||
customThemes: false,
|
||||
devicePreview: false,
|
||||
messagePassing: false,
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
"state": true,
|
||||
"customThemes": true,
|
||||
"devicePreview": true,
|
||||
"messagePassing": true
|
||||
"messagePassing": true,
|
||||
"rowSelection": true
|
||||
},
|
||||
"layout": {
|
||||
"name": "Layout",
|
||||
|
@ -2714,6 +2715,13 @@
|
|||
"key": "showAutoColumns",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Allow row selection",
|
||||
"key": "allowSelectRows",
|
||||
"defaultValue": false
|
||||
},
|
||||
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Link table rows",
|
||||
|
@ -2973,6 +2981,11 @@
|
|||
"label": "Show auto columns",
|
||||
"key": "showAutoColumns"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Allow row selection",
|
||||
"key": "allowSelectRows"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Link table rows",
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
import UserBindingsProvider from "components/context/UserBindingsProvider.svelte"
|
||||
import DeviceBindingsProvider from "components/context/DeviceBindingsProvider.svelte"
|
||||
import StateBindingsProvider from "components/context/StateBindingsProvider.svelte"
|
||||
import RowSelectionProvider from "components/context/RowSelectionProvider.svelte"
|
||||
import SettingsBar from "components/preview/SettingsBar.svelte"
|
||||
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
||||
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
||||
|
@ -90,6 +91,7 @@
|
|||
<UserBindingsProvider>
|
||||
<DeviceBindingsProvider>
|
||||
<StateBindingsProvider>
|
||||
<RowSelectionProvider>
|
||||
<!-- Settings bar can be rendered outside of device preview -->
|
||||
<!-- Key block needs to be outside the if statement or it breaks -->
|
||||
{#key $builderStore.selectedComponentId}
|
||||
|
@ -143,6 +145,7 @@
|
|||
<DNDHandler />
|
||||
{/if}
|
||||
</div>
|
||||
</RowSelectionProvider>
|
||||
</StateBindingsProvider>
|
||||
</DeviceBindingsProvider>
|
||||
</UserBindingsProvider>
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
export let quiet
|
||||
export let compact
|
||||
export let size
|
||||
export let allowSelectRows
|
||||
export let linkRows
|
||||
export let linkURL
|
||||
export let linkColumn
|
||||
|
@ -157,6 +158,7 @@
|
|||
>
|
||||
<BlockComponent
|
||||
type="table"
|
||||
context="table"
|
||||
props={{
|
||||
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
||||
columns: tableColumns,
|
||||
|
@ -164,6 +166,7 @@
|
|||
rowCount,
|
||||
quiet,
|
||||
compact,
|
||||
allowSelectRows,
|
||||
size,
|
||||
linkRows,
|
||||
linkURL,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { Table } from "@budibase/bbui"
|
||||
import SlotRenderer from "./SlotRenderer.svelte"
|
||||
import { UnsortableTypes } from "../../../constants"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
export let dataProvider
|
||||
export let columns
|
||||
|
@ -14,10 +15,12 @@
|
|||
export let linkURL
|
||||
export let linkColumn
|
||||
export let linkPeek
|
||||
export let allowSelectRows
|
||||
export let compact
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable, getAction, ActionTypes, routeStore } = getContext("sdk")
|
||||
const { styleable, getAction, ActionTypes, routeStore, rowSelectionStore } =
|
||||
getContext("sdk")
|
||||
const customColumnKey = `custom-${Math.random()}`
|
||||
const customRenderers = [
|
||||
{
|
||||
|
@ -25,7 +28,7 @@
|
|||
component: SlotRenderer,
|
||||
},
|
||||
]
|
||||
|
||||
let selectedRows = []
|
||||
$: hasChildren = $component.children
|
||||
$: loading = dataProvider?.loading ?? false
|
||||
$: data = dataProvider?.rows || []
|
||||
|
@ -36,6 +39,12 @@
|
|||
dataProvider?.id,
|
||||
ActionTypes.SetDataProviderSorting
|
||||
)
|
||||
$: {
|
||||
rowSelectionStore.actions.updateSelection(
|
||||
$component.id,
|
||||
selectedRows.map(row => row._id)
|
||||
)
|
||||
}
|
||||
|
||||
const getFields = (schema, customColumns, showAutoColumns) => {
|
||||
// Check for an invalid column selection
|
||||
|
@ -117,6 +126,10 @@
|
|||
const split = linkURL.split("/:")
|
||||
routeStore.actions.navigate(`${split[0]}/${id}`, linkPeek)
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
rowSelectionStore.actions.updateSelection($component.id, [])
|
||||
})
|
||||
</script>
|
||||
|
||||
<div use:styleable={$component.styles} class={size}>
|
||||
|
@ -128,7 +141,8 @@
|
|||
{quiet}
|
||||
{compact}
|
||||
{customRenderers}
|
||||
allowSelectRows={false}
|
||||
allowSelectRows={!!allowSelectRows}
|
||||
bind:selectedRows
|
||||
allowEditRows={false}
|
||||
allowEditColumns={false}
|
||||
showAutoColumns={true}
|
||||
|
@ -139,10 +153,19 @@
|
|||
>
|
||||
<slot />
|
||||
</Table>
|
||||
{#if allowSelectRows && selectedRows.length}
|
||||
<div class="row-count">
|
||||
{selectedRows.length} row{selectedRows.length === 1 ? "" : "s"} selected
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
background-color: var(--spectrum-alias-background-color-secondary);
|
||||
}
|
||||
|
||||
.row-count {
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<script>
|
||||
import Provider from "./Provider.svelte"
|
||||
import { rowSelectionStore } from "stores"
|
||||
</script>
|
||||
|
||||
<Provider key="rowSelection" data={$rowSelectionStore}>
|
||||
<slot />
|
||||
</Provider>
|
|
@ -6,6 +6,7 @@ import {
|
|||
screenStore,
|
||||
builderStore,
|
||||
uploadStore,
|
||||
rowSelectionStore,
|
||||
} from "stores"
|
||||
import { styleable } from "utils/styleable"
|
||||
import { linkable } from "utils/linkable"
|
||||
|
@ -19,6 +20,7 @@ export default {
|
|||
authStore,
|
||||
notificationStore,
|
||||
routeStore,
|
||||
rowSelectionStore,
|
||||
screenStore,
|
||||
builderStore,
|
||||
uploadStore,
|
||||
|
|
|
@ -10,7 +10,7 @@ export { peekStore } from "./peek"
|
|||
export { stateStore } from "./state"
|
||||
export { themeStore } from "./theme"
|
||||
export { uploadStore } from "./uploads.js"
|
||||
|
||||
export { rowSelectionStore } from "./rowSelection.js"
|
||||
// Context stores are layered and duplicated, so it is not a singleton
|
||||
export { createContextStore } from "./context"
|
||||
|
||||
|
|
22
packages/client/src/stores/rowSelection.js
Normal file
22
packages/client/src/stores/rowSelection.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
const createRowSelectionStore = () => {
|
||||
const store = writable({})
|
||||
|
||||
function updateSelection(componentId, selectedRows) {
|
||||
store.update(state => {
|
||||
state[componentId] = [...selectedRows]
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
set: store.set,
|
||||
actions: {
|
||||
updateSelection,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const rowSelectionStore = createRowSelectionStore()
|
Loading…
Reference in a new issue