Skip to content
Snippets Groups Projects
GridController.js 43 KiB
Newer Older
  • Learn to ignore specific revisions
  • import _ from 'lodash'
    import autoBind from 'auto-bind'
    
    import { stringify } from 'csv-stringify'
    
    import GridFilterOptions from '@src/js/components/common/grid/GridFilterOptions.js'
    
    import GridExportOptions from '@src/js/components/common/grid/GridExportOptions.js'
    
    import GridPagingOptions from '@src/js/components/common/grid/GridPagingOptions.js'
    import GridSortingOptions from '@src/js/components/common/grid/GridSortingOptions.js'
    import compare from '@src/js/common/compare.js'
    
    const LOCAL_GRID_RELOAD_PERIOD = 200
    const REMOTE_GRID_RELOAD_PERIOD = 500
    
    export default class GridController {
      constructor() {
        autoBind(this)
    
        if (props.filterModes) {
          filterMode = this._getEnumValue(filterMode, props.filterModes)
          if (!filterMode) {
            filterMode = props.filterModes.length > 0 ? props.filterModes[0] : null
          }
        }
    
    
        if (props.sortings) {
          props.sortings.forEach(sorting => {
            sortings.push({
              columnName: sorting.columnName,
              sortDirection: sorting.sortDirection
                ? sorting.sortDirection
                : GridSortingOptions.ASC
            })
          })
        } else if (props.sort) {
    
          sortings.push({
            columnName: props.sort,
            sortDirection: props.sortDirection
              ? props.sortDirection
              : GridSortingOptions.ASC
          })
        }
    
    
        context.initState({
          loaded: false,
          loading: false,
    
          globalFilter: {
            operator: GridFilterOptions.OPERATOR_AND,
            text: null
          },
    
          columnsVisibility: {},
          columnsSorting: [],
    
            columns: GridExportOptions.COLUMNS.VISIBLE,
            rows: GridExportOptions.ROWS.CURRENT_PAGE,
            values: GridExportOptions.VALUES.RICH_TEXT,
    
            includeDependencies: true
    
        try {
          const props = this.context.getProps()
    
          if ((props.rows && props.loadRows) || (!props.rows && !props.loadRows)) {
            throw new Error(
              'Incorrect grid configuration. Please set "rows" or "loadRows" property.'
            )
          }
    
          if (
            (props.columns && props.loadColumns) ||
            (!props.columns && !props.loadColumns)
          ) {
            throw new Error(
              'Incorrect grid configuration. Please set "columns" or "loadColumns" property.'
            )
          }
    
          await this.context.setState(() => ({
            loading: true
          }))
    
          const state = this.context.getState()
          const newState = {
            ...state,
            heights: {},
            loading: false,
            loaded: true
          }
    
          if (!state.loaded) {
            settings = await this._loadSettings()
            _.merge(newState, settings)
          }
    
          if (props.rows) {
            result.rows = props.rows
            result.totalCount = props.rows.length
            result.local = true
          } else if (props.loadRows) {
            const columns = {}
    
            newState.allColumns.forEach(column => {
              columns[column.name] = column
            })
    
            const loadedResult = await props.loadRows({
              columns: columns,
              filterMode: newState.filterMode,
              filters: newState.filters,
              globalFilter: newState.globalFilter,
              page: newState.page,
              pageSize: newState.pageSize,
              sortings: newState.sortings
            })
    
            if (_.isArray(loadedResult)) {
              result.rows = loadedResult
              result.totalCount = loadedResult.length
              result.local = true
            } else {
              result.rows = loadedResult.rows
              result.totalCount = loadedResult.totalCount
              result.local = false
            }
    
          newState.local = result.local
    
          if (result.local) {
            const { newAllColumns, newColumnsVisibility, newColumnsSorting } =
              await this._loadColumns(
                result.rows,
                newState.columnsVisibility,
                newState.columnsSorting
              )
    
            newState.allColumns = newAllColumns
            newState.columnsVisibility = newColumnsVisibility
            newState.columnsSorting = newColumnsSorting
    
            newState.allRows = result.rows
            newState.filteredRows = this._filterRows(
              newState.allRows,
              newState.allColumns,
              newState.columnsVisibility,
              newState.filterMode,
              newState.filters,
              newState.globalFilter
            )
            newState.sortedRows = this._sortRows(
              newState.filteredRows,
              newState.allColumns,
              newState.sortings
            )
            newState.totalCount = newState.filteredRows.length
    
            const pageCount = Math.max(
              Math.ceil(newState.totalCount / newState.pageSize),
              1
            )
    
            newState.page = Math.min(newState.page, pageCount - 1)
            newState.rows = this._pageRows(
              newState.sortedRows,
              newState.page,
              newState.pageSize
            )
          } else {
            newState.allRows = result.rows
            newState.filteredRows = result.rows
            newState.sortedRows = result.rows
            newState.rows = result.rows
            newState.totalCount = result.totalCount
    
            const pageCount = Math.max(
              Math.ceil(result.totalCount / newState.pageSize),
              1
    
            newState.page = Math.min(newState.page, pageCount - 1)
    
            const { newAllColumns, newColumnsVisibility, newColumnsSorting } =
              await this._loadColumns(
                newState.rows,
                newState.columnsVisibility,
                newState.columnsSorting
              )
    
            newState.allColumns = newAllColumns
            newState.columnsVisibility = newColumnsVisibility
            newState.columnsSorting = newColumnsSorting
          }
    
          // do not update filters (this would override filter changes that a user could do while grid was loading)
          delete newState.filters
          delete newState.globalFilter
    
          await this.context.setState(newState)
    
          if (!state.loaded) {
            this.selectRow(props.selectedRowId)
            this.multiselectRows(props.multiselectedRowIds)
          } else {
            this.selectRow(newState.selectedRow ? newState.selectedRow.id : null)
            this.multiselectRows(Object.keys(newState.multiselectedRows))
          }
        } catch (error) {
          this._onError(error)
    
        }
      }
    
      async _loadColumns(rows, columnsVisibility, columnsSorting) {
    
        const props = this.context.getProps()
        const state = this.context.getState()
    
        const newColumnsVisibility = { ...columnsVisibility }
        const newColumnsSorting = [...columnsSorting]
    
    
        if (props.columns) {
          newAllColumns = props.columns
        } else if (props.loadColumns) {
          newAllColumns = await props.loadColumns(rows)
    
        newAllColumns = newAllColumns.map(newColumn => {
    
          if (newColumn.exportable && !newColumn.getValue) {
            throw new Error(
              'column.name cannot be exportable without getValue implementation'
            )
          }
    
          return this._loadColumn(newColumn)
    
        // If there is a filter value defined for a column and this column does not exist
        // in the new columns list then take it over from the previous columns list.
        // This may happen e.g. when a user is filtering by a dynamic column
        // and enters a filter value that does not match any row. Without this trick the column
        // would disappear and the user would not be able to clear the filter value.
    
        Object.keys(state.filters).forEach(columnName => {
          const newColumn = _.find(
            newAllColumns,
            newColumn => newColumn.name === columnName
          )
    
          if (!newColumn) {
            const existingColumn = _.find(
              state.allColumns,
              column => column.name === columnName
            )
            newAllColumns.push(existingColumn)
          }
        })
    
    
        newAllColumns.forEach((newColumn, newColumnIndex) => {
    
          let newColumnVisibility = newColumnsVisibility[newColumn.name]
    
    
          if (newColumnVisibility === undefined || !newColumn.configurable) {
            newColumnsVisibility[newColumn.name] = newColumn.visible
    
          }
    
          let newColumnSorting = _.findIndex(
            newColumnsSorting,
            columnName => columnName === newColumn.name
          )
    
            // If a column does not have a sorting value yet, then set its sorting to
            // the max sorting of the columns that were before it in the columns list
    
    
              .slice(0, newColumnIndex)
              .reduce((maxSorting, column) => {
                const sorting = _.findIndex(
                  newColumnsSorting,
                  columnName => columnName === column.name
                )
                return Math.max(sorting, maxSorting)
              }, -1)
            newColumnsSorting.splice(newColumnSorting + 1, 0, newColumn.name)
          }
        })
    
        return { newAllColumns, newColumnsVisibility, newColumnsSorting }
    
        const defaultMatches = function (value, filter) {
          if (filter) {
            return value !== null && value !== undefined
              ? String(value)
                  .trim()
                  .toUpperCase()
                  .includes(filter.trim().toUpperCase())
              : false
          } else {
            return true
          }
        }
    
        const defaultCompare = compare
    
    
        return {
          ...column,
          name: column.name,
          label: column.label,
          getValue: column.getValue,
    
            const value = column.getValue({ row, column, operation: 'match' })
    
            if (column.matchesValue) {
              return column.matchesValue({
    
                value,
                row,
                column,
                filter,
                defaultMatches
              })
            } else {
              return defaultMatches(value, filter)
            }
          },
    
            const value1 = column.getValue({
              row: row1,
              column,
              operation: 'compare'
            })
            const value2 = column.getValue({
              row: row2,
              column,
              operation: 'compare'
            })
    
            if (column.compareValue) {
              return column.compareValue({
    
                defaultCompare
              })
            } else {
              return defaultCompare(value1, value2)
            }
          },
    
          sortable: column.sortable === undefined ? true : column.sortable,
    
          filterable: column.filterable === undefined ? true : column.filterable,
          visible: column.visible === undefined ? true : column.visible,
          configurable:
    
            column.configurable === undefined ? true : column.configurable,
    
          exportable: column.exportable === undefined ? true : column.exportable,
    
          nowrap: column.nowrap === undefined ? false : column.nowrap,
          truncate: column.truncate === undefined ? false : column.truncate,
    
          metadata: column.metadata === undefined ? {} : column.metadata
    
      _sortColumns(columns, columnsSorting) {
        columns.sort((c1, c2) => {
          const c1Index = _.findIndex(
            columnsSorting,
            columnName => columnName === c1.name
    
          const c2Index = _.findIndex(
            columnsSorting,
            columnName => columnName === c2.name
    
        settings.filterMode = this._getEnumValue(
          loaded.filterMode,
          GridFilterOptions.FILTER_MODE_OPTIONS
        )
    
        if (props.filterModes) {
          settings.filterMode = this._getEnumValue(
            settings.filterMode,
            props.filterModes
          )
        }
    
          globalFilter.operator = this._getEnumValue(
            loaded.globalFilter.operator,
            GridFilterOptions.OPERATOR_OPTIONS
    
          if (globalFilter.operator !== undefined) {
            settings.globalFilter = globalFilter
          }
    
        settings.pageSize = this._getEnumValue(
          loaded.pageSize,
          GridPagingOptions.PAGE_SIZE_OPTIONS
        )
    
    
        if (_.isArray(loaded.sortings)) {
          const sortings = []
          loaded.sortings.forEach(loadedSorting => {
            if (_.isObject(loadedSorting)) {
              const sorting = {}
              sorting.columnName = this._getStringValue(loadedSorting.columnName)
              sorting.sortDirection = this._getEnumValue(
                loadedSorting.sortDirection,
                GridSortingOptions.SORTING_DIRECTION_OPTIONS
              )
              if (
                sorting.columnName !== undefined &&
                sorting.sortDirection !== undefined
              ) {
                sortings.push(sorting)
              }
            }
          })
          if (sortings.length > 0) {
            settings.sortings = sortings
          }
        }
    
        if (settings.sortings === undefined) {
          const sort = this._getStringValue(loaded.sort)
          const sortDirection = this._getEnumValue(
            loaded.sortDirection,
            GridSortingOptions.SORTING_DIRECTION_OPTIONS
          )
          if (sort !== undefined && sortDirection !== undefined) {
            settings.sortings = [
              {
                columnName: sort,
                sortDirection: sortDirection
              }
            ]
          }
        }
    
    
        settings.columnsVisibility = this._getObjectValue(loaded.columnsVisibility)
        settings.columnsSorting = this._getArrayValue(loaded.columnsSorting)
    
          exportOptions.columns = this._getEnumValue(
            loaded.exportOptions.columns,
    
          exportOptions.rows = this._getEnumValue(
            loaded.exportOptions.rows,
    
          )
          exportOptions.values = this._getEnumValue(
            loaded.exportOptions.values,
    
          exportOptions.includeDependencies = this._getBooleanValue(
            loaded.exportOptions.includeDependencies
          )
    
        const { onSettingsChange } = this.context.getProps()
    
        if (onSettingsChange) {
          const state = this.context.getState()
    
            globalFilter: {
              operator: state.globalFilter.operator
            },
    
            columnsSorting: state.columnsSorting,
            exportOptions: state.exportOptions
    
      _filterRows(
        rows,
        columns,
        columnsVisibility,
        filterMode,
        filters,
        globalFilter
      ) {
        if (filterMode === GridFilterOptions.GLOBAL_FILTER) {
    
            if (globalFilter.operator === GridFilterOptions.OPERATOR_AND) {
              rowMatches = true
            } else if (globalFilter.operator === GridFilterOptions.OPERATOR_OR) {
              rowMatches = false
            }
    
            tokens: for (let t = 0; t < tokens.length; t++) {
              let token = tokens[t]
    
              columns: for (let c = 0; c < columns.length; c++) {
                let column = columns[c]
    
                  rowMatchesToken = column.matches(row, token)
                  if (rowMatchesToken) {
                    break columns
                  }
    
              if (globalFilter.operator === GridFilterOptions.OPERATOR_AND) {
                rowMatches = rowMatches && rowMatchesToken
                if (!rowMatches) {
                  break tokens
                }
              } else if (globalFilter.operator === GridFilterOptions.OPERATOR_OR) {
                rowMatches = rowMatches || rowMatchesToken
                if (rowMatches) {
                  break tokens
                }
              }
    
        } else if (filterMode === GridFilterOptions.COLUMN_FILTERS) {
          return _.filter([...rows], row => {
            let matchesAll = true
            columns.forEach(column => {
              let visible = columnsVisibility[column.name]
              if (visible) {
                let filter = filters[column.name]
    
                  matchesAll = matchesAll && column.matches(row, filter)
                }
              }
            })
            return matchesAll
          })
    
      _sortRows(rows, columns, sortings) {
        if (sortings && sortings.length > 0) {
          const columnSortings = []
    
          sortings.forEach(sorting => {
            const column = _.find(columns, ['name', sorting.columnName])
            if (column) {
              columnSortings.push({
                column,
                sorting
              })
            }
          })
    
          if (columnSortings.length > 0) {
    
              let result = 0
              let index = 0
              while (index < columnSortings.length && result === 0) {
                const { column, sorting } = columnSortings[index]
                const sign =
                  sorting.sortDirection === GridSortingOptions.ASC ? 1 : -1
                result = sign * column.compare(t1, t2, sorting.sortDirection)
                index++
              }
              return result
    
            })
          }
        }
    
        return rows
      }
    
      _pageRows(rows, page, pageSize) {
        return rows.slice(
          page * pageSize,
          Math.min(rows.length, (page + 1) * pageSize)
        )
    
        const { selectable, onSelectedRowChange } = this.context.getProps()
    
        const { allRows, rows, selectedRow } = this.context.getState()
    
        let newSelectedRow = null
    
        if (newSelectedRowId !== null && newSelectedRowId !== undefined) {
    
          const data = _.find(allRows, row => row.id === newSelectedRowId)
    
          const visible =
            _.findIndex(rows, row => row.id === newSelectedRowId) !== -1
    
        if (!_.isEqual(selectedRow, newSelectedRow)) {
          await this.context.setState(() => ({
            selectedRow: newSelectedRow
          }))
    
            onSelectedRowChange(newSelectedRow)
    
        const { multiselectable, onMultiselectedRowsChange } =
          this.context.getProps()
    
        const { local, allRows, rows, multiselectedRows } = this.context.getState()
    
        if (newMultiselectedRowIds && newMultiselectedRowIds.length > 0) {
          const allRowsMap = {}
          allRows.forEach(row => {
            allRowsMap[row.id] = row
          })
    
          const rowsMap = {}
          rows.forEach(row => {
            rowsMap[row.id] = row
          })
    
          newMultiselectedRowIds.forEach(rowId => {
            if (rowId !== null && rowId !== undefined) {
    
              const visible = rowsMap[rowId] !== undefined
    
    
              if (data) {
                newMultiselectedRows[rowId] = {
                  id: rowId,
                  data,
                  visible
                }
              } else if (!local) {
    
                const multiselectedRow = multiselectedRows[rowId]
                if (multiselectedRow) {
                  data = multiselectedRow.data
                }
    
            }
          })
        }
    
        await this.context.setState(() => ({
          multiselectedRows: newMultiselectedRows
        }))
    
        if (onMultiselectedRowsChange) {
          onMultiselectedRowsChange(newMultiselectedRows)
        }
      }
    
    
      async showRow(rowId) {
        const { sortedRows, page, pageSize } = this.context.getState()
    
        const index = _.findIndex(sortedRows, ['id', rowId])
    
        if (newPage !== page) {
          await this.context.setState({
            page: newPage
          })
          await this.load()
        }
    
      async handleFilterModeChange(filterMode) {
        await this.context.setState({
          filterMode
        })
        await this.load()
        await this._saveSettings()
      }
    
    
        await this.context.setState(state => {
          const newFilters = {
            ...state.filters
          }
    
          if (filter && _.trim(filter).length > 0) {
            newFilters[column] = filter
          } else {
            delete newFilters[column]
          }
    
        })
    
        if (this.loadTimerId) {
          clearTimeout(this.loadTimerId)
          this.loadTimerId = null
    
          },
          local ? LOCAL_GRID_RELOAD_PERIOD : REMOTE_GRID_RELOAD_PERIOD
        )
    
      async handleGlobalFilterChange(newGlobalFilter) {
        const { local, globalFilter } = this.context.getState()
    
        }))
    
        if (this.loadTimerId) {
          clearTimeout(this.loadTimerId)
          this.loadTimerId = null
        }
    
    
          },
          local ? LOCAL_GRID_RELOAD_PERIOD : REMOTE_GRID_RELOAD_PERIOD
        )
    
    
        if (globalFilter.operator !== newGlobalFilter.operator) {
          await this._saveSettings()
        }
    
      async handleColumnVisibleChange(visibilityMap) {
    
        const { allColumns } = this.context.getState()
    
    
        allColumns.forEach(column => {
          if (!column.configurable) {
            delete visibilityMap[column.name]
          }
        })
    
        await this.context.setState(state => {
    
          const newColumnsVisibility = {
            ...state.columnsVisibility,
            ...visibilityMap
          }
          const newFilters = { ...state.filters }
    
          Object.keys(visibilityMap).forEach(columnName => {
            const visible = visibilityMap[columnName]
            if (!visible) {
              delete newFilters[columnName]
    
          })
    
          return {
            columnsVisibility: newColumnsVisibility,
            filters: newFilters
    
      async handleColumnOrderChange(sourceIndex, destinationIndex) {
    
        await this.context.setState(state => {
    
          const columns = this.getAllColumns()
          const sourceColumn = columns[sourceIndex]
          const destinationColumn = columns[destinationIndex]
    
          const sourceSorting = _.findIndex(
            state.columnsSorting,
            columnName => columnName === sourceColumn.name
          )
          const destinationSorting = _.findIndex(
            state.columnsSorting,
            columnName => columnName === destinationColumn.name
          )
    
          const newColumnsSorting = [...state.columnsSorting]
          newColumnsSorting.splice(sourceSorting, 1)
          newColumnsSorting.splice(destinationSorting, 0, sourceColumn.name)
    
          return {
            columnsSorting: newColumnsSorting
          }
        })
    
        await this.load()
    
        function createInitialSorting(column) {
          return {
            columnName: column.name,
            sortDirection: GridSortingOptions.ASC
          }
        }
    
        function createReversedSorting(column, sorting) {
          return {
            columnName: column.name,
            sortDirection:
              sorting.sortDirection === GridSortingOptions.ASC
                ? GridSortingOptions.DESC
                : GridSortingOptions.ASC
          }
        }
    
    
          const newSortings = []
    
          const index = _.findIndex(
            state.sortings,
            sorting => sorting.columnName === column.name
          )
          const sorting = state.sortings[index]
    
          if (append) {
            if (index !== -1) {
              newSortings.push(...state.sortings)
              newSortings.splice(index, 1)
            } else {
              newSortings.push(...state.sortings)
              newSortings.push(createInitialSorting(column))
    
            if (index !== -1) {
              newSortings.push(...state.sortings)
              newSortings[index] = createReversedSorting(column, sorting)
            } else {
              newSortings.push(createInitialSorting(column))
    
      async handlePageChange(page) {
        await this.context.setState(() => ({
          page
        }))
    
      async handlePageSizeChange(pageSize) {
        await this.context.setState(() => ({
          page: 0,
          pageSize
        }))
    
      async handleRowClick(row) {
        const { onRowClick } = this.context.getProps()
        if (onRowClick) {
          onRowClick({
            id: row.id,
            data: row,
            visible: true
          })
        }
      }
    
    
      async handleRowSelect(row) {
    
      async handleRowMultiselect(row) {
        const { multiselectedRows } = this.context.getState()
    
        if (row) {
          const newMultiselectedRows = { ...multiselectedRows }
    
          if (newMultiselectedRows[row.id]) {