diff --git a/packages/frontend-core/src/components/grid/lib/utils.js b/packages/frontend-core/src/components/grid/lib/utils.js index f1f33d9950..ee74a14bf0 100644 --- a/packages/frontend-core/src/components/grid/lib/utils.js +++ b/packages/frontend-core/src/components/grid/lib/utils.js @@ -1,3 +1,5 @@ +// TODO: remove when all stores are typed + import { GeneratedIDPrefix, CellIDSeparator } from "./constants" import { Helpers } from "@budibase/bbui" diff --git a/packages/frontend-core/src/components/grid/lib/utils.ts b/packages/frontend-core/src/components/grid/lib/utils.ts new file mode 100644 index 0000000000..14aee663b7 --- /dev/null +++ b/packages/frontend-core/src/components/grid/lib/utils.ts @@ -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) +} diff --git a/packages/frontend-core/src/components/grid/stores/filter.js b/packages/frontend-core/src/components/grid/stores/filter.ts similarity index 61% rename from packages/frontend-core/src/components/grid/stores/filter.js rename to packages/frontend-core/src/components/grid/stores/filter.ts index e7adc356ae..36f6cd9483 100644 --- a/packages/frontend-core/src/components/grid/stores/filter.js +++ b/packages/frontend-core/src/components/grid/stores/filter.ts @@ -1,8 +1,28 @@ -import { get, derived } from "svelte/store" -import { FieldType, UILogicalOperator } from "@budibase/types" +import { get, derived, Writable, Readable } from "svelte/store" +import { + ArrayOperator, + BasicOperator, + FieldType, + UIColumn, + UILegacyFilter, + UILogicalOperator, + UISearchFilter, +} from "@budibase/types" +import { Store as StoreContext } from "." import { memo } from "../../../utils/memo" -export const createStores = context => { +export interface FilterStore { + filter: Writable + inlineFilters: Writable +} + +export interface FilterDerivedStore { + allFilters: Readable +} + +export type Store = FilterStore & FilterDerivedStore + +export const createStores = (context: StoreContext): FilterStore => { const { props } = context // 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 allFilters = derived( [filter, inlineFilters], @@ -24,7 +44,7 @@ export const deriveStores = context => { if (!$inlineFilters?.length) { return $filter } - let allFilters = { + const allFilters: UISearchFilter = { logicalOperator: UILogicalOperator.ALL, groups: [ { @@ -33,12 +53,13 @@ export const deriveStores = context => { }, ], } + // Just use inline if no filter if (!$filter?.groups?.length) { return allFilters } // Join them together if both - allFilters.groups = [...allFilters.groups, ...$filter.groups] + allFilters.groups = [...allFilters.groups!, ...$filter.groups] return allFilters } ) @@ -48,16 +69,16 @@ export const deriveStores = context => { } } -export const createActions = context => { +export const createActions = (context: StoreContext) => { const { filter, inlineFilters } = context - const addInlineFilter = (column, value) => { + const addInlineFilter = (column: UIColumn, value: string) => { const filterId = `inline-${column.name}` const type = column.schema.type - let inlineFilter = { + const inlineFilter: UILegacyFilter = { field: column.name, id: filterId, - operator: "string", + operator: BasicOperator.STRING, valueType: "value", type, value, @@ -66,11 +87,11 @@ export const createActions = context => { // Add overrides specific so the certain column type if (type === FieldType.NUMBER) { inlineFilter.value = parseFloat(value) - inlineFilter.operator = "equal" + inlineFilter.operator = BasicOperator.EQUAL } else if (type === FieldType.BIGINT) { - inlineFilter.operator = "equal" + inlineFilter.operator = BasicOperator.EQUAL } else if (type === FieldType.ARRAY) { - inlineFilter.operator = "contains" + inlineFilter.operator = ArrayOperator.CONTAINS } inlineFilters.update($inlineFilters => { @@ -95,7 +116,7 @@ export const createActions = context => { } } -export const initialise = context => { +export const initialise = (context: StoreContext) => { const { filter, initialFilter } = context // Reset filter when initial filter prop changes diff --git a/packages/frontend-core/src/components/grid/stores/index.ts b/packages/frontend-core/src/components/grid/stores/index.ts index d0413cb80a..81e0e824b0 100644 --- a/packages/frontend-core/src/components/grid/stores/index.ts +++ b/packages/frontend-core/src/components/grid/stores/index.ts @@ -63,12 +63,11 @@ export type Store = BaseStore & Datasource.Store & Validation.Store & Users.Store & - Menu.Store & { + Menu.Store & + Filter.Store & + UI.Store & { // TODO while typing the rest of stores fetch: Writable - filter: Writable - inlineFilters: Writable - allFilters: Writable sort: Writable initialFilter: Writable initialSortColumn: Writable @@ -79,13 +78,11 @@ export type Store = BaseStore & dispatch: (event: string, data: any) => any notifications: Writable schemaOverrides: Writable - focusedCellId: Writable - previousFocusedRowId: Writable gridID: string - selectedRows: Writable - selectedRowCount: Writable - selectedCellMap: Writable - selectedCellCount: Writable + props: Writable + rowLookupMap: Writable + width: Writable + fixedRowHeight: Writable } export const attachStores = (context: Store): Store => { diff --git a/packages/frontend-core/src/components/grid/stores/menu.ts b/packages/frontend-core/src/components/grid/stores/menu.ts index 27e41c412b..ae29ae1de3 100644 --- a/packages/frontend-core/src/components/grid/stores/menu.ts +++ b/packages/frontend-core/src/components/grid/stores/menu.ts @@ -15,7 +15,16 @@ interface MenuStore { menu: Writable } -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 = () => { const menu = writable({ @@ -30,7 +39,7 @@ export const createStores = () => { } } -export const createActions = (context: StoreContext) => { +export const createActions = (context: StoreContext): MenuActions => { const { menu, focusedCellId, @@ -60,7 +69,7 @@ export const createActions = (context: StoreContext) => { let multiRowMode = false if (get(selectedRowCount) > 1) { const { rowId } = parseCellID(cellId) - if (get(selectedRows)[rowId]) { + if (rowId !== undefined && get(selectedRows)[rowId]) { multiRowMode = true } } diff --git a/packages/frontend-core/src/components/grid/stores/ui.js b/packages/frontend-core/src/components/grid/stores/ui.ts similarity index 82% rename from packages/frontend-core/src/components/grid/stores/ui.js rename to packages/frontend-core/src/components/grid/stores/ui.ts index 7f39116b05..23710668b7 100644 --- a/packages/frontend-core/src/components/grid/stores/ui.js +++ b/packages/frontend-core/src/components/grid/stores/ui.ts @@ -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 { DefaultRowHeight, @@ -7,8 +7,44 @@ import { NewRowID, } from "../lib/constants" import { getCellID, parseCellID } from "../lib/utils" +import { Store as StoreContext } from "." -export const createStores = context => { +export interface UIStore { + focusedCellId: Writable + focusedCellAPI: Writable<{ + isReadonly: () => boolean + } | null> + selectedRows: Writable> + hoveredRowId: Writable + rowHeight: Writable + previousFocusedRowId: Writable + previousFocusedCellId: Writable + gridFocused: Writable + keyboardBlocked: Writable + isDragging: Writable + buttonColumnWidth: Writable + cellSelection: Writable<{ + active: boolean + sourceCellId: string | null + targetCellId: string | null + }> +} + +export interface UIDerivedStore { + focusedRowId: Readable + focusedRow: Readable + contentLines: Readable<3 | 2 | 1> + compact: Readable + selectedRowCount: Readable + isSelectingCells: Readable + selectedCells: Readable & { actions: any } + selectedCellMap: Readable> + selectedCellCount: Readable +} + +export type Store = UIStore & UIDerivedStore + +export const createStores = (context: StoreContext): UIStore => { const { props } = context const focusedCellId = writable(null) const focusedCellAPI = writable(null) @@ -43,7 +79,7 @@ export const createStores = context => { } } -export const deriveStores = context => { +export const deriveStores = (context: StoreContext) => { const { focusedCellId, rows, @@ -65,6 +101,10 @@ export const deriveStores = context => { const focusedRow = derived( [focusedRowId, rowLookupMap], ([$focusedRowId, $rowLookupMap]) => { + if ($focusedRowId === undefined) { + return + } + if ($focusedRowId === NewRowID) { return { _id: NewRowID } } @@ -116,8 +156,8 @@ export const deriveStores = context => { } // Row indices - const sourceRowIndex = $rowLookupMap[sourceInfo.rowId]?.__idx - const targetRowIndex = $rowLookupMap[targetInfo.rowId]?.__idx + const sourceRowIndex = $rowLookupMap[sourceInfo.rowId!]?.__idx + const targetRowIndex = $rowLookupMap[targetInfo.rowId!]?.__idx if (sourceRowIndex == null || targetRowIndex == null) { return [] } @@ -128,8 +168,8 @@ export const deriveStores = context => { upperRowIndex = Math.min(upperRowIndex, lowerRowIndex + 49) // Column indices - const sourceColIndex = $columnLookupMap[sourceInfo.field].__idx - const targetColIndex = $columnLookupMap[targetInfo.field].__idx + const sourceColIndex = $columnLookupMap[sourceInfo.field!].__idx || 0 + const targetColIndex = $columnLookupMap[targetInfo.field!].__idx || 0 const lowerColIndex = Math.min(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 const selectedCellMap = derived(selectedCells, $selectedCells => { - let map = {} + let map: Record = {} for (let row of $selectedCells) { for (let cell of row) { map[cell] = true @@ -178,7 +218,7 @@ export const deriveStores = context => { } } -export const createActions = context => { +export const createActions = (context: StoreContext) => { const { focusedCellId, hoveredRowId, @@ -190,7 +230,7 @@ export const createActions = context => { selectedCells, } = context // 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 const blur = () => { @@ -200,7 +240,7 @@ export const createActions = context => { } // Toggles whether a certain row ID is selected or not - const toggleSelectedRow = id => { + const toggleSelectedRow = (id: string) => { selectedRows.update(state => { let newState = { ...state, @@ -215,7 +255,7 @@ export const createActions = context => { }) } - const bulkSelectRows = id => { + const bulkSelectRows = (id: string) => { if (!get(selectedRowCount)) { toggleSelectedRow(id) return @@ -241,7 +281,7 @@ export const createActions = context => { }) } - const startCellSelection = sourceCellId => { + const startCellSelection = (sourceCellId: string) => { cellSelection.set({ active: true, sourceCellId, @@ -249,7 +289,7 @@ export const createActions = context => { }) } - const updateCellSelection = targetCellId => { + const updateCellSelection = (targetCellId: string) => { cellSelection.update(state => ({ ...state, targetCellId, @@ -263,7 +303,7 @@ export const createActions = context => { })) } - const selectCellRange = (source, target) => { + const selectCellRange = (source: string, target: string) => { cellSelection.set({ active: false, sourceCellId: source, @@ -305,7 +345,7 @@ export const createActions = context => { } } -export const initialise = context => { +export const initialise = (context: StoreContext) => { const { focusedRowId, previousFocusedRowId, @@ -332,7 +372,7 @@ export const initialise = context => { const $focusedRowId = get(focusedRowId) const $selectedRows = get(selectedRows) const $hoveredRowId = get(hoveredRowId) - const hasRow = id => $rowLookupMap[id] != null + const hasRow = (id: string) => $rowLookupMap[id] != null // Check focused cell 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 - let lastFocusedRowId = null + let lastFocusedRowId: string | null = null focusedRowId.subscribe(id => { previousFocusedRowId.set(lastFocusedRowId) lastFocusedRowId = id }) - let lastFocusedCellId = null + let lastFocusedCellId: string | null = null focusedCellId.subscribe(id => { // Remember the last focused cell ID so that we can store the previous one previousFocusedCellId.set(lastFocusedCellId) diff --git a/packages/frontend-core/src/components/grid/stores/validation.ts b/packages/frontend-core/src/components/grid/stores/validation.ts index 32bb1cf978..f118020b16 100644 --- a/packages/frontend-core/src/components/grid/stores/validation.ts +++ b/packages/frontend-core/src/components/grid/stores/validation.ts @@ -33,10 +33,12 @@ export const deriveStores = (context: StoreContext): DerivedValidationStore => { // Extract row ID from all errored cell IDs if (error) { const { rowId } = parseCellID(key) - if (!map[rowId]) { - map[rowId] = [] + if (rowId !== undefined) { + if (!map[rowId]) { + map[rowId] = [] + } + map[rowId].push(key) } - map[rowId].push(key) } }) return map diff --git a/packages/server/src/api/controllers/view/viewsV2.ts b/packages/server/src/api/controllers/view/viewsV2.ts index da579efed7..5376ce49c3 100644 --- a/packages/server/src/api/controllers/view/viewsV2.ts +++ b/packages/server/src/api/controllers/view/viewsV2.ts @@ -149,6 +149,7 @@ export async function create(ctx: Ctx) { sort: view.sort, schema, primaryDisplay: view.primaryDisplay, + rowHeight: view.rowHeight, } const result = await sdk.views.create(tableId, parsedView) @@ -229,6 +230,7 @@ export async function update(ctx: Ctx) { sort: view.sort, schema, primaryDisplay: view.primaryDisplay, + rowHeight: view.rowHeight, } const { view: result, existingView } = await sdk.views.update( diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index ba2ad422eb..6ace7e256b 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -162,6 +162,7 @@ if (descriptions.length) { visible: true, }, }, + rowHeight: generator.integer(), } const res = await config.api.viewV2.create(newView) @@ -224,6 +225,7 @@ if (descriptions.length) { visible: true, }, }, + rowHeight: generator.integer(), } const res = await config.api.viewV2.create(newView) expect(events.view.created).toHaveBeenCalledTimes(1) @@ -1069,6 +1071,7 @@ if (descriptions.length) { readonly: true, }, }, + rowHeight: generator.integer(), } await config.api.viewV2.update(updatedData) diff --git a/packages/types/src/documents/app/view.ts b/packages/types/src/documents/app/view.ts index 1170284b15..fd97cee409 100644 --- a/packages/types/src/documents/app/view.ts +++ b/packages/types/src/documents/app/view.ts @@ -99,6 +99,7 @@ export interface ViewV2 { type?: SortType } schema?: ViewV2Schema + rowHeight?: number } export interface PublicAPIView extends Omit { diff --git a/packages/types/src/ui/stores/grid/columns.ts b/packages/types/src/ui/stores/grid/columns.ts index d12739fffe..5f02efa8cb 100644 --- a/packages/types/src/ui/stores/grid/columns.ts +++ b/packages/types/src/ui/stores/grid/columns.ts @@ -9,11 +9,13 @@ export type UIColumn = FieldSchema & { subField: string } primaryDisplay?: boolean - schema?: { + schema: { disabled: boolean type: FieldType readonly: boolean autocolumn: boolean } calculationType: CalculationType + __idx?: number + __left?: number } diff --git a/packages/types/src/ui/stores/grid/filters.ts b/packages/types/src/ui/stores/grid/filters.ts new file mode 100644 index 0000000000..d75f32370c --- /dev/null +++ b/packages/types/src/ui/stores/grid/filters.ts @@ -0,0 +1,5 @@ +import { LegacyFilter } from "@budibase/types" + +export type UILegacyFilter = LegacyFilter & { + id: string +} diff --git a/packages/types/src/ui/stores/grid/index.ts b/packages/types/src/ui/stores/grid/index.ts index b6a152ed73..aa68c3f207 100644 --- a/packages/types/src/ui/stores/grid/index.ts +++ b/packages/types/src/ui/stores/grid/index.ts @@ -3,3 +3,4 @@ export * from "./datasource" export * from "./table" export * from "./view" export * from "./user" +export * from "./filters"