Merge pull request #15249 from Budibase/typing/stores-grid-ui

Typing grid UI store
This commit is contained in:
Adria Navarro 2024-12-31 11:24:08 +01:00 committed by GitHub
commit 76cab74a61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 165 additions and 50 deletions

View File

@ -1,3 +1,5 @@
// TODO: remove when all stores are typed
import { GeneratedIDPrefix, CellIDSeparator } from "./constants" import { GeneratedIDPrefix, CellIDSeparator } from "./constants"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"

View File

@ -0,0 +1,30 @@
import { GeneratedIDPrefix, CellIDSeparator } from "./constants"
import { Helpers } from "@budibase/bbui"
export const parseCellID = (cellId: string | null) => {
if (!cellId) {
return { rowId: undefined, field: undefined }
}
const parts = cellId.split(CellIDSeparator)
const field = parts.pop()
return { rowId: parts.join(CellIDSeparator), field }
}
export const getCellID = (rowId: string, fieldName: string) => {
return `${rowId}${CellIDSeparator}${fieldName}`
}
export const parseEventLocation = (e: MouseEvent & TouchEvent) => {
return {
x: e.clientX ?? e.touches?.[0]?.clientX,
y: e.clientY ?? e.touches?.[0]?.clientY,
}
}
export const generateRowID = () => {
return `${GeneratedIDPrefix}${Helpers.uuid()}`
}
export const isGeneratedRowID = (id: string) => {
return id?.startsWith(GeneratedIDPrefix)
}

View File

@ -1,8 +1,28 @@
import { get, derived } from "svelte/store" import { get, derived, Writable, Readable } from "svelte/store"
import { FieldType, UILogicalOperator } from "@budibase/types" import {
ArrayOperator,
BasicOperator,
FieldType,
UIColumn,
UILegacyFilter,
UILogicalOperator,
UISearchFilter,
} from "@budibase/types"
import { Store as StoreContext } from "."
import { memo } from "../../../utils/memo" import { memo } from "../../../utils/memo"
export const createStores = context => { export interface FilterStore {
filter: Writable<UISearchFilter | undefined>
inlineFilters: Writable<UILegacyFilter[]>
}
export interface FilterDerivedStore {
allFilters: Readable<UISearchFilter | undefined>
}
export type Store = FilterStore & FilterDerivedStore
export const createStores = (context: StoreContext): FilterStore => {
const { props } = context const { props } = context
// Initialise to default props // Initialise to default props
@ -15,7 +35,7 @@ export const createStores = context => {
} }
} }
export const deriveStores = context => { export const deriveStores = (context: StoreContext): FilterDerivedStore => {
const { filter, inlineFilters } = context const { filter, inlineFilters } = context
const allFilters = derived( const allFilters = derived(
[filter, inlineFilters], [filter, inlineFilters],
@ -24,7 +44,7 @@ export const deriveStores = context => {
if (!$inlineFilters?.length) { if (!$inlineFilters?.length) {
return $filter return $filter
} }
let allFilters = { const allFilters: UISearchFilter = {
logicalOperator: UILogicalOperator.ALL, logicalOperator: UILogicalOperator.ALL,
groups: [ groups: [
{ {
@ -33,12 +53,13 @@ export const deriveStores = context => {
}, },
], ],
} }
// Just use inline if no filter // Just use inline if no filter
if (!$filter?.groups?.length) { if (!$filter?.groups?.length) {
return allFilters return allFilters
} }
// Join them together if both // Join them together if both
allFilters.groups = [...allFilters.groups, ...$filter.groups] allFilters.groups = [...allFilters.groups!, ...$filter.groups]
return allFilters return allFilters
} }
) )
@ -48,16 +69,16 @@ export const deriveStores = context => {
} }
} }
export const createActions = context => { export const createActions = (context: StoreContext) => {
const { filter, inlineFilters } = context const { filter, inlineFilters } = context
const addInlineFilter = (column, value) => { const addInlineFilter = (column: UIColumn, value: string) => {
const filterId = `inline-${column.name}` const filterId = `inline-${column.name}`
const type = column.schema.type const type = column.schema.type
let inlineFilter = { const inlineFilter: UILegacyFilter = {
field: column.name, field: column.name,
id: filterId, id: filterId,
operator: "string", operator: BasicOperator.STRING,
valueType: "value", valueType: "value",
type, type,
value, value,
@ -66,11 +87,11 @@ export const createActions = context => {
// Add overrides specific so the certain column type // Add overrides specific so the certain column type
if (type === FieldType.NUMBER) { if (type === FieldType.NUMBER) {
inlineFilter.value = parseFloat(value) inlineFilter.value = parseFloat(value)
inlineFilter.operator = "equal" inlineFilter.operator = BasicOperator.EQUAL
} else if (type === FieldType.BIGINT) { } else if (type === FieldType.BIGINT) {
inlineFilter.operator = "equal" inlineFilter.operator = BasicOperator.EQUAL
} else if (type === FieldType.ARRAY) { } else if (type === FieldType.ARRAY) {
inlineFilter.operator = "contains" inlineFilter.operator = ArrayOperator.CONTAINS
} }
inlineFilters.update($inlineFilters => { inlineFilters.update($inlineFilters => {
@ -95,7 +116,7 @@ export const createActions = context => {
} }
} }
export const initialise = context => { export const initialise = (context: StoreContext) => {
const { filter, initialFilter } = context const { filter, initialFilter } = context
// Reset filter when initial filter prop changes // Reset filter when initial filter prop changes

View File

@ -63,12 +63,11 @@ export type Store = BaseStore &
Datasource.Store & Datasource.Store &
Validation.Store & Validation.Store &
Users.Store & Users.Store &
Menu.Store & { Menu.Store &
Filter.Store &
UI.Store & {
// TODO while typing the rest of stores // TODO while typing the rest of stores
fetch: Writable<any> fetch: Writable<any>
filter: Writable<any>
inlineFilters: Writable<any>
allFilters: Writable<any>
sort: Writable<any> sort: Writable<any>
initialFilter: Writable<any> initialFilter: Writable<any>
initialSortColumn: Writable<any> initialSortColumn: Writable<any>
@ -79,13 +78,11 @@ export type Store = BaseStore &
dispatch: (event: string, data: any) => any dispatch: (event: string, data: any) => any
notifications: Writable<any> notifications: Writable<any>
schemaOverrides: Writable<any> schemaOverrides: Writable<any>
focusedCellId: Writable<any>
previousFocusedRowId: Writable<string>
gridID: string gridID: string
selectedRows: Writable<any> props: Writable<any>
selectedRowCount: Writable<any> rowLookupMap: Writable<any>
selectedCellMap: Writable<any> width: Writable<number>
selectedCellCount: Writable<any> fixedRowHeight: Writable<number>
} }
export const attachStores = (context: Store): Store => { export const attachStores = (context: Store): Store => {

View File

@ -15,7 +15,16 @@ interface MenuStore {
menu: Writable<MenuStoreData> menu: Writable<MenuStoreData>
} }
export type Store = MenuStore interface MenuActions {
menu: MenuStore["menu"] & {
actions: {
open: (cellId: string, e: MouseEvent) => void
close: () => void
}
}
}
export type Store = MenuStore & MenuActions
export const createStores = () => { export const createStores = () => {
const menu = writable<MenuStoreData>({ const menu = writable<MenuStoreData>({
@ -30,7 +39,7 @@ export const createStores = () => {
} }
} }
export const createActions = (context: StoreContext) => { export const createActions = (context: StoreContext): MenuActions => {
const { const {
menu, menu,
focusedCellId, focusedCellId,
@ -60,7 +69,7 @@ export const createActions = (context: StoreContext) => {
let multiRowMode = false let multiRowMode = false
if (get(selectedRowCount) > 1) { if (get(selectedRowCount) > 1) {
const { rowId } = parseCellID(cellId) const { rowId } = parseCellID(cellId)
if (get(selectedRows)[rowId]) { if (rowId !== undefined && get(selectedRows)[rowId]) {
multiRowMode = true multiRowMode = true
} }
} }

View File

@ -1,4 +1,4 @@
import { writable, get, derived } from "svelte/store" import { writable, get, derived, Writable, Readable } from "svelte/store"
import { tick } from "svelte" import { tick } from "svelte"
import { import {
DefaultRowHeight, DefaultRowHeight,
@ -7,8 +7,44 @@ import {
NewRowID, NewRowID,
} from "../lib/constants" } from "../lib/constants"
import { getCellID, parseCellID } from "../lib/utils" import { getCellID, parseCellID } from "../lib/utils"
import { Store as StoreContext } from "."
export const createStores = context => { export interface UIStore {
focusedCellId: Writable<string | null>
focusedCellAPI: Writable<{
isReadonly: () => boolean
} | null>
selectedRows: Writable<Record<string, boolean>>
hoveredRowId: Writable<string | null>
rowHeight: Writable<number>
previousFocusedRowId: Writable<string | null>
previousFocusedCellId: Writable<string | null>
gridFocused: Writable<boolean>
keyboardBlocked: Writable<boolean>
isDragging: Writable<boolean>
buttonColumnWidth: Writable<number>
cellSelection: Writable<{
active: boolean
sourceCellId: string | null
targetCellId: string | null
}>
}
export interface UIDerivedStore {
focusedRowId: Readable<string | null>
focusedRow: Readable<string | undefined>
contentLines: Readable<3 | 2 | 1>
compact: Readable<boolean>
selectedRowCount: Readable<number>
isSelectingCells: Readable<boolean>
selectedCells: Readable<string[][]> & { actions: any }
selectedCellMap: Readable<Record<string, boolean>>
selectedCellCount: Readable<number>
}
export type Store = UIStore & UIDerivedStore
export const createStores = (context: StoreContext): UIStore => {
const { props } = context const { props } = context
const focusedCellId = writable(null) const focusedCellId = writable(null)
const focusedCellAPI = writable(null) const focusedCellAPI = writable(null)
@ -43,7 +79,7 @@ export const createStores = context => {
} }
} }
export const deriveStores = context => { export const deriveStores = (context: StoreContext) => {
const { const {
focusedCellId, focusedCellId,
rows, rows,
@ -65,6 +101,10 @@ export const deriveStores = context => {
const focusedRow = derived( const focusedRow = derived(
[focusedRowId, rowLookupMap], [focusedRowId, rowLookupMap],
([$focusedRowId, $rowLookupMap]) => { ([$focusedRowId, $rowLookupMap]) => {
if ($focusedRowId === undefined) {
return
}
if ($focusedRowId === NewRowID) { if ($focusedRowId === NewRowID) {
return { _id: NewRowID } return { _id: NewRowID }
} }
@ -116,8 +156,8 @@ export const deriveStores = context => {
} }
// Row indices // Row indices
const sourceRowIndex = $rowLookupMap[sourceInfo.rowId]?.__idx const sourceRowIndex = $rowLookupMap[sourceInfo.rowId!]?.__idx
const targetRowIndex = $rowLookupMap[targetInfo.rowId]?.__idx const targetRowIndex = $rowLookupMap[targetInfo.rowId!]?.__idx
if (sourceRowIndex == null || targetRowIndex == null) { if (sourceRowIndex == null || targetRowIndex == null) {
return [] return []
} }
@ -128,8 +168,8 @@ export const deriveStores = context => {
upperRowIndex = Math.min(upperRowIndex, lowerRowIndex + 49) upperRowIndex = Math.min(upperRowIndex, lowerRowIndex + 49)
// Column indices // Column indices
const sourceColIndex = $columnLookupMap[sourceInfo.field].__idx const sourceColIndex = $columnLookupMap[sourceInfo.field!].__idx || 0
const targetColIndex = $columnLookupMap[targetInfo.field].__idx const targetColIndex = $columnLookupMap[targetInfo.field!].__idx || 0
const lowerColIndex = Math.min(sourceColIndex, targetColIndex) const lowerColIndex = Math.min(sourceColIndex, targetColIndex)
const upperColIndex = Math.max(sourceColIndex, targetColIndex) const upperColIndex = Math.max(sourceColIndex, targetColIndex)
@ -151,7 +191,7 @@ export const deriveStores = context => {
// Derive a quick lookup map of the selected cells // Derive a quick lookup map of the selected cells
const selectedCellMap = derived(selectedCells, $selectedCells => { const selectedCellMap = derived(selectedCells, $selectedCells => {
let map = {} let map: Record<string, boolean> = {}
for (let row of $selectedCells) { for (let row of $selectedCells) {
for (let cell of row) { for (let cell of row) {
map[cell] = true map[cell] = true
@ -178,7 +218,7 @@ export const deriveStores = context => {
} }
} }
export const createActions = context => { export const createActions = (context: StoreContext) => {
const { const {
focusedCellId, focusedCellId,
hoveredRowId, hoveredRowId,
@ -190,7 +230,7 @@ export const createActions = context => {
selectedCells, selectedCells,
} = context } = context
// Keep the last selected index to use with bulk selection // Keep the last selected index to use with bulk selection
let lastSelectedIndex = null let lastSelectedIndex: number | null = null
// Callback when leaving the grid, deselecting all focussed or selected items // Callback when leaving the grid, deselecting all focussed or selected items
const blur = () => { const blur = () => {
@ -200,7 +240,7 @@ export const createActions = context => {
} }
// Toggles whether a certain row ID is selected or not // Toggles whether a certain row ID is selected or not
const toggleSelectedRow = id => { const toggleSelectedRow = (id: string) => {
selectedRows.update(state => { selectedRows.update(state => {
let newState = { let newState = {
...state, ...state,
@ -215,7 +255,7 @@ export const createActions = context => {
}) })
} }
const bulkSelectRows = id => { const bulkSelectRows = (id: string) => {
if (!get(selectedRowCount)) { if (!get(selectedRowCount)) {
toggleSelectedRow(id) toggleSelectedRow(id)
return return
@ -241,7 +281,7 @@ export const createActions = context => {
}) })
} }
const startCellSelection = sourceCellId => { const startCellSelection = (sourceCellId: string) => {
cellSelection.set({ cellSelection.set({
active: true, active: true,
sourceCellId, sourceCellId,
@ -249,7 +289,7 @@ export const createActions = context => {
}) })
} }
const updateCellSelection = targetCellId => { const updateCellSelection = (targetCellId: string) => {
cellSelection.update(state => ({ cellSelection.update(state => ({
...state, ...state,
targetCellId, targetCellId,
@ -263,7 +303,7 @@ export const createActions = context => {
})) }))
} }
const selectCellRange = (source, target) => { const selectCellRange = (source: string, target: string) => {
cellSelection.set({ cellSelection.set({
active: false, active: false,
sourceCellId: source, sourceCellId: source,
@ -305,7 +345,7 @@ export const createActions = context => {
} }
} }
export const initialise = context => { export const initialise = (context: StoreContext) => {
const { const {
focusedRowId, focusedRowId,
previousFocusedRowId, previousFocusedRowId,
@ -332,7 +372,7 @@ export const initialise = context => {
const $focusedRowId = get(focusedRowId) const $focusedRowId = get(focusedRowId)
const $selectedRows = get(selectedRows) const $selectedRows = get(selectedRows)
const $hoveredRowId = get(hoveredRowId) const $hoveredRowId = get(hoveredRowId)
const hasRow = id => $rowLookupMap[id] != null const hasRow = (id: string) => $rowLookupMap[id] != null
// Check focused cell // Check focused cell
if ($focusedRowId && !hasRow($focusedRowId)) { if ($focusedRowId && !hasRow($focusedRowId)) {
@ -362,13 +402,13 @@ export const initialise = context => {
}) })
// Remember the last focused row ID so that we can store the previous one // Remember the last focused row ID so that we can store the previous one
let lastFocusedRowId = null let lastFocusedRowId: string | null = null
focusedRowId.subscribe(id => { focusedRowId.subscribe(id => {
previousFocusedRowId.set(lastFocusedRowId) previousFocusedRowId.set(lastFocusedRowId)
lastFocusedRowId = id lastFocusedRowId = id
}) })
let lastFocusedCellId = null let lastFocusedCellId: string | null = null
focusedCellId.subscribe(id => { focusedCellId.subscribe(id => {
// Remember the last focused cell ID so that we can store the previous one // Remember the last focused cell ID so that we can store the previous one
previousFocusedCellId.set(lastFocusedCellId) previousFocusedCellId.set(lastFocusedCellId)

View File

@ -33,10 +33,12 @@ export const deriveStores = (context: StoreContext): DerivedValidationStore => {
// Extract row ID from all errored cell IDs // Extract row ID from all errored cell IDs
if (error) { if (error) {
const { rowId } = parseCellID(key) const { rowId } = parseCellID(key)
if (!map[rowId]) { if (rowId !== undefined) {
map[rowId] = [] if (!map[rowId]) {
map[rowId] = []
}
map[rowId].push(key)
} }
map[rowId].push(key)
} }
}) })
return map return map

View File

@ -149,6 +149,7 @@ export async function create(ctx: Ctx<CreateViewRequest, CreateViewResponse>) {
sort: view.sort, sort: view.sort,
schema, schema,
primaryDisplay: view.primaryDisplay, primaryDisplay: view.primaryDisplay,
rowHeight: view.rowHeight,
} }
const result = await sdk.views.create(tableId, parsedView) const result = await sdk.views.create(tableId, parsedView)
@ -229,6 +230,7 @@ export async function update(ctx: Ctx<UpdateViewRequest, UpdateViewResponse>) {
sort: view.sort, sort: view.sort,
schema, schema,
primaryDisplay: view.primaryDisplay, primaryDisplay: view.primaryDisplay,
rowHeight: view.rowHeight,
} }
const { view: result, existingView } = await sdk.views.update( const { view: result, existingView } = await sdk.views.update(

View File

@ -162,6 +162,7 @@ if (descriptions.length) {
visible: true, visible: true,
}, },
}, },
rowHeight: generator.integer(),
} }
const res = await config.api.viewV2.create(newView) const res = await config.api.viewV2.create(newView)
@ -224,6 +225,7 @@ if (descriptions.length) {
visible: true, visible: true,
}, },
}, },
rowHeight: generator.integer(),
} }
const res = await config.api.viewV2.create(newView) const res = await config.api.viewV2.create(newView)
expect(events.view.created).toHaveBeenCalledTimes(1) expect(events.view.created).toHaveBeenCalledTimes(1)
@ -1069,6 +1071,7 @@ if (descriptions.length) {
readonly: true, readonly: true,
}, },
}, },
rowHeight: generator.integer(),
} }
await config.api.viewV2.update(updatedData) await config.api.viewV2.update(updatedData)

View File

@ -99,6 +99,7 @@ export interface ViewV2 {
type?: SortType type?: SortType
} }
schema?: ViewV2Schema schema?: ViewV2Schema
rowHeight?: number
} }
export interface PublicAPIView extends Omit<ViewV2, "query" | "queryUI"> { export interface PublicAPIView extends Omit<ViewV2, "query" | "queryUI"> {

View File

@ -9,11 +9,13 @@ export type UIColumn = FieldSchema & {
subField: string subField: string
} }
primaryDisplay?: boolean primaryDisplay?: boolean
schema?: { schema: {
disabled: boolean disabled: boolean
type: FieldType type: FieldType
readonly: boolean readonly: boolean
autocolumn: boolean autocolumn: boolean
} }
calculationType: CalculationType calculationType: CalculationType
__idx?: number
__left?: number
} }

View File

@ -0,0 +1,5 @@
import { LegacyFilter } from "@budibase/types"
export type UILegacyFilter = LegacyFilter & {
id: string
}

View File

@ -3,3 +3,4 @@ export * from "./datasource"
export * from "./table" export * from "./table"
export * from "./view" export * from "./view"
export * from "./user" export * from "./user"
export * from "./filters"