o2xp/react-datatable

View on GitHub
src/redux/reducers/datatableReducer.js

Summary

Maintainability
A
2 hrs
Test Coverage
import deepmerge from "deepmerge";
import arrayMove from "array-move";
import {
  chunk,
  cloneDeep,
  orderBy as orderByFunction,
  differenceBy
} from "lodash";
import { tableRef } from "../../components/DatatableCore/Body/Body";

const Fuse = require("fuse.js");

const optionsFuse = {
  shouldSort: true,
  threshold: 0.0,
  location: 0,
  distance: 100,
  maxPatternLength: 32,
  minMatchCharLength: 1
};
const defaultState = {
  title: "",
  dimensions: {
    datatable: {
      width: "100%",
      height: "100%",
      widthNumber: 0,
      totalWidthNumber: 0
    },
    header: {
      height: "0px",
      heightNumber: 0
    },
    body: {
      heightNumber: 0
    },
    row: {
      height: "33px",
      heightNumber: 0
    },
    columnSizeMultiplier: 1,
    isScrolling: false
  },
  keyColumn: null,
  font: "Roboto",
  data: {
    columns: [],
    rows: []
  },
  pagination: {
    pageSelected: 1,
    pageTotal: 1,
    rowsPerPageSelected: "",
    rowsCurrentPage: [],
    rowsToUse: []
  },
  newRows: [],
  rowsDeleted: [],
  rowsEdited: [],
  rowsGlobalEdited: [],
  rowsSelected: [],
  actions: null,
  refreshRows: null,
  isRefreshing: false,
  stripped: false,
  searchTerm: "",
  orderBy: [],
  features: {
    canEdit: false,
    canEditRow: null,
    canGlobalEdit: false,
    canPrint: false,
    canDownload: false,
    canAdd: false,
    canDelete: false,
    canDuplicate: false,
    canSearch: false,
    canRefreshRows: false,
    canOrderColumns: false,
    canSelectRow: false,
    canSaveUserConfiguration: false,
    editableIdNewRow: [],
    userConfiguration: {
      columnsOrder: [],
      copyToClipboard: false
    },
    rowsPerPage: {
      available: [10, 25, 50, 100, "All"],
      selected: "All"
    },
    additionalActions: [],
    additionalIcons: [],
    selectionIcons: []
  }
};

const overwriteMerge = (destinationArray, sourceArray) => sourceArray;

const convertSizeToNumber = (val, height) => {
  const splitSize = val.match(/[0-9]+|(px|%|vw|vh)/gi);
  let valSize = splitSize[0];
  const unitSize = splitSize[1];
  if (unitSize === "px") {
    valSize = Number(valSize);
  }
  if (unitSize === "vw") {
    valSize = window.innerWidth * (valSize / 100);
  }
  if (unitSize === "vh") {
    valSize = window.innerHeight * (valSize / 100);
  }
  if (unitSize === "%") {
    if (height) {
      valSize =
        document.getElementById("o2xp").parentElement.clientHeight *
        (valSize / 100);
    } else {
      valSize =
        document.getElementById("o2xp").parentElement.clientWidth *
        (valSize / 100);
    }
  }

  return valSize;
};

const updateRowSizeMultiplier = state => {
  const { canEdit, canDelete, canSelectRow, canDuplicate } = state.features;
  const numberOfActions = [
    canEdit,
    canDelete,
    canSelectRow,
    canDuplicate
  ].filter(v => v).length;
  const colDisplayed = [];
  state.data.columns.forEach(col => {
    if (state.features.userConfiguration.columnsOrder.includes(col.id)) {
      colDisplayed.push(col);
    }
  });

  const widthColDisplayed = colDisplayed.map(col => {
    const column = col;
    if (column.colSize && col.id !== "o2xpActions") {
      return Number(column.colSize.split("px")[0]) + 35;
    }
    if (col.id !== "o2xpActions") {
      column.colSize = "100px";
      return 100;
    }
    return 0;
  });

  const totalWidthColDisplayed = widthColDisplayed.reduce((a, b) => {
    return a + b;
  });

  const splitWidth = state.dimensions.datatable.width.match(
    /[0-9]+|(px|%|vw|vh)/gi
  );
  let widthTable = splitWidth[0];
  const unitWidthTable = splitWidth[1];

  if (unitWidthTable === "vw") {
    widthTable = (window.innerWidth * widthTable) / 100;
  }

  if (unitWidthTable === "%") {
    widthTable =
      document.getElementById("o2xp").parentElement.clientWidth *
      (widthTable / 100);
  }

  if (numberOfActions.length > 0) {
    widthTable -= 50;
  }

  widthTable -= state.features.additionalActions.length * 50;

  if (canEdit) {
    widthTable -= 100;
  }

  if (canDelete) {
    widthTable -= 100;
  }

  if (canDuplicate) {
    widthTable -= 50;
  }

  if (canSelectRow) {
    widthTable -= 50;
  }

  if (canEdit && canDelete) {
    widthTable += 100;
  }

  widthTable -= state.features.userConfiguration.columnsOrder.length * 50;
  widthTable -= 22;
  let mult = 1;

  if (widthTable > totalWidthColDisplayed) {
    mult = widthTable / totalWidthColDisplayed;
  }

  return mult;
};

const totalWidth = state => {
  const colDisplayed = [];
  state.data.columns.forEach(col => {
    if (
      state.features.userConfiguration.columnsOrder.includes(col.id) &&
      col.id !== "o2xpActions"
    ) {
      colDisplayed.push(col);
    }
  });

  const widthColDisplayed = colDisplayed.map(
    col => Number(col.colSize.split("px")[0]) + 50
  );

  let totalWidthColDisplayed = widthColDisplayed.reduce((a, b) => {
    return a + b;
  });
  totalWidthColDisplayed *= state.dimensions.columnSizeMultiplier;
  totalWidthColDisplayed -= 22;
  return totalWidthColDisplayed;
};

const calcComponentSize = state => {
  const newState = {
    ...state,
    dimensions: {
      ...state.dimensions,
      datatable: {
        ...state.dimensions.datatable,
        widthNumber: convertSizeToNumber(state.dimensions.datatable.width),
        totalWidthNumber: totalWidth(state)
      },
      row: {
        ...state.dimensions.row,
        heightNumber: convertSizeToNumber(state.dimensions.row.height)
      },
      columnSizeMultiplier: updateRowSizeMultiplier(state)
    }
  };
  const heightNumber = convertSizeToNumber(
    newState.dimensions.datatable.height,
    true
  );
  newState.dimensions.body.heightNumber =
    heightNumber - newState.dimensions.header.heightNumber - 50 - 70;

  return newState;
};

const setPagination = ({
  state,
  newPageSelected = null,
  newRowsPerPageSelected = null
}) => {
  const { searchTerm, orderBy } = state;

  let rowsToUse = state.data.rows;
  if (searchTerm.length) {
    const fuse = new Fuse(state.data.rows, {
      ...optionsFuse,
      keys: state.features.userConfiguration.columnsOrder
    });
    rowsToUse = fuse.search(searchTerm);
  }

  if (orderBy) {
    rowsToUse = orderByFunction(
      rowsToUse,
      orderBy.map(el => el.id),
      orderBy.map(el => el.value)
    );
  }

  const rowsPerPageSelected =
    newRowsPerPageSelected ||
    state.pagination.rowsPerPageSelected ||
    state.features.rowsPerPage.selected;
  let pageSelected =
    rowsPerPageSelected === "All"
      ? 1
      : newPageSelected || state.pagination.pageSelected;
  const pageTotal =
    rowsPerPageSelected === "All"
      ? 1
      : Math.ceil(rowsToUse.length / rowsPerPageSelected);
  pageSelected = pageSelected > pageTotal ? pageTotal : pageSelected;
  pageSelected = pageSelected < 1 ? 1 : pageSelected;
  let rowsCurrentPage = [];
  if (rowsToUse.length > 0) {
    rowsCurrentPage =
      rowsPerPageSelected === "All"
        ? rowsToUse
        : chunk(rowsToUse, rowsPerPageSelected)[
            pageSelected ? pageSelected - 1 : 0
          ];
  }

  if (
    tableRef &&
    tableRef.current &&
    (newPageSelected || newRowsPerPageSelected)
  ) {
    tableRef.current.scrollToItem(0);
  }

  return {
    pageSelected,
    pageTotal,
    rowsPerPageSelected,
    rowsCurrentPage,
    rowsToUse
  };
};

const removeNullUndefined = obj => {
  Object.keys(obj).forEach(key => {
    if (
      obj[key] &&
      typeof obj[key] === "object" &&
      key !== "icon" &&
      key !== "rows"
    )
      removeNullUndefined(obj[key]);
    /* eslint-disable no-param-reassign */ else if (
      obj[key] == null ||
      obj[key] === undefined
    )
      delete obj[key]; // delete
  });
  return obj;
};

const updateRows = ({
  rows,
  oldRows,
  rowsEdited,
  newRows,
  rowsDeleted,
  rowsGlobalEdited,
  keyColumn
}) => {
  const oldRowsId = oldRows.map(row => row[keyColumn]);
  const toRemove = differenceBy(oldRows, rows, keyColumn);
  const toRemoveId = toRemove.map(row => row[keyColumn]);

  const newRowsAdded = newRows.filter(
    row => !toRemoveId.includes(row[keyColumn])
  );
  const newRowsGlobalEdited = rowsGlobalEdited.filter(
    row => !toRemoveId.includes(row[keyColumn])
  );
  // If rows has beend added then deleted no adding it to rowsDeleted
  const newRowsAddedId = newRows.map(row => row[keyColumn]);
  const newRowsDeleted = deepmerge(rowsDeleted, toRemove).filter(
    row => !newRowsAddedId.includes(row[keyColumn])
  );

  const newRowsEdited = [];

  // Add new rows
  rows.forEach(row => {
    // if row already exist, push existing
    if (oldRowsId.includes(row[keyColumn])) {
      newRowsEdited.push(
        rowsEdited.find(rowEdited => rowEdited[keyColumn] === row[keyColumn])
      );
    } else {
      // if not, push new
      const newRow = { ...row, idOfColumnErr: [], hasBeenEdited: true };
      newRowsAdded.push(newRow);
      newRowsGlobalEdited.push(newRow);
      newRowsEdited.push(newRow);
    }
  });

  return { newRowsEdited, newRowsAdded, newRowsDeleted, newRowsGlobalEdited };
};

const initializeOptions = (
  state,
  {
    optionsInit,
    forceRerender = false,
    actions = null,
    refreshRows = null,
    stripped = false
  }
) => {
  let newState = deepmerge(
    forceRerender ? defaultState : state,
    removeNullUndefined({ ...optionsInit }),
    {
      arrayMerge: overwriteMerge
    }
  );

  const {
    canEdit,
    canDelete,
    canSelectRow,
    canDuplicate,
    isUpdatingRows
  } = newState.features;

  if (isUpdatingRows) {
    const { rows } = newState.data;
    const { rows: oldRows } = state.data;
    const {
      rowsEdited,
      newRows,
      rowsDeleted,
      keyColumn,
      rowsGlobalEdited
    } = newState;
    if (newState.rowsEdited.length > 0) {
      const {
        newRowsEdited,
        newRowsAdded,
        newRowsDeleted,
        newRowsGlobalEdited
      } = updateRows({
        rows,
        oldRows,
        rowsEdited,
        newRows,
        rowsDeleted,
        rowsGlobalEdited,
        keyColumn
      });

      newState = {
        ...newState,
        rowsEdited: newRowsEdited,
        newRows: newRowsAdded,
        rowsDeleted: newRowsDeleted,
        rowsGlobalEdited: newRowsGlobalEdited
      };
    }
  }

  newState.actions = actions;
  newState.refreshRows = refreshRows;
  newState.stripped = stripped;

  const { height } = newState.dimensions.row;
  newState.dimensions.row.height = height.split("px")[0] < 33 ? "33px" : height;

  if (newState.features.userConfiguration.columnsOrder.length === 0) {
    newState.features.userConfiguration.columnsOrder = optionsInit.data.columns.map(
      col => col.id
    );
  }

  const actionsUser = [canEdit, canDelete, canSelectRow];
  const numberOfActions = actionsUser.filter(v => v).length;

  if (
    (numberOfActions > 0 ||
      newState.features.additionalActions.length > 0 ||
      canDuplicate) &&
    !newState.data.columns.find(col => col.id === "o2xpActions")
  ) {
    newState.features.userConfiguration.columnsOrder = newState.features.userConfiguration.columnsOrder.filter(
      colId => colId !== "o2xpActions"
    );

    newState.data.columns = newState.data.columns.filter(
      column => column.id !== "o2xpActions"
    );

    newState.features.userConfiguration.columnsOrder.unshift("o2xpActions");
    let colSize = canDuplicate ? 50 : 0;
    switch (actionsUser.join(" ")) {
      case "true true true":
      case "false true true":
      case "true false true":
        colSize += 150;
        break;
      case "true true false":
      case "false true false":
      case "true false false":
        colSize += 100;
        break;
      case "false false true":
        colSize += 50;
        break;
      default:
        colSize += 0;
        break;
    }

    colSize = `${colSize + newState.features.additionalActions.length * 50}px`;
    newState.data.columns.unshift({
      id: "o2xpActions",
      label: "Actions",
      colSize,
      hiddenCreate: true,
      editable: false
    });
  }

  newState.pagination = setPagination({
    state: newState,
    current: newState.pagination.current,
    rowsPerPageSelected: newState.features.rowsPerPage.selected
  });

  const { title, features } = newState;
  const {
    canGlobalEdit,
    canPrint,
    canDownload,
    canSearch,
    canRefreshRows,
    canOrderColumns,
    canSaveUserConfiguration,
    additionalIcons,
    selectionIcons
  } = features;
  const hasHeader =
    canGlobalEdit ||
    canPrint ||
    canDownload ||
    canSearch ||
    canRefreshRows ||
    canOrderColumns ||
    canSaveUserConfiguration ||
    title.length > 0 ||
    additionalIcons.length > 0 ||
    selectionIcons.length > 0;

  newState.dimensions.columnSizeMultiplier = updateRowSizeMultiplier(newState);
  newState.dimensions.datatable.widthNumber = convertSizeToNumber(
    newState.dimensions.datatable.width
  );

  const heightNumber = convertSizeToNumber(
    newState.dimensions.datatable.height,
    true
  );
  newState.dimensions.header.height = hasHeader ? "60px" : "0px";

  newState.dimensions.header.heightNumber = convertSizeToNumber(
    newState.dimensions.header.height
  );

  newState.dimensions.row.heightNumber = convertSizeToNumber(
    newState.dimensions.row.height
  );

  newState.dimensions.body.heightNumber =
    heightNumber - newState.dimensions.header.heightNumber - 50 - 70;

  return newState;
};

const sortColumn = (state, { newIndex, oldIndex }) => {
  const newState = {
    ...state,
    features: {
      ...state.features,
      userConfiguration: {
        ...state.features.userConfiguration,
        columnsOrder: arrayMove(
          state.features.userConfiguration.columnsOrder,
          oldIndex,
          newIndex
        )
      }
    }
  };

  return newState;
};

const setRowsPerPage = (state, newRowsPerPageSelected) => {
  return {
    ...state,
    pagination: setPagination({
      state,
      newPageSelected: 1,
      newRowsPerPageSelected
    })
  };
};

const setPage = (state, newPageSelected) => {
  return {
    ...state,
    pagination: setPagination({ state, newPageSelected })
  };
};

const setIsScrolling = (state, isScrolling) => {
  return {
    ...state,
    dimensions: {
      ...state.dimensions,
      isScrolling
    }
  };
};

const addRowEdited = (state, row) => {
  const { rowsEdited, keyColumn } = state;
  if (rowsEdited.find(re => re[keyColumn] === row[keyColumn])) {
    return state;
  }
  return {
    ...state,
    rowsEdited: [
      ...rowsEdited,
      {
        ...row,
        idOfColumnErr: [],
        hasBeenEdited: false
      }
    ]
  };
};

const addAllRowsToEdited = state => {
  const { actions, data } = state;
  const { rows } = data;

  if (actions) {
    actions({ type: "editMode" });
  }

  const rowsEdited = rows.map(row => {
    return {
      ...row,
      idOfColumnErr: [],
      hasBeenEdited: false
    };
  });

  return {
    ...state,
    rowsEdited
  };
};

const checkHasBeenEdited = ({ rows, rowEdited, keyColumn }) => {
  const rowNonEdited = rows.find(
    row => row[keyColumn] === rowEdited[keyColumn]
  );

  let hasBeenEdited = false;
  Object.keys(rowNonEdited).forEach(key => {
    if (
      rowNonEdited[key] !== rowEdited[key] &&
      !(rowNonEdited[key] == null && rowEdited[key].length === 0)
    ) {
      hasBeenEdited = true;
    }
  });
  return hasBeenEdited;
};

const addNewRow = (state, payload) => {
  const {
    keyColumn,
    data,
    pagination,
    rowsGlobalEdited,
    rowsEdited,
    features
  } = state;
  const { columns, rows } = data;
  let newRow = {
    hasBeenEdited: true,
    idOfColumnErr: [],
    [keyColumn]: `_${Math.random()
      .toString(36)
      .substr(2, 18)}`
  };

  if (features.editableIdNewRow.length > 0) {
    newRow.editableId = features.editableIdNewRow;
  }

  columns.forEach(col => {
    switch (col.id) {
      case "o2xpActions":
      case "modalOpen":
        break;
      case keyColumn:
        newRow[keyColumn] = `_${Math.random()
          .toString(36)
          .substr(2, 9)}`;
        break;
      default:
        newRow[col.id] = "";
    }
  });

  newRow = { ...newRow, ...payload };
  const newState = {
    ...state,
    rowsGlobalEdited: [newRow, ...rowsGlobalEdited],
    rowsEdited: [newRow, ...rowsEdited],
    data: { ...data, rows: [newRow, ...rows] },
    newRows: [...state.newRows, newRow]
  };
  const newPagination = setPagination({
    state: newState,
    newPageSelected: pagination.pageSelected,
    newRowsPerPageSelected: pagination.rowsPerPageSelected
  });

  return { ...newState, pagination: newPagination };
};

const setRowEdited = (state, { columnId, rowId, newValue, error }) => {
  const { rowsEdited, keyColumn, rowsGlobalEdited, features } = state;
  let newRowsGlobalEdited = [...rowsGlobalEdited];
  const rowsEditedUpdate = rowsEdited.map(row => {
    if (row[keyColumn] === rowId) {
      let idOfColumnErrUpdate = row.idOfColumnErr;
      if (error) {
        if (!idOfColumnErrUpdate.includes(columnId)) {
          idOfColumnErrUpdate = [...idOfColumnErrUpdate, columnId];
        }
      } else {
        idOfColumnErrUpdate = idOfColumnErrUpdate.filter(e => e !== columnId);
      }
      /* eslint-disable no-restricted-globals */
      const r = {
        ...row,
        [columnId]:
          isNaN(newValue) && typeof newValue !== "string" ? "" : newValue,
        idOfColumnErr: idOfColumnErrUpdate
      };

      const hasBeenEdited = checkHasBeenEdited({
        rows: state.data.rows,
        rowEdited: r,
        keyColumn
      });

      if (features.canGlobalEdit) {
        newRowsGlobalEdited = newRowsGlobalEdited.filter(
          rowGlobalEdited => rowGlobalEdited[keyColumn] !== r[keyColumn]
        );

        if (hasBeenEdited) {
          newRowsGlobalEdited = [...newRowsGlobalEdited, r];
        }
      }

      return {
        ...r,
        hasBeenEdited
      };
    }
    return row;
  });
  return {
    ...state,
    rowsEdited: rowsEditedUpdate,
    rowsGlobalEdited: newRowsGlobalEdited
  };
};

const mergeArrayInArray = (firstArray, keyColumn, secondArray) => {
  return [
    ...secondArray.map(el => {
      const object = firstArray.find(obj => el[keyColumn] === obj[keyColumn]);
      if (object) {
        return object;
      }
      return el;
    })
  ];
};

const saveRowEdited = (state, payload) => {
  const row = payload;
  delete row.idOfColumnErr;
  delete row.hasBeenEdited;
  const { data, rowsEdited, keyColumn, pagination, actions } = state;

  const { rows, columns } = state.data;
  const rowNonEdited = rows.find(r => r[keyColumn] === row[keyColumn]);

  columns.forEach(col => {
    if (
      row[col.id] != null &&
      row[col.id].length === 0 &&
      rowNonEdited[col.id] == null
    ) {
      row[col.id] = rowNonEdited[col.id];
    }
  });

  if (actions) {
    actions({ type: "save", payload: row });
  }
  return {
    ...state,
    data: {
      ...data,
      rows: mergeArrayInArray([row], keyColumn, data.rows)
    },
    pagination: {
      ...pagination,
      rowsCurrentPage: mergeArrayInArray(
        [row],
        keyColumn,
        pagination.rowsCurrentPage
      )
    },
    rowsGlobalEdited: [],
    rowsEdited: [...rowsEdited.filter(r => r[keyColumn] !== row[keyColumn])]
  };
};

const saveAllRowsEdited = state => {
  const {
    data,
    keyColumn,
    rowsGlobalEdited,
    rowsDeleted,
    actions,
    pagination,
    newRows
  } = state;

  const rows = rowsGlobalEdited.map(row => {
    const r = row;
    delete r.idOfColumnErr;
    delete r.hasBeenEdited;
    return r;
  });

  rowsDeleted.forEach(rd => delete rd.indexInsert);

  const newRowsId = newRows.map(nr => nr[keyColumn]);

  const rowsEdited = rowsGlobalEdited.filter(
    rge => !newRowsId.includes(rge[keyColumn])
  );
  const rowsAdded = rowsGlobalEdited.filter(rge =>
    newRowsId.includes(rge[keyColumn])
  );

  const { rows: rowsNonEdited, columns } = state.data;
  const rowsEditedId = rowsEdited.map(re => re[keyColumn]);
  const rowsNonEditedFiltered = rowsNonEdited.filter(rne =>
    rowsEditedId.includes(rne[keyColumn])
  );

  rowsEdited.forEach(re => {
    const rowNonEdited = rowsNonEditedFiltered.find(
      rne => rne[keyColumn] === re[keyColumn]
    );
    columns.forEach(col => {
      if (
        re[col.id] != null &&
        re[col.id].length === 0 &&
        rowNonEdited[col.id] == null
      ) {
        re[col.id] = rowNonEdited[col.id];
      }
    });
  });

  const rowsMerged = mergeArrayInArray(rows, keyColumn, data.rows);

  if (actions) {
    actions({
      type: "save",
      payload: {
        rows: rowsMerged,
        rowsDeleted,
        rowsEdited,
        rowsAdded
      }
    });
  }

  return {
    ...state,
    data: {
      ...data,
      rows: rowsMerged
    },
    pagination: {
      ...pagination,
      rowsCurrentPage: mergeArrayInArray(
        rows,
        keyColumn,
        pagination.rowsCurrentPage
      )
    },
    rowsDeleted: [],
    newRows: [],
    rowsGlobalEdited: [],
    rowsEdited: []
  };
};

const revertRowEdited = (state, payload) => {
  const row = payload;
  const { rowsEdited, keyColumn } = state;
  return {
    ...state,
    rowsEdited: [...rowsEdited.filter(r => r[keyColumn] !== row[keyColumn])]
  };
};

const revertAllRowsToEdited = state => {
  const { newRows, data, keyColumn, pagination, rowsDeleted, actions } = state;
  const newRowsId = newRows.map(r => r[keyColumn]);
  const { rows } = data;

  rowsDeleted.forEach((rd, i) => rows.splice(rd.indexInsert + i, 0, rd));

  if (actions) {
    actions({
      type: "revert"
    });
  }

  const newState = {
    ...state,
    newRows: [],
    rowsEdited: [],
    rowsGlobalEdited: [],
    rowsDeleted: [],
    data: {
      ...data,
      rows: [...rows.filter(r => !newRowsId.includes(r[keyColumn]))]
    }
  };

  const newPagination = setPagination({
    state: newState,
    newPageSelected: pagination.pageSelected,
    newRowsPerPageSelected: pagination.rowsPerPageSelected
  });

  return {
    ...newState,
    pagination: newPagination
  };
};

const addToDeleteRow = (state, payload) => {
  const row = payload;
  const {
    data,
    keyColumn,
    rowsEdited,
    rowsGlobalEdited,
    rowsDeleted,
    newRows
  } = state;

  const newlyAdded =
    newRows.filter(nr => nr[keyColumn] === row[keyColumn]).length > 0;

  const index = data.rows.findIndex(r => r[keyColumn] === row[keyColumn]);
  row.indexInsert = index;
  let newState = {
    ...state,
    rowsEdited: [...rowsEdited.filter(r => r[keyColumn] !== row[keyColumn])],
    rowsGlobalEdited: [
      ...rowsGlobalEdited.filter(r => r[keyColumn] !== row[keyColumn])
    ],
    data: {
      ...data,
      rows: [...data.rows.filter(r => r[keyColumn] !== row[keyColumn])]
    }
  };

  newState = {
    ...newState,
    rowsDeleted: newlyAdded ? rowsDeleted : [...rowsDeleted, row],
    pagination: setPagination({ state: newState })
  };

  return newState;
};

const deleteRow = (state, payload) => {
  const row = payload;
  const { data, keyColumn, actions, rowsEdited, rowsGlobalEdited } = state;

  if (actions) {
    actions({ type: "delete", payload: row });
  }

  let newState = {
    ...state,
    rowsEdited: [...rowsEdited.filter(r => r[keyColumn] !== row[keyColumn])],
    rowsGlobalEdited: [
      ...rowsGlobalEdited.filter(r => r[keyColumn] !== row[keyColumn])
    ],
    data: {
      ...data,
      rows: [...data.rows.filter(r => r[keyColumn] !== row[keyColumn])]
    }
  };

  newState = {
    ...newState,
    pagination: setPagination({ state: newState })
  };

  return newState;
};

const selectRow = (state, payload) => {
  const { keyColumn, rowsSelected, actions } = state;
  const { checked, row } = payload;
  let newRowsSelected;
  if (checked) {
    newRowsSelected = [...rowsSelected, row];
  } else {
    newRowsSelected = [
      ...rowsSelected.filter(r => r[keyColumn] !== row[keyColumn])
    ];
  }

  if (actions) {
    actions({ type: "select", payload: newRowsSelected });
  }

  return {
    ...state,
    rowsSelected: newRowsSelected
  };
};

const setRowsSelected = (state, payload) => {
  return {
    ...state,
    rowsSelected: payload
  };
};

const setRowsGlobalSelected = (state, payload) => {
  const { keyColumn, rowsSelected, actions } = state;
  const { rows, checked } = payload;
  const ids = rows.map(row => row[keyColumn]);
  const idsRowsSelected = rowsSelected.map(row => row[keyColumn]);
  let newRowsSelected;
  if (checked) {
    newRowsSelected = [
      ...rowsSelected,
      ...rows.filter(row => !idsRowsSelected.includes(row[keyColumn]))
    ];
  } else {
    newRowsSelected = rowsSelected.filter(row => !ids.includes(row[keyColumn]));
  }

  if (actions) {
    actions({ type: "select", payload: newRowsSelected });
  }

  return {
    ...state,
    rowsSelected: newRowsSelected
  };
};

const search = (state, payload) => {
  const newState = {
    ...state,
    searchTerm: payload
  };

  return {
    ...newState,
    pagination: setPagination({ state: newState })
  };
};

const setColumnVisibilty = (state, payload) => {
  const { columns } = state.data;
  const { columnsOrder } = state.features.userConfiguration;
  const orderDisplayed = columns.filter(
    col => columnsOrder.includes(col.id) || col.id === payload.id
  );
  const columnsId = orderDisplayed.map(col => col.id);
  const index = columnsId.indexOf(payload.id);

  let newColumnsOrder;
  if (columnsOrder.includes(payload.id)) {
    newColumnsOrder = columnsOrder.filter(col => col !== payload.id);
  } else {
    columnsOrder.splice(index, 0, payload.id);
    newColumnsOrder = cloneDeep(columnsOrder);
  }

  const newState = {
    ...state,
    orderBy: state.orderBy.filter(el => el.id !== payload.id),
    features: {
      ...state.features,
      userConfiguration: {
        ...state.features.userConfiguration,
        columnsOrder: newColumnsOrder
      }
    }
  };

  return {
    ...newState,
    pagination: setPagination({
      state: newState
    }),
    dimensions: {
      ...newState.dimensions,
      columnSizeMultiplier: updateRowSizeMultiplier(newState)
    }
  };
};

const setUserConfiguration = (state, payload) => {
  const { columnsOrder, copyToClipboard, action } = payload;
  const { actions } = state;

  if (action === "save" && actions) {
    actions({
      type: "saveUserConfiguration",
      payload: { columnsOrder, copyToClipboard }
    });
  }

  const newState = {
    ...state,
    features: {
      ...state.features,
      userConfiguration: {
        columnsOrder,
        copyToClipboard
      }
    }
  };

  return {
    ...newState,
    dimensions: {
      ...newState.dimensions,
      columnSizeMultiplier: updateRowSizeMultiplier(newState)
    }
  };
};

const refreshRowsStarted = state => {
  return {
    ...state,
    isRefreshing: true,
    searchTerm: "",
    rowsEdited: [],
    rowsSelected: []
  };
};

const refreshRowsSuccess = (state, payload) => {
  const newState = {
    ...state,
    data: {
      ...state.data,
      rows: payload
    },
    isRefreshing: false
  };
  return {
    ...newState,
    pagination: setPagination({ state: newState })
  };
};

const refreshRowsError = state => {
  return { ...state, isRefreshing: false };
};

const orderByColumns = (state, payload) => {
  const { orderBy } = state;
  const index = orderBy.findIndex(el => el.id === payload);
  let newOrderBy = [...orderBy];
  if (index === -1) {
    newOrderBy = [...newOrderBy, { id: payload, value: "asc" }];
  } else if (newOrderBy[index].value === "desc") {
    newOrderBy = newOrderBy.filter(el => el.id !== payload);
  } else {
    newOrderBy[index].value = "desc";
  }

  const newState = {
    ...state,
    orderBy: newOrderBy
  };

  return {
    ...newState,
    pagination: setPagination({
      state: newState
    })
  };
};

const insert = (arr, index, newItem) => [
  ...arr.slice(0, index),
  newItem,
  ...arr.slice(index)
];

const duplicateRow = (state, payload) => {
  const { keyColumn, data, pagination, rowsGlobalEdited, rowsEdited } = state;
  const { rows } = data;
  const newRow = {
    ...payload,
    hasBeenEdited: true,
    idOfColumnErr: [],
    [keyColumn]: `_${Math.random()
      .toString(36)
      .substr(2, 18)}`
  };

  const index =
    data.rows.findIndex(r => r[keyColumn] === payload[keyColumn]) + 1;

  const newState = {
    ...state,
    rowsGlobalEdited: insert(rowsGlobalEdited, index, newRow),
    rowsEdited: insert(rowsEdited, index, newRow),
    data: { ...data, rows: insert(rows, index, newRow) },
    newRows: [...state.newRows, newRow]
  };

  const newPagination = setPagination({
    state: newState,
    newPageSelected: pagination.pageSelected,
    newRowsPerPageSelected: pagination.rowsPerPageSelected
  });

  return { ...newState, pagination: newPagination };
};

const datatableReducer = (state = defaultState, action) => {
  const { payload, type } = action;

  switch (type) {
    case "INITIALIZE_OPTIONS":
      return initializeOptions(state, payload);
    case "UPDATE_COMPONENT_SIZE":
      return calcComponentSize(state);
    case "SORT_COLUMNS":
      return sortColumn(state, payload);
    case "SET_ROWS_PER_PAGE":
      return setRowsPerPage(state, payload);
    case "SET_PAGE":
      return setPage(state, payload);
    case "SET_IS_SCROLLING":
      return setIsScrolling(state, payload);
    case "ADD_ROW_EDITED":
      return addRowEdited(state, payload);
    case "ADD_NEW_ROW":
      return addNewRow(state, payload);
    case "ADD_ALL_ROWS_TO_EDITED":
      return addAllRowsToEdited(state);
    case "SET_ROW_EDITED":
      return setRowEdited(state, payload);
    case "SAVE_ROW_EDITED":
      return saveRowEdited(state, payload);
    case "SAVE_ALL_ROWS_EDITED":
      return saveAllRowsEdited(state);
    case "REVERT_ROW_EDITED":
      return revertRowEdited(state, payload);
    case "REVERT_ALL_ROWS_TO_EDITED":
      return revertAllRowsToEdited(state);
    case "DELETE_ROW":
      return deleteRow(state, payload);
    case "ADD_TO_DELETE_ROW":
      return addToDeleteRow(state, payload);
    case "SELECT_ROW":
      return selectRow(state, payload);
    case "SET_ROWS_SELECTED":
      return setRowsSelected(state, payload);
    case "SET_ROWS_GLOBAL_SELECTED":
      return setRowsGlobalSelected(state, payload);
    case "SEARCH":
      return search(state, payload);
    case "SET_COLUMN_VISIBILITY":
      return setColumnVisibilty(state, payload);
    case "SET_USER_CONFIGURATION":
      return setUserConfiguration(state, payload);
    case "REFRESH_ROWS_STARTED":
      return refreshRowsStarted(state);
    case "REFRESH_ROWS_SUCCESS":
      return refreshRowsSuccess(state, payload);
    case "REFRESH_ROWS_ERROR":
      return refreshRowsError(state);
    case "ORDER_BY_COLUMNS":
      return orderByColumns(state, payload);
    case "DUPLICATE_ROW":
      return duplicateRow(state, payload);
    default:
      return state;
  }
};

export default datatableReducer;