daisycrego / jbg-admin

Admin app for Jill Biggs Group follow-up boss event management
1 stars 0 forks source link

`Table` component refactor #37

Closed daisycrego closed 3 years ago

daisycrego commented 3 years ago

Convert the EventsTable/LeadsTable to a Table component that takes a dictionary/set of props related to all of the characteristics of the table. More abstraction than currently used.

Side issue

Refactor the stage/status <Select>s to (a) work properly and (b) use less redundant code, be less domain-specific (relying on fub vs zillow distinctions, etc, not extendible.

daisycrego commented 3 years ago

Current Implementation


const initialEventSearchState = {
  page: 0,
  pageSize: 10,
  activeSources: ["Zillow Flex"],
  activeStatuses: zillowStatusOptions,
  order: "desc",
  orderBy: "created",
  startDate: null,
  endDate: null,
};

const initialLeadSearchState = {
  page: 0,
  pageSize: 10,
  activeSources: ["Zillow Flex"],
  activeFubStages: null,
  activeZillowStages: null,
  order: "desc",
  orderBy: "created",
  startDate: null,
  endDate: null,
};

const [eventSearchState, setEventSearchState] = useState(
   initialEventSearchState
);
const [leadSearchState, setLeadSearchState] = useState(
    initialLeadSearchState
);

<MainRouter>
  <Switch>
    <Route>
      <Events queryState={eventSearchState} setQueryState={setEventSearchState} />
    </Route>
    <Route>  
    <Leads queryState={leadSearchState} setQueryState={setLeadSearchState} />
    </Route>
...
</MainRouter>

<Leads>
  const [allRows, setAllRows] = useState([]);
  const [currentPageRows, setCurrentPageRows] = React.useState([]);
  const [snackbar, setSnackbar] = useState({ open: false, message: "" });
  const [redirectToSignin, setRedirectToSignin] = useState(false);
  const [sources, setSources] = useState([]);
  const [fubStages, setFubStages] = useState([]);
  const [zillowStages, setZillowStages] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [openFilter, setOpenFilter] = useState(null);

  const createSnackbarAlert = (message) => { /... 

  useEffect(() => {
    if (!jwt) {
      setRedirectToSignin(true);
    }

    const abortController = new AbortController();
    const signal = abortController.signal;

    setIsLoading(true);
    list(signal, queryState).then((data) => {
      if (data && data.error) {
        console.log(data.error);
        setIsLoading(false);
        setRedirectToSignin(true);
      } else {
        setIsLoading(false);
        setAllRows(data ? data.leads : []);
        setCurrentPageRows(
          data.leads
            ? data.leads.slice(
                queryState.page * queryState.pageSize,
                queryState.page * queryState.pageSize + queryState.pageSize
              )
            : []
        );
        setSources(data.sources);
        setFubStages(data.fubStages);
        setZillowStages(
          data.zillowStages ? data.zillowStages : zillowStageOptions
        );
      }
    });

    return function cleanup() {
      abortController.abort();
    };
  }, []);

  const updateLeads = (initialState) => {
    const abortController = new AbortController();
    const signal = abortController.signal;

    setIsLoading(true);
    list(signal, initialState).then((data) => {
      if (data && data.error) {
        console.log(data.error);
        setIsLoading(false);
        handleUpdate(null, "filter");
        set;
        setRedirectToSignin(true);
      } else {
        setAllRows(data ? data.leads : []);
        const page =
          data && data.leads
            ? data.leads.slice(
                queryState.page * queryState.pageSize,
                queryState.page * queryState.pageSize + queryState.pageSize
              )
            : [];
        setCurrentPageRows(page);
        setSources(data.sources ? data.sources : []);
        setFubStages(data.fubStages ? data.fubStages : []);
        setZillowStages(
          data.zillowStages ? data.zillowStages : zillowStageOptions
        );
        setIsLoading(false);
        handleUpdate(null, "filter");
      }
    });
  };

  <LeadsTable 
    handleSyncLeadsClick={confirmSyncLeadsClick}
    isLoading={isLoading}
    activeRows={allRows}
    currentPageRows={currentPageRows}
    sources={sources}
    fubStages={fubStages}
    zillowStages={zillowStages}
    zillowStageOptions={zillowStageOptions}
    openFilter={openFilter}
    createSnackbarAlert={createSnackbarAlert}
    queryState={queryState}
    updateQueryState={(e) => handleUpdate(e, "datePicker")}
    handleUpdate={handleUpdate}
  />
</Leads>

<LeadsTable>
  const [showSourceSelect, setShowSourceSelect] = useState(false);
  const [updatingRow, setUpdatingRow] = useState(null);
  const [updatingStage, setUpdatingStage] = useState(null);
  const [zillowStage, setZillowStage] = useState("");
  const [fubStage, setFubStage] = useState("");
  const [showDatePicker, setShowDatePicker] = useState(false);
  const [startDate, setStartDate] = useState(queryState.startDate);
  const [endDate, setEndDate] = useState(queryState.endDate);  

  const handleFilterClick = (type) => {
  const handleSelect = (type, newValues) => {
  const handleChangePage = (event, newPage) => {
  const handleChangeRowsPerPage = (event) => {
  const handleDatesChange = (data, type) => {
  const handleRequestSort = (event, property) => {
  const selectRowState = (row, stageType) => { 
  const handleStageCheckboxClick = (stage, stageType) => {
  const handleCheckboxClick = (source) => {
  const handleStageSelectSubmit = (rowId, stage, lead, stageType) => {
  const handleStageSelectUpdate = (e, stageType) => {
  const handleUpdateStageClick = (rowId, rowStatus, stageType) => {

  const data = (row, stageType, menuOptions) => {
    if (
      showSourceSelect &&
      updatingRow &&
      row._id === updatingRow &&
      stageType === updatingStage
    ) {
      return (
        <>
          <Select
            labelId="status-select"
            id={`status_select_${row._id}`}
            value={selectRowState(row, stageType)}
            key={`select_${row._id}`}
            onChange={(e) => handleStageSelectUpdate(e, stageType)}
          >
            {menuOptions.map((option) => (
              <MenuItem key={option} value={option}>
                {option}
              </MenuItem>
            ))}
          </Select>
          <IconButton
            aria-label="save"
            color="primary"
            onClick={(e) =>
              handleStageSelectSubmit(
                row._id,
                stageType === "zillow" ? zillowStage : fubStage,
                row,
                stageType
              )
            }
          >
            <Tooltip title="Save changes">
              <Check />
            </Tooltip>
          </IconButton>
          <IconButton
            aria-label="cancel"
            color="primary"
            onClick={() => {
              console.log(`a`);
              handleUpdateStageClick(
                row._id,
                selectRowState(row, stageType),
                stageType
              );
            }}
          >
            <Tooltip title="Cancel changes">
              <Cancel />
            </Tooltip>
          </IconButton>
          <IconButton
            aria-label="delete"
            color="primary"
            onClick={() => {
              console.log(`b`);
              handleUpdateStageClick(row._id, "", updatingStage);
            }}
          >
            <Tooltip title="Clear Stage">
              <Delete />
            </Tooltip>
          </IconButton>
        </>
      );
    } else {
      return (
        <Tooltip title="Edit">
          <Button
            key={`status_button_${row._id}`}
            onClick={() => {
              console.log(`c`);
              handleUpdateStageClick(
                row._id,
                selectRowState(row, stageType),
                stageType
              );
            }}
          >
            {selectRowState(row, stageType)}
            <Edit key={`edit_icon_${row._id}`} />
          </Button>
        </Tooltip>
      );
    }
  };

  <div>
    <Paper>
      <EnhancedTableToolbar
        rows={activeRows}
        showDatePicker={showDatePicker}
        setShowDatePicker={setShowDatePicker}
        startDate={startDate}
        endDate={endDate}
        handleDatesChange={handleDatesChange}
        setStartDate={setStartDate}
        setEndDate={setEndDate}
        createSnackbarAlert={createSnackbarAlert}
        handleSyncLeadsClick={handleSyncLeadsClick}
        queryState={queryState}
        updateQueryState={updateQueryState}
        handleUpdate={handleUpdate}
      />

    <TableContainer>
      <Table>
        <EnhancedTableHead
              classes={classes}
              queryState={queryState}
              onRequestSort={handleRequestSort}
              onSourceFilterClick={() => handleFilterClick("source")}
              openFilter={openFilter}
              sources={sources}
              fubStages={fubStages}
              zillowStages={zillowStages}
              onCheckboxClick={handleCheckboxClick}
              onFubStageCheckboxClick={(e) =>
                handleStageCheckboxClick(e, "fubStage")
              }
              onZillowStageCheckboxClick={(e) =>
                handleStageCheckboxClick(e, "zillowStage")
              }
              onSelectAllSources={() => handleSelect("source", sources)}
              onClearSources={() => handleSelect("source", [])}
              onResetSources={() => handleSelect("source", sources)}
              onFubStageFilterClick={() => handleFilterClick("fubStage")}
              onZillowStageFilterClick={() => handleFilterClick("zillowStage")}
              onSelectAllFubStages={() => handleSelect("fubStage", fubStages)}
              onSelectAllZillowStages={() =>
                handleSelect("zillowStage", zillowStages)
              }
              onClearFubStages={() => handleSelect("fubStage", [])}
              onClearZillowStages={() => handleSelect("zillowStage", [])}
              onResetFubStages={() => handleSelect("fubStage", fubStages)}
              onResetZillowStages={() =>
                handleSelect("zillowStage", zillowStages)
              }
        />
        <TableBody>
              {isLoading ? (
                <TableRow>
                  <TableCell>
                    <h2>Loading...</h2>
                  </TableCell>
                </TableRow>
              ) : null}
              {currentPageRows.map((row, index) => {
                const labelId = `enhanced-table-checkbox-${index}`;

                return (
                  <TableRow hover tabIndex={-1} key={row._id}>
                    <TableCell
                      component="th"
                      id={labelId}
                      scope="row"
                      align={"center"}
                      padding={"normal"}
                    >
                      {row.name ? row.name : ""}
                    </TableCell>
                    <TableCell
                      component="th"
                      id={labelId}
                      scope="row"
                      align={"center"}
                      padding={"normal"}
                    >
                      {row.phones && row.phones.length
                        ? row.phones[0].value
                        : ""}
                    </TableCell>
                    <TableCell
                      component="th"
                      id={labelId}
                      scope="row"
                      align={"center"}
                      padding={"normal"}
                    >
                      {row.emails && row.emails.length
                        ? row.emails[0].value
                        : ""}
                    </TableCell>
                    <TableCell align={"center"} padding={"normal"}>
                      {`${new Date(row.created).toDateString()} ${new Date(
                        row.created
                      ).toLocaleTimeString()}`}
                    </TableCell>
                    <TableCell align={"center"} padding={"normal"}>
                      {row.source}
                    </TableCell>
                    <TableCell align={"center"} padding={"normal"}>
                      {data(row, "fub", fubStages)}
                    </TableCell>
                    <TableCell align={"center"} padding={"normal"}>
                      {data(row, "zillow", zillowStageOptions)}
                    </TableCell>
                    <TableCell align={"center"} padding={"normal"}>
                      <Tooltip title="More">
                        <Link to={"/lead/" + row._id} key={row._id}>
                          <IconButton
                            color="primary"
                            variant="contained"
                            className={classes.button}
                          >
                            <ArrowForward />
                          </IconButton>
                        </Link>
                      </Tooltip>
                    </TableCell>
                  </TableRow>
                );
              })}
              {emptyRows > 0 && (
                <TableRow style={{ height: 53 * emptyRows }}>
                  <TableCell colSpan={6} />
                </TableRow>
              )}
            </TableBody>
      </Table>
    </TableContainer>
    <TablePagination
          rowsPerPageOptions={[5, 10, 25, 50, 100]}
          component="div"
          count={activeRows.length}
          rowsPerPage={queryState.pageSize}
          page={queryState.page}
          onPageChange={handleChangePage}
          onRowsPerPageChange={handleChangeRowsPerPage}
        />
    </Paper>
  </div>
</LeadsTable>
daisycrego commented 3 years ago

Current Implementation (cont)

const initialLeadSearchState = { page: 0, pageSize: 10, activeSources: ["Zillow Flex"], activeFubStages: null, activeZillowStages: null, order: "desc", orderBy: "created", startDate: null, endDate: null, };


- `eventSearchState` and `leadSearchState` are stored as state (hooks) of the `MainRouter`, and the `set` functions for the state are passed down to the `Events` and `Leads` pages. When `setQueryState` is called by `Events`/`Leads`, the page will be re-rendered and the new query will be run (as the query is run once when the component is loaded in the `useEffect` handler, or when `updateEvents`/`updateLeads` is called within the `Events`/`Leads` pages themselves.
- 
daisycrego commented 3 years ago

EventsPage, EnhancedTable architecture

daisycrego commented 3 years ago

Column Types

Using Column Description and Query State

Also define a queryState and updateQueryState handler, where queryState is an object with all of the state for the table:

// MainRouter.js
const initialTableSearchState = {
  page: 0,
  pageSize: 10,
  categories: {
    sources: {
      active: ["Zillow Flex"],
      all: null,
      default: ["Zillow Flex"],
    },
    statuses: {
      active: zillowStatusOptions,
      all: null,
      default: zillowStatusOptions,
    },
  },
  order: "desc",
  orderBy: "created",
  startDate: null,
  endDate: null,
};
daisycrego commented 3 years ago

EnhancedTable

Example - EnhancedTable(Events)

Initial search state

// MainRouter.js
const initialEventSearchState = {
  page: 0,
  pageSize: 10,
  categories: {
    sources: {
      active: ["Zillow Flex"],
      all: null,
      default: ["Zillow Flex"],
    },
    statuses: {
      active: zillowStatusOptions,
      all: null,
      default: zillowStatusOptions,
    },
    isPossibleZillowExemption: {
      active: [booleanOptions.true, booleanOptions.false],
      all: [booleanOptions.true, booleanOptions.false],
      default: [booleanOptions.true, booleanOptions.false],
    },
  },
  order: "desc",
  orderBy: "created",
  startDate: null,
  endDate: null,
  searchText: "",
};

Table columns definition

// EventsPage.js
const generateColumnDesc = () => {
    return [
      {
        name: "propertyStreet",
        title: "Property address",
        type: tableDataTypes.STRING,
        attr: [],
        categories: null,
        categoriesName: null,
      },
      {
        name: "created",
        title: "Created",
        type: tableDataTypes.DATE,
        attr: [],
        categories: null,
        categoriesName: null,
      },
      {
        name: "source",
        title: "Source",
        type: tableDataTypes.STRING,
        attr: [tableAttr.FILTERABLE],
        categories: filterCategories.sources, // if null, base the filter options on the available data (eg distinct values are the categories)
        categoriesName: "sources", // queryState.categories[categoriesName] will have the `all`, `default`, and `active` arrays for the current query state
      },
      {
        name: "status",
        title: "Status",
        type: tableDataTypes.STRING,
        attr: [tableAttr.FILTERABLE, tableAttr.UPDATABLE],
        categories: filterCategories.statuses,
        categoriesName: "statuses",
        updateHandler: handleStatusUpdate,
      },
      {
        name: "isPossibleZillowExemption",
        title: "Possible Zillow Flex exemption?",
        type: tableDataTypes.BOOLEAN,
        attr: [],
        categories: null,
        categoriesName: null,
      },
      {
        name: "id",
        title: "More",
        type: tableDataTypes.LINK,
        attr: [],
        categories: null,
        categoriesName: null,
      },
    ];
  };

Table UI

image

Features

EnhancedTable.props
  - `name`: Used to access the data from `rows`. `rows[columns.name]`, e.g. if `columns.name` is `status`, `rows["status"]` should have the data for the column.  
  - `title`: For display - used in the table header for this column. E.g. `Status` or `Is Possible Zillow Exemption?`
  - `type`: Type of data stored in the column. Options:
```js
// lib/table.js
const tableDataTypes = {
  STRING: 1,
  NUMBER: 2,
  DATE: 3,
  LINK: 4,
  BOOLEAN: 5,
};

Query State

Screen Shot 2021-08-17 at 10 26 50 AM Screen Shot 2021-08-17 at 10 26 59 AM Screen Shot 2021-08-17 at 10 27 10 AM Screen Shot 2021-08-17 at 10 27 31 AM Screen Shot 2021-08-17 at 10 27 45 AM
daisycrego commented 3 years ago
daisycrego commented 3 years ago

Ended up keeping filterCategories as local state, but changed the way that I create the filterCategories object, deriving it from the column metadata rather than requiring the filterCategories to be hard-coded in the definition of the new FooPage, BarPage, etc..