Closed daisycrego closed 3 years ago
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>
MainRouter
has an initial query state for both the event and lead pages:
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, };
- `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.
-
EventsPage
, EnhancedTable
architectureEventsPage
receives queryState
and updateQueryState
from the MainRouter
. Why? This is because the MainRouter
needs access to the query state in order to allow the user to go Back to the search results
after navigating to some page. It allows the app to persist the query state outside of the EventsPage
. The problem is that if we hold the queryState
in the MainRouter
, every time that the queryState
is updated via a call to updateQueryState
from somewhere down in the hierarchy (EnhancedTable
, TableFilterList
, etc.) the entire EventsPage
will actually be re-rendered because the props passed to it have updated.useEffect
hook in the EventsPage
to [queryState]
, initiating a new query whenever the queryState
is updated. EnhancedTable
and everything below will re-render when the query state changes, and a fresh set of data will be passed down from the EventsPage
to the components below because of the useEffect(queryState)
hook in the EventsPage
. TableFilterList
UpdatableCell
EventsPage
defines the following array for the EnhancedTable(Events)
:
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",
},
// ...
];
};
Pass the array down to the EnhancedTable
as columns
.
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,
};
EnhancedTable
uses the data retrieved based on the current queryState
(page, pageSize, categor(ies), order, etc.) to populate the columns as needed. The column description is used in combination with the queryState
categories
object.STRING
or DATE
column, the representation is fairly straightforward, as the cells are read-only.FILTERABLE
column, that means that the data at rows[column.name]
(e.g. rows.source
) fall into a distinct set of categories, which can be found at queryState.categories[column.categoriesName]
or derived directly from the rows data if needed. If the column is FILTERABLE
, the user should be able to choose which of the available categories are displayed, including what the default
is.EnhancedTable
EnhancedTable(Events)
// 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: "",
};
// 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,
},
];
};
EnhancedTable.props
title
: Follow-Up Boss Events
isLoading
: Initially false
. Set to true
when the list
API method is called to fetch the events data. Set back to false
when the data is loaded. Provides more friendly loading UI. rows
: Array of the data to be displayed. Data must be objects, of whatever structure. The table column description you pass to the table will be used to determine which values to extract from the objects. columns
: Array of objects describing the metadata for each column:
[
{
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,
},
];
- `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,
};
attr
: Array of the features available for this column. Options:
const tableAttr = {
SORTABLE: 1,
FILTERABLE: 2,
UPDATABLE: 3,
};
SORTABLE
: @TODOFILTERABLE
: If a column's data is filterable, the data in the column will fall into a set of distinct categories, which you pass to the table column descriptions (columns
) as categories
. This will be used in the table header so you can apply a filter by any, all, none of the categories:
In order for a FILTERABLE
column to work, you also need to pass some additional information about the categories to the initial state for the table (queryState
). For example, the column description for the filterable column is:
{
name: "source",
title: "Source",
type: tableDataTypes.STRING,
attr: [tableAttr.FILTERABLE],
categories: filterCategories.sources,
categoriesName: "sources",
},
categories.sources
object (categories[<categoriesName>]
) with 3 key-value pairs, all
, default
, and active
, to represent the current filter state for the column:
const initialEventSearchState = {
page: 0,
pageSize: 10,
categories: {
sources: {
active: ["Zillow Flex"],
all: null,
default: ["Zillow Flex"],
},
...
},
...
};
UPDATABLE
: If a column's data is updatable, pass an updateHandler
as well in columns
which will be used to handle the update requests. Note: The underlying assumption about an updatable
table is that the update is going to be to one of the available categories passed in categories
. And these categories
are going to be listed as options in a select dropdown:
categories
object for all of the FILTERABLE/UPDATABLE
columns:
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: "",
};
Filter Dates
and selecting a range using the date pickers. Parser
class (a class with a static generateCSV
method to generate a CSV for a given set of rows
) to the EnhancedTable
and it will be passed the current set of rows
to generate a CSV for download. The CSVParser
for the Events table uses json2csv
convert all of the data to the correct format, with the desired column names. Important to add a custom parser if there are any nested objects in the data, otherwise you will end up with large objects as values in the CSV. filterCategories
from the EnhancedTable
local state. Instead use the data received from the API endpoint only (queryState.categories.<categoriesName>.active
). Currently there is redudancy in terms of where this data of all categories is stored. Starting to become confusing with the Leads Enhanced Table.// EnhancedTable.js
<EnhancedTableCell
key={`enhanced-${index}-${row._id}`}
options={column.categories} // change to queryState.categories[column.categoryName].all
column={column}
row={row}
index={index}
classes={classes}
isUpdatingCell={
column.attr.includes(tableAttr.UPDATABLE) &&
updatingRowId &&
updatingRowId === row._id
}
updatingCellState={
column.attr.includes(tableAttr.UPDATABLE) &&
updatingRowId &&
updatingRowId === row._id
? updatingRowState
: null
}
updateRowState={setUpdatingRowState}
updateRowId={setUpdatingRowId}
/>
[ ] Ensure that queryState
is updated all
values become available (in EventsPage
). Change this:
useEffect(() => {
if (!jwt) {
setRedirectToSignin(true);
return;
}
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);
setRows(prepareEvents(data.events));
setFilterCategories({
sources: data.sources,
statuses: data.statuses,
}); // Remove use of filterCategories. Set queryState.categories[column.categoryName] instead.
}
});
return function cleanup() {
abortController.abort();
};
}, [queryState]);
updateQueryState
where setFilterCategories
is currently called. The useEffect
hook has a dependency on queryState
, so if the queryState has changed (which it will when we update the new categories, then our calling updateQueryState
will actually trigger the same handler to run again. filterCategories
with updateQueryState
, as described, and then (2) remove the dependency on queryState
and have the useEffect
hook run once when the component mounts. When queryState
is updated, the component (EventsPage
) will re-render anyway.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..
Convert the
EventsTable
/LeadsTable
to aTable
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 onfub
vszillow
distinctions, etc, not extendible.