# step-ag-grid

[![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)
> Reusable [ag-grid](https://www.ag-grid.com/) component for LINZ / Toitū te whenua.

## Features

- [ag-grid-community](https://www.npmjs.com/package/ag-grid-community) based grid with custom popover components
  implemented using a modified [react-menu](https://www.npmjs.com/package/@szhsin/react-menu).
- Default components
  - Text input
  - Text area
  - Drop-down
  - Multi-select
  - Multi-select-grid
  - Bearing/Bearing Correction
  - Popover message
  - Custom form
  - Context menu

_Please note this requires React >=17, ag-grid-community >=28, and SASS._

## Install

with npm

```bash
npm install @linzjs/step-ag-grid
```

or with Yarn

```bash
yarn add @linzjs/step-ag-grid
```

## Demo

```bash
npm run storybook
```

Storybook demo deployed at: https://master--633cd0dc2fe91d7df3ed32e4.chromatic.com/

## Usage

Check `src\stories` for more usage examples

```tsx
import { useMemo } from "react";

import "@linzjs/lui/dist/fonts";
import "@linzjs/lui/dist/scss/base.scss";
import {
  ColDefT,
  GridCell,
  GridCellFiller,
  GridContextProvider,
  GridContextMenuComponentProps,
  GridPopoverEditDropDown,
  GridPopoverMessage,
  GridUpdatingContextProvider,
  GridWrapper,
  GridFilters,
  GridFilterQuick,
  GridFilterButtons
} from "@linzjs/step-ag-grid";
// Only required for LINZ themes otherwise import the default theme from ag-grid
import "@linzjs/step-ag-grid/dist/GridTheme.scss";
import "@linzjs/step-ag-grid/dist/index.css";
import { GridFilterDownloadCsvButton } from "./GridFilterDownloadCsvButton";

const GridDemo = () => {
  interface ITestRow {
    id: number;
    name: number;
    position: string;
  }

  const columnDefs: ColDefT<ITestRow>[] = useMemo(
    () => [
      GridCell({
        field: "id",
        headerName: "Id",
        export: false,
      }),
      // This is the flex column that will expand to fit
      GridCell<ITestRow, string>({
        field: "name",
        headerName: "Name",
        flex: 1,
        cellRendererParams: {
          warning: ({ value }) => value === "Tester" && "Testers are testing",
          info: ({ value }) => value === "Developer" && "Developers are awesome",
        },
      }),
      GridPopoverEditDropDown(
        {
          field: "position",
          headerName: "Position",
        },
        {
          multiEdit: false,
          editorParams: {
            options: ["Architect", "Developer", "Product Owner", "Scrum Master", "Tester", MenuSeparator, "(other)"],
          },
        },
      ),
      GridPopoverMessage(
        {
          headerName: "Popout message",
          cellRenderer: () => <>Click me!</>,
        },
        {
          multiEdit: true,
          editorParams: {
            message: async ({selectedRows}) => {
              return `There are ${selectedRows.length} row(s) selected`;
            },
          },
        },
      ),
      // If your flex column gets hidden this will become active
      GridCellFiller(),
    ],
    [],
  );

  const ContextMenu = ({ selectedRows, colDef, close }: GridContextMenuComponentProps<ITestRow>): ReactElement => {
    const onClick = useCallback(() => {
      selectedRows.forEach((row) => {
        switch (colDef.field) {
          case "name":
            row.name = "";
            break;
          case "distance":
            row.distance = null;
            break;
        }
      });
      close();
    }, [close, colDef.field, selectedRows]);

    return (
      <>
        <button onClick={onClick}>Button - Clear cell</button>
        <MenuItem onClick={onClick}>Menu Item - Clear cell</MenuItem>
      </>
    );
  };
  
  const rowData: ITestRow[] = useMemo(
    () => [
      { id: 1000, name: "Tom", position: "Tester" },
      { id: 1001, name: "Sue", position: "Developer" },
    ],
    [],
  );

  return (
    <GridUpdatingContextProvider>
      <GridContextProvider>
        <GridWrapper>
          <GridFilters>
            <GridFilterQuick/>
            <GridFilterButtons<ITestRow>
              options={[
                {
                  label: "All",
                },
                {
                  label: "Developers",
                  filter: (row) => row.position === "Developer",
                },
                {
                  label: "Testers",
                  filter: (row) => row.position === "Tester",
                },
              ]}
            />
            <GridFilterColumnsToggle/>
            <GridFilterDownloadCsvButton fileName={"exportFile"}/>
          </GridFilters>
          <Grid selectable={true}
                columnDefs={columnDefs}
                rowData={rowData}
                contextMenu={contextMenu}
                contextMenuSelectRow={false}
                onContentSize={({ width }) => setPanelSize(width)} />
        </GridWrapper>
      </GridContextProvider>
    </GridUpdatingContextProvider>
  );
};
```

## Bulk editing
If you are editing a cell and tab out of the cell, the grid will edit the next editable cell.

At this point you can send the change to the back-end immediately and then wait for an update response
_OR_
you could cache the required change, update then cell locally, and then wait for the callback
```<Grid onCellEditingComplete={fn}/>``` which will get invoked when the grid cannot find any
more editable cells on the grid row, which will speed up editing.

## Grid sizing
Grid uses ```<Grid sizeColumns="auto"/>``` which sizes by cell content by default.
To ignore cell content use "fit", to disable use "none".

If you are within a resizable window/dialog/container there is a callback parameter
```<Grid onContentSize={({ width }) => setPanelSize(width)}/>```
to receive the recommended container width.

## CSV Download
CSV download relies on column valueFormatters vs ag-grid's default valueGetter implementation.
If you use a customRenderer for a column be sure to include a valueFormatter.
To disable this behaviour pass undefined to processCellCallback.
```<GridFilterDownloadCsvButton processCellCallback={undefined}/>```

To exclude a column from CSV download add ```export: false``` to the GridCell definition. 

## Writing tests

The following testing calls can be imported from step-ag-grid:

- findRow
- queryRow
- selectRow
- deselectRow
- findCell
- selectCell
- editCell
- findOpenMenu
- validateMenuOptions
- queryMenuOption
- findMenuOption
- clickMenuOption
- openAndClickMenuOption
- getMultiSelectOptions
- findMultiSelectOption
- clickMultiSelectOption
- typeOnlyInput
- typeInputByLabel
- typeInputByPlaceholder
- typeOtherInput
- typeOtherTextArea
- closeMenu
- findActionButton
- clickActionButton

```tsx
import { render, screen } from "@testing-library/react";
import { waitFor } from "@testing-library/react";
import { findRow, GridUpdatingContextProvider, openAndClickMenuOption } from "@linzjs/step-ag-grid";

const TestComponent = (): JSX.Element => {
  return (
    <GridUpdatingContextProvider>
      <MyGrid />
    </GridUpdatingContextProvider>
  );
};

test("click Delete menu option removes row from the table", async () => {
  await render(<TestComponent />);
  await screen.findByText("My component header");
  expect((await findRow(12345)).getAttribute("row-index")).toBe("1");
  await openAndClickMenuOption(12345, "actions", "Delete");
  await waitFor(async () => expect((await queryRow(12345)).not.toBeDefined()));
});
```

## Playwright support

If your grid has a data-testid a global will be exposed in window with the helper scrollRowIntoViewById.
This will throw an exception if the row id is not found.

```tsx
window.__stepAgGrid.grids[dataTestId].scrollRowIntoViewById("1000")
```