Repository: cityofaustin/atd-moped
Pull request #1567: 18175 migrate dashboard tables to datagridpro
Author: login: tillyw
Created: 2025-03-20T23:48:09Z | Merged: 2025-04-24T17:52:44Z

## Associated issues
fixes https://github.com/cityofaustin/atd-data-tech/issues/18175

## Testing
**URL to test:** 
<!---
deploy-preview-[pr-number]--atd-moped-main.netlify.app/moped/
--->
https://deploy-preview-1567--atd-moped-main.netlify.app/

**Steps to test:**
1) make sure you are following and assigned to at least two or three projects and visit the dashboard
2) test the 'my projects' and 'following' tabs by clicking the full name to make sure the link directs you to the correct project, clicking the status field to update the current status, adding a status update, and adding or setting milestones as completed
3) for each change, the view should automatically refetch the updated field
4) test the sorting on each column to make sure it sorts as expected, pinning columns, filtering, and hiding and managing columns


---
#### Ship list
- [x] Code reviewed 
- [x] Product manager approved
- [x] Product manager checked [DB view dependencies](https://atd-dts.gitbook.io/moped-documentation/product-management/updating-the-arcgis-online-components-database-view)
- [x] Product manager added to [QA test script](https://docs.google.com/spreadsheets/d/1n_O6MLh9cwwPf57HUM394Ea-z9uuoEV1-QW4axNZXLE/edit#) if applicable
   - Updated existing test - "Look at the milestones % complete for a project on the dashboard. Add a milestone to the project, set it to complete, and revisit dashboard to ensure the % in the milestone column changes. You should see the edited cell highlighted in the table."  



Diff:
diff --git a/moped-editor/src/utils/dateAndTime.js b/moped-editor/src/utils/dateAndTime.js
index 12e6ee450a..361ed625a9 100644
--- a/moped-editor/src/utils/dateAndTime.js
+++ b/moped-editor/src/utils/dateAndTime.js
@@ -1,4 +1,5 @@
 import theme from "src/theme";
+import { format } from "date-fns";
 
 /**
  * Format date from the DB in YYYY-MM-DD format into human-readable locale date
@@ -168,3 +169,25 @@ export const FormattedDateString = ({ date, primary, secondary }) => {
     </span>
   );
 };
+
+/**
+ * Get the time of day to display to users on the dashboard
+ * @returns {string} - Time of day phrased as a salutation
+ */
+export const getTimeOfDay = (date) => {
+  const curHr = date.getHours();
+  switch (true) {
+    case curHr < 12:
+      return "morning";
+    case curHr >= 12 && curHr < 18:
+      return "afternoon";
+    default:
+      return "evening";
+  }
+};
+
+/**
+ * Get the calendar date to display to users on the dashboard
+ * @returns {string} - Date formatted in a user-friendly format
+ */
+export const getCalendarDate = (date) => format(date, "EEEE - LLLL dd, yyyy");
diff --git a/moped-editor/src/views/dashboard/DashboardStatusModal.js b/moped-editor/src/views/dashboard/DashboardStatusModal.js
index ac95526074..185a2fd2b5 100644
--- a/moped-editor/src/views/dashboard/DashboardStatusModal.js
+++ b/moped-editor/src/views/dashboard/DashboardStatusModal.js
@@ -1,11 +1,11 @@
 import React, { useState } from "react";
 import {
-  Typography,
   Dialog,
   DialogTitle,
   DialogContent,
   IconButton,
   Tooltip,
+  Typography,
 } from "@mui/material";
 import clsx from "clsx";
 import CloseIcon from "@mui/icons-material/Close";
@@ -22,7 +22,7 @@ const DashboardStatusModal = ({
   handleSnackbar,
   children,
   classes,
-  data
+  data,
 }) => {
   const [isDialogOpen, setIsDialogOpen] = useState(false);
 
diff --git a/moped-editor/src/views/dashboard/DashboardView.js b/moped-editor/src/views/dashboard/DashboardView.js
index 0b84d8fb23..7946cf1757 100644
--- a/moped-editor/src/views/dashboard/DashboardView.js
+++ b/moped-editor/src/views/dashboard/DashboardView.js
@@ -1,6 +1,6 @@
-import React, { useState } from "react";
+import React, { useState, useMemo, useEffect } from "react";
 import { useQuery } from "@apollo/client";
-import { format } from "date-fns";
+import { NavLink as RouterLink } from "react-router-dom";
 
 // Material
 import {
@@ -10,16 +10,15 @@ import {
   CircularProgress,
   Container,
   Grid,
+  Link,
   Tab,
   Tabs,
   Typography,
 } from "@mui/material";
-import MaterialTable from "@material-table/core";
 import makeStyles from "@mui/styles/makeStyles";
 
 import Page from "src/components/Page";
 import ActivityMetrics from "src/components/ActivityMetrics";
-import RenderFieldLink from "../../components/RenderFieldLink";
 import DashboardStatusModal from "./DashboardStatusModal";
 import DashboardTimelineModal from "./DashboardTimelineModal";
 import ProjectStatusBadge from "../projects/projectView/ProjectStatusBadge";
@@ -28,8 +27,9 @@ import FeedbackSnackbar, {
   useFeedbackSnackbar,
 } from "src/components/FeedbackSnackbar";
 import UserSavedViewsTable from "./UserSavedViewsTable";
-
-import typography from "../../theme/typography";
+import { DataGridPro } from "@mui/x-data-grid-pro";
+import dataGridProStyleOverrides from "src/styles/dataGridProStylesOverrides";
+import { getTimeOfDay, getCalendarDate } from "src/utils/dateAndTime";
 
 import { DASHBOARD_QUERY } from "../../queries/dashboard";
 
@@ -79,6 +79,12 @@ const useStyles = makeStyles((theme) => ({
   tooltipIcon: {
     fontSize: "20px",
   },
+  tableRowDiv: {
+    display: "flex",
+    alignItems: "center",
+    paddingTop: theme.spacing(2),
+    paddingBottom: theme.spacing(2),
+  },
 }));
 
 function a11yProps(index) {
@@ -100,19 +106,155 @@ const TABS = [
   },
 ];
 
+const UserGreeting = ({ classes, userName }) => {
+  const date = new Date();
+
+  /** Build custom user greeting
+   */
+  return (
+    <Grid className={classes.greeting}>
+      <Typography className={classes.greetingText}>
+        <strong>{`Good ${getTimeOfDay(date)}, ${userName}!`}</strong>
+      </Typography>
+      <Typography variant="h1" className={classes.date}>
+        {getCalendarDate(date)}
+      </Typography>
+    </Grid>
+  );
+};
+
+/** Hook that provides memoized column settings */
+const useColumns = ({ data, refetch, handleSnackbar, classes }) =>
+  useMemo(() => {
+    return [
+      {
+        headerName: "ID",
+        field: "project_id",
+        editable: false,
+        renderCell: ({ row }) => (
+          <div className={classes.tableRowDiv}>{row.project_id}</div>
+        ),
+        flex: 0.5,
+      },
+      {
+        headerName: "Full name",
+        field: "project_name_full",
+        editable: false,
+        valueGetter: (value) => value.trim(),
+        renderCell: ({ row }) => (
+          <div className={classes.tableRowDiv}>
+            <Link
+              component={RouterLink}
+              to={`/moped/projects/${row.project_id}`}
+              sx={{
+                overflow: "hidden",
+                textOverflow: "ellipsis",
+                display: "block",
+                fontSize: 16,
+              }}
+            >
+              {row.project_name_full}
+            </Link>
+          </div>
+        ),
+        flex: 4,
+      },
+      {
+        headerName: "Status",
+        field: "moped_proj_phases",
+        editable: false,
+        valueGetter: (value) => value[0]?.moped_phase.phase_name || "Unknown",
+        renderCell: ({ row }) => (
+          <div className={classes.tableRowDiv}>
+            <DashboardTimelineModal
+              table="phases"
+              projectId={row.project_id}
+              projectName={row.project_name_full}
+              dashboardRefetch={refetch}
+              handleSnackbar={handleSnackbar}
+            >
+              <ProjectStatusBadge
+                phaseName={row.moped_proj_phases?.[0]?.moped_phase.phase_name}
+                phaseKey={row.moped_proj_phases?.[0]?.moped_phase.phase_key}
+                condensed
+                clickable
+              />
+            </DashboardTimelineModal>
+          </div>
+        ),
+        width: 200,
+      },
+      {
+        headerName: "Status update",
+        field: "moped_proj_notes",
+        editable: false,
+        sortable: false,
+        renderCell: ({ row }) => (
+          // Display status update (from Project details page), i.e., most recent note
+          <div className={classes.tableRowDiv}>
+            <DashboardStatusModal
+              projectId={row.project_id}
+              projectName={row.project_name_full}
+              currentPhaseId={row.moped_proj_phases?.[0]?.moped_phase.phase_id}
+              modalParent="dashboard"
+              statusUpdate={row.moped_proj_notes?.[0]?.project_note ?? ""}
+              queryRefetch={refetch}
+              handleSnackbar={handleSnackbar}
+              classes={classes}
+              data={data}
+            >
+              {parse(String(row.moped_proj_notes?.[0]?.project_note)) ?? ""}
+            </DashboardStatusModal>
+          </div>
+        ),
+        flex: 4,
+      },
+      {
+        headerName: "Milestones",
+        field: "moped_proj_milestones",
+        valueGetter: (value) =>
+          value.length
+            ? (value.filter((milestone) => milestone.completed === true)
+                .length /
+                value.length) *
+              100
+            : 0,
+        renderCell: ({ row }) => (
+          // Display percentage of milestone completed, or 0 if no milestones saved
+          <div className={classes.tableRowDiv}>
+            <DashboardTimelineModal
+              table="milestones"
+              projectId={row.project_id}
+              projectName={row.project_name_full}
+              handleSnackbar={handleSnackbar}
+              dashboardRefetch={refetch}
+            >
+              <MilestoneProgressMeter
+                completedMilestonesPercentage={
+                  row.moped_proj_milestones.length
+                    ? (row.moped_proj_milestones.filter(
+                        (milestone) => milestone.completed === true
+                      ).length /
+                        row.moped_proj_milestones.length) *
+                      100
+                    : 0
+                }
+              />
+            </DashboardTimelineModal>
+          </div>
+        ),
+        flex: 1,
+      },
+    ];
+  }, [data, refetch, handleSnackbar, classes]);
+
 const DashboardView = () => {
+  const classes = useStyles();
+
   const userSessionData = getSessionDatabaseData();
   const userId = userSessionData?.user_id;
   const userName = userSessionData?.first_name;
 
-  const classes = useStyles();
-  const typographyStyle = {
-    fontFamily: typography.fontFamily,
-    fontSize: "14px",
-  };
-
-  const [activeTab, setActiveTab] = useState(0);
-
   const { loading, error, data, refetch } = useQuery(DASHBOARD_QUERY, {
     variables: { userId },
     fetchPolicy: "no-cache",
@@ -125,154 +267,42 @@ const DashboardView = () => {
     console.log(error);
   }
 
-  let selectedData = [];
-
-  if (TABS[activeTab].label === "Following" && !!data) {
-    selectedData = data.moped_user_followed_projects;
-  } else if (TABS[activeTab].label === "My projects" && !!data) {
-    selectedData = data.moped_proj_personnel;
-  }
+  const [activeTab, setActiveTab] = useState(0);
 
-  if (selectedData) {
-    /**
-     * Build data needed in Dashboard Material Table
-     */
-    selectedData.forEach((project) => {
-      project["project_name_full"] = project.project.project_name_full;
-      project["project_id"] = project.project.project_id;
-      project["phase_name"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
-      project["phase_key"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
-      project["current_phase_id"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
-      /**
-       * Get percentage of milestones completed
-       */
-      const milestonesTotal = project.project.moped_proj_milestones.length;
-      const milestonesCompleted = project.project.moped_proj_milestones.filter(
-        (milestone) => milestone.completed === true
-      ).length;
-      project["completed_milestones_percentage"] = !!milestonesTotal
-        ? (milestonesCompleted / milestonesTotal) * 100
-        : 0;
+  // rows and rowModesModel used in DataGrid
+  const [rows, setRows] = useState([]);
+  const [rowModesModel, setRowModesModel] = useState({});
 
-      // project status update equivalent to most recent project note
-      // html is parsed before being rendered in the DashboardStatusModal component
-      project["status_update"] = "";
-      if (project?.project?.moped_proj_notes?.length) {
-        const note = project.project.moped_proj_notes[0]["project_note"];
-        project["status_update"] = note ? note : "";
+  // sets the data grid row data when query data is fetched
+  useEffect(() => {
+    if (data) {
+      if (TABS[activeTab].label === "My projects") {
+        setRows(data.moped_proj_personnel.map((row) => row.project));
+      }
+      if (TABS[activeTab].label === "Following") {
+        setRows(data.moped_user_followed_projects.map((row) => row.project));
       }
-    });
-  }
-
-  /** Build custom user greeting
-   */
-  const date = new Date();
-  const curHr = format(date, "HH");
-  const dateFormatted = format(date, "EEEE - LLLL dd, yyyy");
-  const getTimeOfDay = (curHr) => {
-    switch (true) {
-      case curHr < 12:
-        return "morning";
-      case curHr >= 12 && curHr < 18:
-        return "afternoon";
-      default:
-        return "evening";
     }
-  };
+  }, [data, activeTab]);
 
-  const columns = [
-    {
-      title: "ID",
-      field: "project.project_id",
-      editable: "never",
-      cellStyle: { ...typographyStyle },
-      width: "10%",
-    },
-    {
-      title: "Full name",
-      field: "project.project_name_full",
-      editable: "never",
-      cellStyle: { ...typographyStyle },
-      render: (entry) => (
-        <RenderFieldLink
-          projectId={entry.project_id}
-          value={entry.project_name_full}
-        />
-      ),
-      width: "20%",
-    },
-    {
-      title: "Status",
-      field: "phase_name",
-      editable: "never",
-      render: (entry) => (
-        <DashboardTimelineModal
-          table="phases"
-          projectId={entry.project_id}
-          projectName={entry.project.project_name_full}
-          dashboardRefetch={refetch}
-          handleSnackbar={handleSnackbar}
-        >
-          <ProjectStatusBadge
-            phaseName={entry.phase_name}
-            phaseKey={entry.phase_key}
-            condensed
-            clickable
-          />
-        </DashboardTimelineModal>
-      ),
-      width: "20%",
-    },
-    {
-      title: "Status update",
-      field: "status_update", // Status update (from Project details page)
-      editable: "never",
-      render: (entry) => (
-        <DashboardStatusModal
-          projectId={entry.project_id}
-          projectName={entry.project.project_name_full}
-          currentPhaseId={entry.current_phase_id}
-          modalParent="dashboard"
-          statusUpdate={entry.status_update}
-          queryRefetch={refetch}
-          handleSnackbar={handleSnackbar}
-          classes={classes}
-          data={data}
-        >
-          {parse(String(entry.status_update))}
-        </DashboardStatusModal>
-      ),
-      width: "40%",
-    },
-    {
-      title: "Milestones",
-      field: "completed_milestones",
-      render: (entry) => (
-        <DashboardTimelineModal
-          table="milestones"
-          projectId={entry.project_id}
-          projectName={entry.project.project_name_full}
-          handleSnackbar={handleSnackbar}
-          dashboardRefetch={refetch}
-        >
-          <MilestoneProgressMeter
-            completedMilestonesPercentage={
-              entry.completed_milestones_percentage
-            }
-          />
-        </DashboardTimelineModal>
-      ),
-      width: "10%",
-    },
-  ];
+  const handleRowModesModelChange = (newRowModesModel) => {
+    setRowModesModel(newRowModesModel);
+  };
 
   const handleChange = (event, newValue) => {
     setActiveTab(newValue);
   };
 
+  const dataGridColumns = useColumns({
+    data,
+    rowModesModel,
+    refetch,
+    handleSnackbar,
+    classes,
+  });
+
+  if (loading || !data) return <CircularProgress />;
+
   return (
     <ActivityMetrics eventName="dashboard_load">
       <Page title={"Dashboard"}>
@@ -280,16 +310,7 @@ const DashboardView = () => {
           <Card className={classes.cardWrapper}>
             <Grid className={classes.root}>
               <Box pl={3} pt={3}>
-                <Grid className={classes.greeting}>
-                  <Typography className={classes.greetingText}>
-                    <strong>{`Good ${getTimeOfDay(
-                      curHr
-                    )}, ${userName}!`}</strong>
-                  </Typography>
-                  <Typography variant="h1" className={classes.date}>
-                    {dateFormatted}
-                  </Typography>
-                </Grid>
+                <UserGreeting classes={classes} userName={userName} />
               </Box>
               <Box px={3} py={3}>
                 <Grid>
@@ -316,31 +337,19 @@ const DashboardView = () => {
                   ) : TABS[activeTab].label === "Saved views" ? (
                     <UserSavedViewsTable handleSnackbar={handleSnackbar} />
                   ) : (
-                    <MaterialTable
-                      columns={columns}
-                      data={selectedData}
-                      localization={{
-                        body: {
-                          emptyDataSourceMessage: (
-                            <Typography>
-                              {TABS[activeTab].label === "Following" &&
-                                "No projects to display. You have not followed any current projects."}
-                              {TABS[activeTab].label === "My projects" &&
-                                "No projects to display. You are not listed as a Team Member on any current projects."}
-                            </Typography>
-                          ),
-                        },
-                      }}
-                      options={{
-                        search: false,
-                        toolbar: false,
-                        tableLayout: "fixed",
-                        ...(selectedData.length < 51 && {
-                          paging: false,
-                        }),
-                        pageSize: 50,
-                        pageSizeOptions: [10, 50, 100],
-                        idSynonym: "project_id",
+                    <DataGridPro
+                      sx={dataGridProStyleOverrides}
+                      columns={dataGridColumns}
+                      rows={rows}
+                      autoHeight
+                      getRowHeight={() => "auto"}
+                      getRowId={(row) => row.project_id}
+                      rowModesModel={rowModesModel}
+                      onRowModesModelChange={handleRowModesModelChange}
+                      hideFooter
+                      disableRowSelectionOnClick
+                      localeText={{
+                        noRowsLabel: "No projects to display",
                       }}
                     />
                   )}


Reviews:
user: login: chiaberry
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T15:52:12Z
user: login: chiaberry
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T16:03:50Z
user: login: chiaberry
body: Congrats on getting rid of what I think is the last table that is using material table! I tested and functionality works great, I did leave some comments about possible performance gains, wondering if you think they are feasible?


state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T16:06:09Z
user: login: tillyw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T17:11:37Z
user: login: tillyw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T17:13:51Z
user: login: chiaberry
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T18:15:04Z
user: login: frankhereford
body: Thanks for this Tilly -- I've gone over your code and the site on the netlify deploy, and I think you've checked all the boxes on this issue and done it really well. I have an observation about some rendering things that I am not sure if you consider in scope or not -- but they are minor. This looks great.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-10T18:53:22Z
user: login: tillyw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T02:53:37Z
user: login: johnclary
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T14:44:34Z
user: login: johnclary
body: The grid is working really well! I flagged a few potential concerns, a few of which may be picking nits 🙏 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T15:05:18Z
user: login: tillyw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T15:55:34Z
user: login: tillyw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T20:50:41Z
user: login: tillyw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T21:55:02Z
user: login: tillyw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T19:51:49Z
user: login: mddilley
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T17:22:17Z
user: login: mddilley
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T17:45:31Z
user: login: mddilley
body: So much code clean up ✨ - thanks for addressing the suggested improvements while you were in here. I requested a few changes to polish before launch, but this is working just as it should.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T18:39:04Z
user: login: mddilley
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T18:45:13Z
user: login: tillyw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T21:53:25Z
user: login: tillyw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T22:07:48Z
user: login: johnclary
body: Looking good, Tilly! Thanks for making these updates.

One issue I'm noticing is that sorting does not seem to have any affect. I am testing with the **Followed** projects

<img width="541" alt="Screenshot 2025-04-21 at 11 41 11 AM" src="https://github.com/user-attachments/assets/d2663c8c-40fb-481a-857b-ad3320ec01f9" />

state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-21T15:41:28Z
user: login: mddilley
body: I requested a few smaller changes, and I'd like the sorting to work as well. I think that Rose and Chia are good resources since they've worked with sorting in other tables recently. I'm happy to rubber duck on getting the sorting working as well!
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T18:07:24Z
user: login: tillyw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T18:34:43Z
user: login: tillyw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T19:13:14Z
user: login: chiaberry
body: all the updates look good and the DashboardView code is in a much better place now than what's in prod. (sounds like I think its dead, but you know what i really mean)

I agree that I dont think its necessary to be able to sort the project status update alphabetically. Sorting it on timestamp of when last updated could be a helpful future enhancement, but we'd need to surface the timestamp in this table too.

When I sorted by project name, I noticed that project 17 and project 3601 both have leading empty spaces in their project name. @mddilley do you think we should clean up project names leading spaces? dont know if its affecting sorting on agol too
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T14:18:22Z
user: login: roseeichelmann
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T19:33:03Z
user: login: roseeichelmann
body: this looks great tilly!! so exciting to be done with the datagridpro migration!! its been a long time coming. thanks for getting us over the finish line! other than the one line fix i mentioned in a comment to make sure the status sorts alphabetically correctly, this is good to go!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T19:36:18Z
user: login: tillyw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T20:32:08Z
user: login: mddilley
body: Looks amazing! Thanks for adding these final touches. 🙏 Can't wait to have this final table out in the wild! 🚀 🚢  🙌

Forgot to mention that a hidden improvement in this code is that it is now much easier to see which status or milestone was updated after editing in the modals since DataGrid Pro highlights cells. This has been a note in the QA spreadsheet for a while that we get for free. 🙌
 

https://github.com/user-attachments/assets/666e3454-576f-4ed0-9385-91799ad0f0dd


state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-23T22:31:52Z
user: login: mateoclarke
body: Tested and works as described. 🚀
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:56:51Z

Review comments:
diff_hunk: @@ -125,47 +211,66 @@ const DashboardView = () => {
     console.log(error);
   }
 
-  let selectedData = [];
+  const [activeTab, setActiveTab] = useState(0);
 
-  if (TABS[activeTab].label === "Following" && !!data) {
-    selectedData = data.moped_user_followed_projects;
-  } else if (TABS[activeTab].label === "My projects" && !!data) {
-    selectedData = data.moped_proj_personnel;
-  }
+  // rows and rowModesModel used in DataGrid
+  const [rows, setRows] = useState([]);
+  const [rowModesModel, setRowModesModel] = useState({});
 
-  if (selectedData) {
-    /**
-     * Build data needed in Dashboard Material Table
-     */
-    selectedData.forEach((project) => {
-      project["project_name_full"] = project.project.project_name_full;
-      project["project_id"] = project.project.project_id;
-      project["phase_name"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
-      project["phase_key"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
-      project["current_phase_id"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+  // sets the data grid row data when query data is fetched
+  useEffect(() => {
+    let selectedData = [];
+
+    if (TABS[activeTab].label === "Following" && !!data) {
+      selectedData = data.moped_user_followed_projects;
+    } else if (TABS[activeTab].label === "My projects" && !!data) {
+      selectedData = data.moped_proj_personnel;
+    }
+
+    if (selectedData) {
       /**
-       * Get percentage of milestones completed
+       * Build data needed in Dashboard Material Table
        */
-      const milestonesTotal = project.project.moped_proj_milestones.length;
-      const milestonesCompleted = project.project.moped_proj_milestones.filter(
-        (milestone) => milestone.completed === true
-      ).length;
-      project["completed_milestones_percentage"] = !!milestonesTotal
-        ? (milestonesCompleted / milestonesTotal) * 100
-        : 0;
+      selectedData.forEach((project) => {
+        project["project_name_full"] = project.project.project_name_full;
+        project["project_id"] = project.project.project_id;
+        project["phase_name"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
+        project["phase_key"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
+        project["current_phase_id"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+        /**
+         * Get percentage of milestones completed
+         */
+        const milestonesTotal = project.project.moped_proj_milestones.length;
+        const milestonesCompleted =
+          project.project.moped_proj_milestones.filter(
+            (milestone) => milestone.completed === true
+          ).length;
+        project["completed_milestones_percentage"] = !!milestonesTotal
+          ? (milestonesCompleted / milestonesTotal) * 100
+          : 0;
 
-      // project status update equivalent to most recent project note
-      // html is parsed before being rendered in the DashboardStatusModal component
-      project["status_update"] = "";
-      if (project?.project?.moped_proj_notes?.length) {
-        const note = project.project.moped_proj_notes[0]["project_note"];
-        project["status_update"] = note ? note : "";
-      }
-    });
-  }
+        project["id"] = project.project.project_id;
+
+        // project status update equivalent to most recent project note
+        // html is parsed before being rendered in the DashboardStatusModal component
+        project["status_update"] = "";
+        if (project?.project?.moped_proj_notes?.length) {
+          const note = project.project.moped_proj_notes[0]["project_note"];
+          project["status_update"] = note ? note : "";
+        }
+      });
+    }
+    if (data) {
+      setRows(selectedData);
+    }
+  }, [data, activeTab]);
+
+  const handleRowModesModelChange = (newRowModesModel) => {
+    setRowModesModel(newRowModesModel);
+  };
 
   /** Build custom user greeting
    */
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: chiaberry
body: What do y'all think about pulling this custom user greeting code outside of the component definition? its currently being recreated on every render I believe
created_at: 2025-04-10T15:52:12Z
author_association: MEMBER
diff_hunk: @@ -125,47 +211,66 @@ const DashboardView = () => {
     console.log(error);
   }
 
-  let selectedData = [];
+  const [activeTab, setActiveTab] = useState(0);
 
-  if (TABS[activeTab].label === "Following" && !!data) {
-    selectedData = data.moped_user_followed_projects;
-  } else if (TABS[activeTab].label === "My projects" && !!data) {
-    selectedData = data.moped_proj_personnel;
-  }
+  // rows and rowModesModel used in DataGrid
+  const [rows, setRows] = useState([]);
+  const [rowModesModel, setRowModesModel] = useState({});
 
-  if (selectedData) {
-    /**
-     * Build data needed in Dashboard Material Table
-     */
-    selectedData.forEach((project) => {
-      project["project_name_full"] = project.project.project_name_full;
-      project["project_id"] = project.project.project_id;
-      project["phase_name"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
-      project["phase_key"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
-      project["current_phase_id"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+  // sets the data grid row data when query data is fetched
+  useEffect(() => {
+    let selectedData = [];
+
+    if (TABS[activeTab].label === "Following" && !!data) {
+      selectedData = data.moped_user_followed_projects;
+    } else if (TABS[activeTab].label === "My projects" && !!data) {
+      selectedData = data.moped_proj_personnel;
+    }
+
+    if (selectedData) {
       /**
-       * Get percentage of milestones completed
+       * Build data needed in Dashboard Material Table
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: chiaberry
body: Is there a way to memoize this, or just build once and not every time a tab is clicked? When I use prod data locally, moped thinks I'm Ivonne and as a result I have a ton of records in the My Projects tab. Clicking back and forth between the tabs, it feels a tiny bit sluggish. 

created_at: 2025-04-10T16:03:50Z
author_association: MEMBER
diff_hunk: @@ -125,47 +211,66 @@ const DashboardView = () => {
     console.log(error);
   }
 
-  let selectedData = [];
+  const [activeTab, setActiveTab] = useState(0);
 
-  if (TABS[activeTab].label === "Following" && !!data) {
-    selectedData = data.moped_user_followed_projects;
-  } else if (TABS[activeTab].label === "My projects" && !!data) {
-    selectedData = data.moped_proj_personnel;
-  }
+  // rows and rowModesModel used in DataGrid
+  const [rows, setRows] = useState([]);
+  const [rowModesModel, setRowModesModel] = useState({});
 
-  if (selectedData) {
-    /**
-     * Build data needed in Dashboard Material Table
-     */
-    selectedData.forEach((project) => {
-      project["project_name_full"] = project.project.project_name_full;
-      project["project_id"] = project.project.project_id;
-      project["phase_name"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
-      project["phase_key"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
-      project["current_phase_id"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+  // sets the data grid row data when query data is fetched
+  useEffect(() => {
+    let selectedData = [];
+
+    if (TABS[activeTab].label === "Following" && !!data) {
+      selectedData = data.moped_user_followed_projects;
+    } else if (TABS[activeTab].label === "My projects" && !!data) {
+      selectedData = data.moped_proj_personnel;
+    }
+
+    if (selectedData) {
       /**
-       * Get percentage of milestones completed
+       * Build data needed in Dashboard Material Table
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: tillyw
body: this is definitely doable and worth it in my opinion! i'll think about how to do this as simply as possible, although if it's more complex it might be best to spin it off into a separate issue as an enhancement for the subsequent release
created_at: 2025-04-10T17:11:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,47 +211,66 @@ const DashboardView = () => {
     console.log(error);
   }
 
-  let selectedData = [];
+  const [activeTab, setActiveTab] = useState(0);
 
-  if (TABS[activeTab].label === "Following" && !!data) {
-    selectedData = data.moped_user_followed_projects;
-  } else if (TABS[activeTab].label === "My projects" && !!data) {
-    selectedData = data.moped_proj_personnel;
-  }
+  // rows and rowModesModel used in DataGrid
+  const [rows, setRows] = useState([]);
+  const [rowModesModel, setRowModesModel] = useState({});
 
-  if (selectedData) {
-    /**
-     * Build data needed in Dashboard Material Table
-     */
-    selectedData.forEach((project) => {
-      project["project_name_full"] = project.project.project_name_full;
-      project["project_id"] = project.project.project_id;
-      project["phase_name"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
-      project["phase_key"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
-      project["current_phase_id"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+  // sets the data grid row data when query data is fetched
+  useEffect(() => {
+    let selectedData = [];
+
+    if (TABS[activeTab].label === "Following" && !!data) {
+      selectedData = data.moped_user_followed_projects;
+    } else if (TABS[activeTab].label === "My projects" && !!data) {
+      selectedData = data.moped_proj_personnel;
+    }
+
+    if (selectedData) {
       /**
-       * Get percentage of milestones completed
+       * Build data needed in Dashboard Material Table
        */
-      const milestonesTotal = project.project.moped_proj_milestones.length;
-      const milestonesCompleted = project.project.moped_proj_milestones.filter(
-        (milestone) => milestone.completed === true
-      ).length;
-      project["completed_milestones_percentage"] = !!milestonesTotal
-        ? (milestonesCompleted / milestonesTotal) * 100
-        : 0;
+      selectedData.forEach((project) => {
+        project["project_name_full"] = project.project.project_name_full;
+        project["project_id"] = project.project.project_id;
+        project["phase_name"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
+        project["phase_key"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
+        project["current_phase_id"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+        /**
+         * Get percentage of milestones completed
+         */
+        const milestonesTotal = project.project.moped_proj_milestones.length;
+        const milestonesCompleted =
+          project.project.moped_proj_milestones.filter(
+            (milestone) => milestone.completed === true
+          ).length;
+        project["completed_milestones_percentage"] = !!milestonesTotal
+          ? (milestonesCompleted / milestonesTotal) * 100
+          : 0;
 
-      // project status update equivalent to most recent project note
-      // html is parsed before being rendered in the DashboardStatusModal component
-      project["status_update"] = "";
-      if (project?.project?.moped_proj_notes?.length) {
-        const note = project.project.moped_proj_notes[0]["project_note"];
-        project["status_update"] = note ? note : "";
-      }
-    });
-  }
+        project["id"] = project.project.project_id;
+
+        // project status update equivalent to most recent project note
+        // html is parsed before being rendered in the DashboardStatusModal component
+        project["status_update"] = "";
+        if (project?.project?.moped_proj_notes?.length) {
+          const note = project.project.moped_proj_notes[0]["project_note"];
+          project["status_update"] = note ? note : "";
+        }
+      });
+    }
+    if (data) {
+      setRows(selectedData);
+    }
+  }, [data, activeTab]);
+
+  const handleRowModesModelChange = (newRowModesModel) => {
+    setRowModesModel(newRowModesModel);
+  };
 
   /** Build custom user greeting
    */
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: tillyw
body: i think this is a great idea!
created_at: 2025-04-10T17:13:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,47 +211,66 @@ const DashboardView = () => {
     console.log(error);
   }
 
-  let selectedData = [];
+  const [activeTab, setActiveTab] = useState(0);
 
-  if (TABS[activeTab].label === "Following" && !!data) {
-    selectedData = data.moped_user_followed_projects;
-  } else if (TABS[activeTab].label === "My projects" && !!data) {
-    selectedData = data.moped_proj_personnel;
-  }
+  // rows and rowModesModel used in DataGrid
+  const [rows, setRows] = useState([]);
+  const [rowModesModel, setRowModesModel] = useState({});
 
-  if (selectedData) {
-    /**
-     * Build data needed in Dashboard Material Table
-     */
-    selectedData.forEach((project) => {
-      project["project_name_full"] = project.project.project_name_full;
-      project["project_id"] = project.project.project_id;
-      project["phase_name"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
-      project["phase_key"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
-      project["current_phase_id"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+  // sets the data grid row data when query data is fetched
+  useEffect(() => {
+    let selectedData = [];
+
+    if (TABS[activeTab].label === "Following" && !!data) {
+      selectedData = data.moped_user_followed_projects;
+    } else if (TABS[activeTab].label === "My projects" && !!data) {
+      selectedData = data.moped_proj_personnel;
+    }
+
+    if (selectedData) {
       /**
-       * Get percentage of milestones completed
+       * Build data needed in Dashboard Material Table
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: chiaberry
body: I was thinking about this, and how its not a regression since its currently behaving like this in prod. this is an improvement because of the useEffect, so spinoff makes sense so we arent rushed
created_at: 2025-04-10T18:15:04Z
author_association: MEMBER
diff_hunk: @@ -100,18 +99,105 @@ const TABS = [
   },
 ];
 
+/** Hook that provides memoized column settings */
+const useColumns = ({refetch, handleSnackbar, classes}) =>
+  useMemo(() => {
+    return [
+      {
+        headerName: "ID",
+        field: "project_id",
+        editable: false,
+        flex: 0.5,
+      },
+      {
+        headerName: "Full name",
+        field: "project_name_full",
+        editable: false,
+        renderCell: ({ row }) => (
+          <Link
+            href={`/moped/projects/${row.id}`}
+            sx={{
+              overflow: "hidden",
+              textOverflow: "ellipsis",
+              display: "block",
+            }}
+          >
+            {row.project_name_full}
+          </Link>
+        ),
+        flex: 4,
+      },
+      {
+        headerName: "Status",
+        field: "phase_name",
+        editable: false,
+        renderCell: ({ row }) => (
+          <DashboardTimelineModal
+            table="phases"
+            projectId={row.project_id}
+            projectName={row.project.project_name_full}
+            dashboardRefetch={refetch}
+            handleSnackbar={handleSnackbar}
+          >
+            <ProjectStatusBadge
+              phaseName={row.phase_name}
+              phaseKey={row.phase_key}
+              condensed
+              clickable
+            />
+          </DashboardTimelineModal>
+        ),
+        flex: 2,
+      },
+      {
+        headerName: "Status update",
+        field: "status_update", // Status update (from Project details page)
+        editable: false,
+        renderCell: ({ row }) => (
+          <DashboardStatusModal
+            projectId={row.project_id}
+            projectName={row.project_name_full}
+            currentPhaseId={row.current_phase_id}
+            modalParent="dashboard"
+            statusUpdate={row.status_update}
+            queryRefetch={refetch}
+            handleSnackbar={handleSnackbar}
+            classes={classes}
+          >
+            {parse(String(row.status_update))}
+          </DashboardStatusModal>
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: frankhereford
body: I'm not sure if you want to consider this `DashboardStatusModal` component in scope or not, but there are couple of small layout things that maybe should be worked on if it is. 

* If the status is longer than two lines, there is not a clear indication that there's more content that's hidden. If this state can be detected, then adding an elipsese may make that more obvious -- or of course, the expanding the rows height to include all the content. 
* The `⊕` character is rendering at the very top of the row and may benefit from being vertically aligned in the center.
* Similar for the Milestone gauge -- it is also touching the top of the row.

![image](https://github.com/user-attachments/assets/364ba819-894b-4073-afc6-a4870ded3f19)

In this picture, the bulleted list of cat colors / patterns has a 3rd and 4th entry.
created_at: 2025-04-10T18:44:58Z
author_association: MEMBER
diff_hunk: @@ -100,18 +99,105 @@ const TABS = [
   },
 ];
 
+/** Hook that provides memoized column settings */
+const useColumns = ({refetch, handleSnackbar, classes}) =>
+  useMemo(() => {
+    return [
+      {
+        headerName: "ID",
+        field: "project_id",
+        editable: false,
+        flex: 0.5,
+      },
+      {
+        headerName: "Full name",
+        field: "project_name_full",
+        editable: false,
+        renderCell: ({ row }) => (
+          <Link
+            href={`/moped/projects/${row.id}`}
+            sx={{
+              overflow: "hidden",
+              textOverflow: "ellipsis",
+              display: "block",
+            }}
+          >
+            {row.project_name_full}
+          </Link>
+        ),
+        flex: 4,
+      },
+      {
+        headerName: "Status",
+        field: "phase_name",
+        editable: false,
+        renderCell: ({ row }) => (
+          <DashboardTimelineModal
+            table="phases"
+            projectId={row.project_id}
+            projectName={row.project.project_name_full}
+            dashboardRefetch={refetch}
+            handleSnackbar={handleSnackbar}
+          >
+            <ProjectStatusBadge
+              phaseName={row.phase_name}
+              phaseKey={row.phase_key}
+              condensed
+              clickable
+            />
+          </DashboardTimelineModal>
+        ),
+        flex: 2,
+      },
+      {
+        headerName: "Status update",
+        field: "status_update", // Status update (from Project details page)
+        editable: false,
+        renderCell: ({ row }) => (
+          <DashboardStatusModal
+            projectId={row.project_id}
+            projectName={row.project_name_full}
+            currentPhaseId={row.current_phase_id}
+            modalParent="dashboard"
+            statusUpdate={row.status_update}
+            queryRefetch={refetch}
+            handleSnackbar={handleSnackbar}
+            classes={classes}
+          >
+            {parse(String(row.status_update))}
+          </DashboardStatusModal>
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: tillyw
body: i think these would be valuable improvements! in the interest of shipping this update, i'm going to add these to the follow-up issue @chiaberry and i discussed!
created_at: 2025-04-11T02:53:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,18 +103,131 @@ const TABS = [
   },
 ];
 
-const DashboardView = () => {
-  const userSessionData = getSessionDatabaseData();
-  const userId = userSessionData?.user_id;
-  const userName = userSessionData?.first_name;
-
-  const classes = useStyles();
-  const typographyStyle = {
-    fontFamily: typography.fontFamily,
-    fontSize: "14px",
+const getUserGreeting = ({ classes }) => {
+  /** Build custom user greeting
+   */
+  const date = new Date();
+  const curHr = format(date, "HH");
+  const dateFormatted = format(date, "EEEE - LLLL dd, yyyy");
+  const getTimeOfDay = (curHr) => {
+    switch (true) {
+      case curHr < 12:
+        return "morning";
+      case curHr >= 12 && curHr < 18:
+        return "afternoon";
+      default:
+        return "evening";
+    }
   };
 
-  const [activeTab, setActiveTab] = useState(0);
+  return (
+    <Grid className={classes.greeting}>
+      {getUserGreeting}
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: johnclary
body: @tillyw i'm just starting the review but wanted to flag this now. this `getUserGreeting ` looks like it may have been unintentional and is throwing a linter error
created_at: 2025-04-14T14:44:33Z
author_association: MEMBER
diff_hunk: @@ -28,15 +27,19 @@ import FeedbackSnackbar, {
   useFeedbackSnackbar,
 } from "src/components/FeedbackSnackbar";
 import UserSavedViewsTable from "./UserSavedViewsTable";
-
-import typography from "../../theme/typography";
+import { DataGridPro } from "@mui/x-data-grid-pro";
+import dataGridProStyleOverrides from "src/styles/dataGridProStylesOverrides";
 
 import { DASHBOARD_QUERY } from "../../queries/dashboard";
 
 import { getSessionDatabaseData } from "../../auth/user";
 
 import parse from "html-react-parser";
 
+const userSessionData = getSessionDatabaseData();
+const userId = userSessionData?.user_id;
+const userName = userSessionData?.first_name;
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: johnclary
body: i'm not familiar with the Moped auth helpers, but i was surprised to see this working outside of a component's context. just wanted to make sure we're confident this will always work as expected.  e.g. maybe signing in/out as different user could break this?
created_at: 2025-04-14T14:50:26Z
author_association: MEMBER
diff_hunk: @@ -100,18 +103,131 @@ const TABS = [
   },
 ];
 
-const DashboardView = () => {
-  const userSessionData = getSessionDatabaseData();
-  const userId = userSessionData?.user_id;
-  const userName = userSessionData?.first_name;
-
-  const classes = useStyles();
-  const typographyStyle = {
-    fontFamily: typography.fontFamily,
-    fontSize: "14px",
+const getUserGreeting = ({ classes }) => {
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: johnclary
body: Since we're using this as a JSX component, it should be title case, e.g. `<UserGreeting>`
created_at: 2025-04-14T14:51:44Z
author_association: MEMBER
diff_hunk: @@ -100,18 +103,131 @@ const TABS = [
   },
 ];
 
-const DashboardView = () => {
-  const userSessionData = getSessionDatabaseData();
-  const userId = userSessionData?.user_id;
-  const userName = userSessionData?.first_name;
-
-  const classes = useStyles();
-  const typographyStyle = {
-    fontFamily: typography.fontFamily,
-    fontSize: "14px",
+const getUserGreeting = ({ classes }) => {
+  /** Build custom user greeting
+   */
+  const date = new Date();
+  const curHr = format(date, "HH");
+  const dateFormatted = format(date, "EEEE - LLLL dd, yyyy");
+  const getTimeOfDay = (curHr) => {
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: johnclary
body: this looks like a helper function that could live with wherever y'all are keeping other date/time methods for the app. 
created_at: 2025-04-14T14:52:30Z
author_association: MEMBER
diff_hunk: @@ -125,47 +211,66 @@ const DashboardView = () => {
     console.log(error);
   }
 
-  let selectedData = [];
+  const [activeTab, setActiveTab] = useState(0);
 
-  if (TABS[activeTab].label === "Following" && !!data) {
-    selectedData = data.moped_user_followed_projects;
-  } else if (TABS[activeTab].label === "My projects" && !!data) {
-    selectedData = data.moped_proj_personnel;
-  }
+  // rows and rowModesModel used in DataGrid
+  const [rows, setRows] = useState([]);
+  const [rowModesModel, setRowModesModel] = useState({});
 
-  if (selectedData) {
-    /**
-     * Build data needed in Dashboard Material Table
-     */
-    selectedData.forEach((project) => {
-      project["project_name_full"] = project.project.project_name_full;
-      project["project_id"] = project.project.project_id;
-      project["phase_name"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
-      project["phase_key"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
-      project["current_phase_id"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+  // sets the data grid row data when query data is fetched
+  useEffect(() => {
+    let selectedData = [];
+
+    if (TABS[activeTab].label === "Following" && !!data) {
+      selectedData = data.moped_user_followed_projects;
+    } else if (TABS[activeTab].label === "My projects" && !!data) {
+      selectedData = data.moped_proj_personnel;
+    }
+
+    if (selectedData) {
       /**
-       * Get percentage of milestones completed
+       * Build data needed in Dashboard Material Table
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: johnclary
body: agreed—it'd be nice to clean up this code, esp if this is causing performance issues. since this is purely a tech debt PR on this component i'd vote for just taking care of it here and now 👍 
created_at: 2025-04-14T14:59:42Z
author_association: MEMBER
diff_hunk: @@ -125,170 +241,89 @@ const DashboardView = () => {
     console.log(error);
   }
 
-  let selectedData = [];
+  const [activeTab, setActiveTab] = useState(0);
 
-  if (TABS[activeTab].label === "Following" && !!data) {
-    selectedData = data.moped_user_followed_projects;
-  } else if (TABS[activeTab].label === "My projects" && !!data) {
-    selectedData = data.moped_proj_personnel;
-  }
+  // rows and rowModesModel used in DataGrid
+  const [rows, setRows] = useState([]);
+  const [rowModesModel, setRowModesModel] = useState({});
+
+  // sets the data grid row data when query data is fetched
+  useEffect(() => {
+    let selectedData = [];
 
-  if (selectedData) {
-    /**
-     * Build data needed in Dashboard Material Table
-     */
-    selectedData.forEach((project) => {
-      project["project_name_full"] = project.project.project_name_full;
-      project["project_id"] = project.project.project_id;
-      project["phase_name"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
-      project["phase_key"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
-      project["current_phase_id"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+    if (TABS[activeTab].label === "Following" && !!data) {
+      selectedData = data.moped_user_followed_projects;
+    } else if (TABS[activeTab].label === "My projects" && !!data) {
+      selectedData = data.moped_proj_personnel;
+    }
+
+    if (selectedData) {
       /**
-       * Get percentage of milestones completed
+       * Build data needed in Dashboard Material Table
        */
-      const milestonesTotal = project.project.moped_proj_milestones.length;
-      const milestonesCompleted = project.project.moped_proj_milestones.filter(
-        (milestone) => milestone.completed === true
-      ).length;
-      project["completed_milestones_percentage"] = !!milestonesTotal
-        ? (milestonesCompleted / milestonesTotal) * 100
-        : 0;
+      selectedData.forEach((project) => {
+        project["project_name_full"] = project.project.project_name_full;
+        project["project_id"] = project.project.project_id;
+        project["phase_name"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
+        project["phase_key"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
+        project["current_phase_id"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+        /**
+         * Get percentage of milestones completed
+         */
+        const milestonesTotal = project.project.moped_proj_milestones.length;
+        const milestonesCompleted =
+          project.project.moped_proj_milestones.filter(
+            (milestone) => milestone.completed === true
+          ).length;
+        project["completed_milestones_percentage"] = !!milestonesTotal
+          ? (milestonesCompleted / milestonesTotal) * 100
+          : 0;
 
-      // project status update equivalent to most recent project note
-      // html is parsed before being rendered in the DashboardStatusModal component
-      project["status_update"] = "";
-      if (project?.project?.moped_proj_notes?.length) {
-        const note = project.project.moped_proj_notes[0]["project_note"];
-        project["status_update"] = note ? note : "";
-      }
-    });
-  }
+        project["id"] = project.project.project_id;
 
-  /** Build custom user greeting
-   */
-  const date = new Date();
-  const curHr = format(date, "HH");
-  const dateFormatted = format(date, "EEEE - LLLL dd, yyyy");
-  const getTimeOfDay = (curHr) => {
-    switch (true) {
-      case curHr < 12:
-        return "morning";
-      case curHr >= 12 && curHr < 18:
-        return "afternoon";
-      default:
-        return "evening";
+        // project status update equivalent to most recent project note
+        // html is parsed before being rendered in the DashboardStatusModal component
+        project["status_update"] = "";
+        if (project?.project?.moped_proj_notes?.length) {
+          const note = project.project.moped_proj_notes[0]["project_note"];
+          project["status_update"] = note ? note : "";
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: johnclary
body: i'm struggling a bit with this block. is note ever null/undefined, and why is an empty string preferred?
created_at: 2025-04-14T15:02:32Z
author_association: MEMBER
diff_hunk: @@ -100,18 +103,131 @@ const TABS = [
   },
 ];
 
-const DashboardView = () => {
-  const userSessionData = getSessionDatabaseData();
-  const userId = userSessionData?.user_id;
-  const userName = userSessionData?.first_name;
-
-  const classes = useStyles();
-  const typographyStyle = {
-    fontFamily: typography.fontFamily,
-    fontSize: "14px",
+const getUserGreeting = ({ classes }) => {
+  /** Build custom user greeting
+   */
+  const date = new Date();
+  const curHr = format(date, "HH");
+  const dateFormatted = format(date, "EEEE - LLLL dd, yyyy");
+  const getTimeOfDay = (curHr) => {
+    switch (true) {
+      case curHr < 12:
+        return "morning";
+      case curHr >= 12 && curHr < 18:
+        return "afternoon";
+      default:
+        return "evening";
+    }
   };
 
-  const [activeTab, setActiveTab] = useState(0);
+  return (
+    <Grid className={classes.greeting}>
+      {getUserGreeting}
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: tillyw
body: you're right, i meant to delete that line. thanks for catching that!
created_at: 2025-04-14T15:55:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,15 +27,19 @@ import FeedbackSnackbar, {
   useFeedbackSnackbar,
 } from "src/components/FeedbackSnackbar";
 import UserSavedViewsTable from "./UserSavedViewsTable";
-
-import typography from "../../theme/typography";
+import { DataGridPro } from "@mui/x-data-grid-pro";
+import dataGridProStyleOverrides from "src/styles/dataGridProStylesOverrides";
 
 import { DASHBOARD_QUERY } from "../../queries/dashboard";
 
 import { getSessionDatabaseData } from "../../auth/user";
 
 import parse from "html-react-parser";
 
+const userSessionData = getSessionDatabaseData();
+const userId = userSessionData?.user_id;
+const userName = userSessionData?.first_name;
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: tillyw
body: good call, i'm moving this back into the component just to be safe!
created_at: 2025-04-14T20:50:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,170 +241,89 @@ const DashboardView = () => {
     console.log(error);
   }
 
-  let selectedData = [];
+  const [activeTab, setActiveTab] = useState(0);
 
-  if (TABS[activeTab].label === "Following" && !!data) {
-    selectedData = data.moped_user_followed_projects;
-  } else if (TABS[activeTab].label === "My projects" && !!data) {
-    selectedData = data.moped_proj_personnel;
-  }
+  // rows and rowModesModel used in DataGrid
+  const [rows, setRows] = useState([]);
+  const [rowModesModel, setRowModesModel] = useState({});
+
+  // sets the data grid row data when query data is fetched
+  useEffect(() => {
+    let selectedData = [];
 
-  if (selectedData) {
-    /**
-     * Build data needed in Dashboard Material Table
-     */
-    selectedData.forEach((project) => {
-      project["project_name_full"] = project.project.project_name_full;
-      project["project_id"] = project.project.project_id;
-      project["phase_name"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
-      project["phase_key"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
-      project["current_phase_id"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+    if (TABS[activeTab].label === "Following" && !!data) {
+      selectedData = data.moped_user_followed_projects;
+    } else if (TABS[activeTab].label === "My projects" && !!data) {
+      selectedData = data.moped_proj_personnel;
+    }
+
+    if (selectedData) {
       /**
-       * Get percentage of milestones completed
+       * Build data needed in Dashboard Material Table
        */
-      const milestonesTotal = project.project.moped_proj_milestones.length;
-      const milestonesCompleted = project.project.moped_proj_milestones.filter(
-        (milestone) => milestone.completed === true
-      ).length;
-      project["completed_milestones_percentage"] = !!milestonesTotal
-        ? (milestonesCompleted / milestonesTotal) * 100
-        : 0;
+      selectedData.forEach((project) => {
+        project["project_name_full"] = project.project.project_name_full;
+        project["project_id"] = project.project.project_id;
+        project["phase_name"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
+        project["phase_key"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
+        project["current_phase_id"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+        /**
+         * Get percentage of milestones completed
+         */
+        const milestonesTotal = project.project.moped_proj_milestones.length;
+        const milestonesCompleted =
+          project.project.moped_proj_milestones.filter(
+            (milestone) => milestone.completed === true
+          ).length;
+        project["completed_milestones_percentage"] = !!milestonesTotal
+          ? (milestonesCompleted / milestonesTotal) * 100
+          : 0;
 
-      // project status update equivalent to most recent project note
-      // html is parsed before being rendered in the DashboardStatusModal component
-      project["status_update"] = "";
-      if (project?.project?.moped_proj_notes?.length) {
-        const note = project.project.moped_proj_notes[0]["project_note"];
-        project["status_update"] = note ? note : "";
-      }
-    });
-  }
+        project["id"] = project.project.project_id;
 
-  /** Build custom user greeting
-   */
-  const date = new Date();
-  const curHr = format(date, "HH");
-  const dateFormatted = format(date, "EEEE - LLLL dd, yyyy");
-  const getTimeOfDay = (curHr) => {
-    switch (true) {
-      case curHr < 12:
-        return "morning";
-      case curHr >= 12 && curHr < 18:
-        return "afternoon";
-      default:
-        return "evening";
+        // project status update equivalent to most recent project note
+        // html is parsed before being rendered in the DashboardStatusModal component
+        project["status_update"] = "";
+        if (project?.project?.moped_proj_notes?.length) {
+          const note = project.project.moped_proj_notes[0]["project_note"];
+          project["status_update"] = note ? note : "";
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: tillyw
body: double-checking this, this is logic from when i originally wrote this component so it could use a reexamination
created_at: 2025-04-14T21:55:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,170 +241,89 @@ const DashboardView = () => {
     console.log(error);
   }
 
-  let selectedData = [];
+  const [activeTab, setActiveTab] = useState(0);
 
-  if (TABS[activeTab].label === "Following" && !!data) {
-    selectedData = data.moped_user_followed_projects;
-  } else if (TABS[activeTab].label === "My projects" && !!data) {
-    selectedData = data.moped_proj_personnel;
-  }
+  // rows and rowModesModel used in DataGrid
+  const [rows, setRows] = useState([]);
+  const [rowModesModel, setRowModesModel] = useState({});
+
+  // sets the data grid row data when query data is fetched
+  useEffect(() => {
+    let selectedData = [];
 
-  if (selectedData) {
-    /**
-     * Build data needed in Dashboard Material Table
-     */
-    selectedData.forEach((project) => {
-      project["project_name_full"] = project.project.project_name_full;
-      project["project_id"] = project.project.project_id;
-      project["phase_name"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
-      project["phase_key"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
-      project["current_phase_id"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+    if (TABS[activeTab].label === "Following" && !!data) {
+      selectedData = data.moped_user_followed_projects;
+    } else if (TABS[activeTab].label === "My projects" && !!data) {
+      selectedData = data.moped_proj_personnel;
+    }
+
+    if (selectedData) {
       /**
-       * Get percentage of milestones completed
+       * Build data needed in Dashboard Material Table
        */
-      const milestonesTotal = project.project.moped_proj_milestones.length;
-      const milestonesCompleted = project.project.moped_proj_milestones.filter(
-        (milestone) => milestone.completed === true
-      ).length;
-      project["completed_milestones_percentage"] = !!milestonesTotal
-        ? (milestonesCompleted / milestonesTotal) * 100
-        : 0;
+      selectedData.forEach((project) => {
+        project["project_name_full"] = project.project.project_name_full;
+        project["project_id"] = project.project.project_id;
+        project["phase_name"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
+        project["phase_key"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
+        project["current_phase_id"] =
+          project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+        /**
+         * Get percentage of milestones completed
+         */
+        const milestonesTotal = project.project.moped_proj_milestones.length;
+        const milestonesCompleted =
+          project.project.moped_proj_milestones.filter(
+            (milestone) => milestone.completed === true
+          ).length;
+        project["completed_milestones_percentage"] = !!milestonesTotal
+          ? (milestonesCompleted / milestonesTotal) * 100
+          : 0;
 
-      // project status update equivalent to most recent project note
-      // html is parsed before being rendered in the DashboardStatusModal component
-      project["status_update"] = "";
-      if (project?.project?.moped_proj_notes?.length) {
-        const note = project.project.moped_proj_notes[0]["project_note"];
-        project["status_update"] = note ? note : "";
-      }
-    });
-  }
+        project["id"] = project.project.project_id;
 
-  /** Build custom user greeting
-   */
-  const date = new Date();
-  const curHr = format(date, "HH");
-  const dateFormatted = format(date, "EEEE - LLLL dd, yyyy");
-  const getTimeOfDay = (curHr) => {
-    switch (true) {
-      case curHr < 12:
-        return "morning";
-      case curHr >= 12 && curHr < 18:
-        return "afternoon";
-      default:
-        return "evening";
+        // project status update equivalent to most recent project note
+        // html is parsed before being rendered in the DashboardStatusModal component
+        project["status_update"] = "";
+        if (project?.project?.moped_proj_notes?.length) {
+          const note = project.project.moped_proj_notes[0]["project_note"];
+          project["status_update"] = note ? note : "";
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: tillyw
body: i reexamined this data structuring function and was able to completely get rid of it by simply defining the proper field values in the ``useColumns`` function ✨ 
created_at: 2025-04-16T19:51:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,18 +99,105 @@ const TABS = [
   },
 ];
 
+/** Hook that provides memoized column settings */
+const useColumns = ({refetch, handleSnackbar, classes}) =>
+  useMemo(() => {
+    return [
+      {
+        headerName: "ID",
+        field: "project_id",
+        editable: false,
+        flex: 0.5,
+      },
+      {
+        headerName: "Full name",
+        field: "project_name_full",
+        editable: false,
+        renderCell: ({ row }) => (
+          <Link
+            href={`/moped/projects/${row.id}`}
+            sx={{
+              overflow: "hidden",
+              textOverflow: "ellipsis",
+              display: "block",
+            }}
+          >
+            {row.project_name_full}
+          </Link>
+        ),
+        flex: 4,
+      },
+      {
+        headerName: "Status",
+        field: "phase_name",
+        editable: false,
+        renderCell: ({ row }) => (
+          <DashboardTimelineModal
+            table="phases"
+            projectId={row.project_id}
+            projectName={row.project.project_name_full}
+            dashboardRefetch={refetch}
+            handleSnackbar={handleSnackbar}
+          >
+            <ProjectStatusBadge
+              phaseName={row.phase_name}
+              phaseKey={row.phase_key}
+              condensed
+              clickable
+            />
+          </DashboardTimelineModal>
+        ),
+        flex: 2,
+      },
+      {
+        headerName: "Status update",
+        field: "status_update", // Status update (from Project details page)
+        editable: false,
+        renderCell: ({ row }) => (
+          <DashboardStatusModal
+            projectId={row.project_id}
+            projectName={row.project_name_full}
+            currentPhaseId={row.current_phase_id}
+            modalParent="dashboard"
+            statusUpdate={row.status_update}
+            queryRefetch={refetch}
+            handleSnackbar={handleSnackbar}
+            classes={classes}
+          >
+            {parse(String(row.status_update))}
+          </DashboardStatusModal>
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: mddilley
body: Tilly and I chatted about this one, and Tilly is going to vertically center the icon and milestone progress meter. Thanks @frankhereford! Nice detail to catch 🙏
created_at: 2025-04-17T17:22:17Z
author_association: COLLABORATOR
diff_hunk: @@ -125,47 +211,66 @@ const DashboardView = () => {
     console.log(error);
   }
 
-  let selectedData = [];
+  const [activeTab, setActiveTab] = useState(0);
 
-  if (TABS[activeTab].label === "Following" && !!data) {
-    selectedData = data.moped_user_followed_projects;
-  } else if (TABS[activeTab].label === "My projects" && !!data) {
-    selectedData = data.moped_proj_personnel;
-  }
+  // rows and rowModesModel used in DataGrid
+  const [rows, setRows] = useState([]);
+  const [rowModesModel, setRowModesModel] = useState({});
 
-  if (selectedData) {
-    /**
-     * Build data needed in Dashboard Material Table
-     */
-    selectedData.forEach((project) => {
-      project["project_name_full"] = project.project.project_name_full;
-      project["project_id"] = project.project.project_id;
-      project["phase_name"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
-      project["phase_key"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
-      project["current_phase_id"] =
-        project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
+  // sets the data grid row data when query data is fetched
+  useEffect(() => {
+    let selectedData = [];
+
+    if (TABS[activeTab].label === "Following" && !!data) {
+      selectedData = data.moped_user_followed_projects;
+    } else if (TABS[activeTab].label === "My projects" && !!data) {
+      selectedData = data.moped_proj_personnel;
+    }
+
+    if (selectedData) {
       /**
-       * Get percentage of milestones completed
+       * Build data needed in Dashboard Material Table
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: mddilley
body: Looks great, Tilly! I was looking for the previous chunk of code that was changing the shape of the project data, and I then I realized that it is just gone and no longer needed. So good! 🧹🚀
created_at: 2025-04-17T17:45:31Z
author_association: COLLABORATOR
diff_hunk: @@ -100,19 +102,142 @@ const TABS = [
   },
 ];
 
+const UserGreeting = ({ classes, userName }) => {
+  const date = new Date();
+
+  /** Build custom user greeting
+   */
+  return (
+    <Grid className={classes.greeting}>
+      <Typography className={classes.greetingText}>
+        <strong>{`Good ${getTimeOfDay(date)}, ${userName}!`}</strong>
+      </Typography>
+      <Typography variant="h1" className={classes.date}>
+        {getCalendarDate(date)}
+      </Typography>
+    </Grid>
+  );
+};
+
+/** Hook that provides memoized column settings */
+const useColumns = ({ refetch, handleSnackbar, classes }) =>
+  useMemo(() => {
+    return [
+      {
+        headerName: "ID",
+        field: "project",
+        valueGetter: (params) => {
+          return params.project_id;
+        },
+        editable: false,
+        flex: 0.5,
+      },
+      {
+        headerName: "Full name",
+        field: "project_name_full",
+        editable: false,
+        renderCell: ({ row }) => (
+          <Link
+            href={`/moped/projects/${row.project.project_id}`}
+            sx={{
+              overflow: "hidden",
+              textOverflow: "ellipsis",
+              display: "block",
+            }}
+          >
+            {row.project.project_name_full}
+          </Link>
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: mddilley
body: There is a small regression here since the MUI Link is using browser redirect instead of client-side navigation with React Router. Could you update the `Link` component to use the `component` and `to` props like in [this project list view column](https://github.com/cityofaustin/atd-moped/blob/c79b0ce8476e2720e985e9ecca521f753851e76f/moped-editor/src/views/projects/projectsListView/helpers.js#L162-L163)? 🙏
created_at: 2025-04-17T18:17:17Z
author_association: COLLABORATOR
diff_hunk: @@ -315,32 +320,22 @@ const DashboardView = () => {
                   ) : TABS[activeTab].label === "Saved views" ? (
                     <UserSavedViewsTable handleSnackbar={handleSnackbar} />
                   ) : (
-                    <MaterialTable
-                      columns={columns}
-                      data={selectedData}
-                      localization={{
-                        body: {
-                          emptyDataSourceMessage: (
-                            <Typography>
-                              {TABS[activeTab].label === "Following" &&
-                                "No projects to display. You have not followed any current projects."}
-                              {TABS[activeTab].label === "My projects" &&
-                                "No projects to display. You are not listed as a Team Member on any current projects."}
-                            </Typography>
-                          ),
-                        },
-                      }}
-                      options={{
-                        search: false,
-                        toolbar: false,
-                        tableLayout: "fixed",
-                        ...(selectedData.length < 51 && {
-                          paging: false,
-                        }),
-                        pageSize: 50,
-                        pageSizeOptions: [10, 50, 100],
-                        idSynonym: "project_id",
+                    <DataGridPro
+                      sx={dataGridProStyleOverrides}
+                      columns={dataGridColumns}
+                      rows={rows}
+                      autoHeight
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: mddilley
body: I just noticed Frank's note about part of the status update being hidden. In the project list view, we use the prop `getRowHeight={() => "auto"}` which i think will handle making the entire update being visible like in production right now. I think the entire status update should show like we do in the project list.

Also, if it becomes to difficult to vertically center items in the rows, I am comfortable with adding padding (theme.spacing(2)) like we have done in the project list as another option.

<img width="402" alt="Screenshot 2025-04-17 at 1 23 39 PM" src="https://github.com/user-attachments/assets/8a0da660-f5e2-4fd6-80b3-9c1c63189dd6" />

created_at: 2025-04-17T18:25:42Z
author_association: COLLABORATOR
diff_hunk: @@ -315,32 +320,22 @@ const DashboardView = () => {
                   ) : TABS[activeTab].label === "Saved views" ? (
                     <UserSavedViewsTable handleSnackbar={handleSnackbar} />
                   ) : (
-                    <MaterialTable
-                      columns={columns}
-                      data={selectedData}
-                      localization={{
-                        body: {
-                          emptyDataSourceMessage: (
-                            <Typography>
-                              {TABS[activeTab].label === "Following" &&
-                                "No projects to display. You have not followed any current projects."}
-                              {TABS[activeTab].label === "My projects" &&
-                                "No projects to display. You are not listed as a Team Member on any current projects."}
-                            </Typography>
-                          ),
-                        },
-                      }}
-                      options={{
-                        search: false,
-                        toolbar: false,
-                        tableLayout: "fixed",
-                        ...(selectedData.length < 51 && {
-                          paging: false,
-                        }),
-                        pageSize: 50,
-                        pageSizeOptions: [10, 50, 100],
-                        idSynonym: "project_id",
+                    <DataGridPro
+                      sx={dataGridProStyleOverrides}
+                      columns={dataGridColumns}
+                      rows={rows}
+                      autoHeight
+                      getRowId={(row) => row.project.project_id}
+                      rowModesModel={rowModesModel}
+                      onRowModesModelChange={handleRowModesModelChange}
+                      onProcessRowUpdateError={(error) => console.error}
+                      editMode="row"
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: mddilley
body: Could you remove these two props since this table doesn't have any editing happening? Thanks!
created_at: 2025-04-17T18:36:48Z
author_association: COLLABORATOR
diff_hunk: @@ -315,32 +320,22 @@ const DashboardView = () => {
                   ) : TABS[activeTab].label === "Saved views" ? (
                     <UserSavedViewsTable handleSnackbar={handleSnackbar} />
                   ) : (
-                    <MaterialTable
-                      columns={columns}
-                      data={selectedData}
-                      localization={{
-                        body: {
-                          emptyDataSourceMessage: (
-                            <Typography>
-                              {TABS[activeTab].label === "Following" &&
-                                "No projects to display. You have not followed any current projects."}
-                              {TABS[activeTab].label === "My projects" &&
-                                "No projects to display. You are not listed as a Team Member on any current projects."}
-                            </Typography>
-                          ),
-                        },
-                      }}
-                      options={{
-                        search: false,
-                        toolbar: false,
-                        tableLayout: "fixed",
-                        ...(selectedData.length < 51 && {
-                          paging: false,
-                        }),
-                        pageSize: 50,
-                        pageSizeOptions: [10, 50, 100],
-                        idSynonym: "project_id",
+                    <DataGridPro
+                      sx={dataGridProStyleOverrides}
+                      columns={dataGridColumns}
+                      rows={rows}
+                      autoHeight
+                      getRowId={(row) => row.project.project_id}
+                      rowModesModel={rowModesModel}
+                      onRowModesModelChange={handleRowModesModelChange}
+                      onProcessRowUpdateError={(error) => console.error}
+                      editMode="row"
+                      hideFooter
+                      disableRowSelectionOnClick
+                      localeText={{
+                        noRowsLabel: "No projects to display",
                       }}
+                      initialState={{ pinnedColumns: { right: ["edit"] } }}
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: mddilley
body: Same with this one, could you remove since there is no edit column to pin? Thank you 🙏
created_at: 2025-04-17T18:37:11Z
author_association: COLLABORATOR
path: moped-editor/src/components/DataGridPro/utils/helpers.js
user: login: mddilley
body: could you move these helpers to [dateAndTime.js](https://github.com/cityofaustin/atd-moped/blob/main/moped-editor/src/utils/dateAndTime.js) in the interest of making them more discoverable in the future? Thanks for pulling these out of the Dashboard component btw!
created_at: 2025-04-17T18:45:13Z
author_association: COLLABORATOR
path: moped-editor/src/components/DataGridPro/utils/helpers.js
user: login: tillyw
body: yes, that is a better place for them, thank you!
created_at: 2025-04-17T21:53:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,19 +102,142 @@ const TABS = [
   },
 ];
 
+const UserGreeting = ({ classes, userName }) => {
+  const date = new Date();
+
+  /** Build custom user greeting
+   */
+  return (
+    <Grid className={classes.greeting}>
+      <Typography className={classes.greetingText}>
+        <strong>{`Good ${getTimeOfDay(date)}, ${userName}!`}</strong>
+      </Typography>
+      <Typography variant="h1" className={classes.date}>
+        {getCalendarDate(date)}
+      </Typography>
+    </Grid>
+  );
+};
+
+/** Hook that provides memoized column settings */
+const useColumns = ({ refetch, handleSnackbar, classes }) =>
+  useMemo(() => {
+    return [
+      {
+        headerName: "ID",
+        field: "project",
+        valueGetter: (params) => {
+          return params.project_id;
+        },
+        editable: false,
+        flex: 0.5,
+      },
+      {
+        headerName: "Full name",
+        field: "project_name_full",
+        editable: false,
+        renderCell: ({ row }) => (
+          <Link
+            href={`/moped/projects/${row.project.project_id}`}
+            sx={{
+              overflow: "hidden",
+              textOverflow: "ellipsis",
+              display: "block",
+            }}
+          >
+            {row.project.project_name_full}
+          </Link>
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: tillyw
body: thanks for pointing that out!
created_at: 2025-04-17T22:07:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,3 +14,25 @@ export const getLookupValueByID = (lookupTable, attribute, id) => {
     `${attribute}_name`
   ];
 };
+
+/**
+ * Get the time of day to display to users on the dashboard
+ * @returns {string} - Time of day phrased as a salutation
+ */
+export const getTimeOfDay = (date) => {
+  const curHr = format(date, "HH");
path: moped-editor/src/components/DataGridPro/utils/helpers.js
user: login: johnclary
body: i believe you can do this with plane ole javascript: `date.getHours()`
created_at: 2025-04-21T15:33:05Z
author_association: MEMBER
diff_hunk: @@ -249,52 +258,13 @@ const DashboardView = () => {
 
   // sets the data grid row data when query data is fetched
   useEffect(() => {
-    let selectedData = [];
-
-    if (TABS[activeTab].label === "Following" && !!data) {
-      selectedData = data.moped_user_followed_projects;
-    } else if (TABS[activeTab].label === "My projects" && !!data) {
-      selectedData = data.moped_proj_personnel;
-    }
-
-    if (selectedData) {
-      /**
-       * Build data needed in Dashboard Material Table
-       */
-      selectedData.forEach((project) => {
-        project["project_name_full"] = project.project.project_name_full;
-        project["project_id"] = project.project.project_id;
-        project["phase_name"] =
-          project.project.moped_proj_phases?.[0]?.moped_phase.phase_name;
-        project["phase_key"] =
-          project.project.moped_proj_phases?.[0]?.moped_phase.phase_key;
-        project["current_phase_id"] =
-          project.project.moped_proj_phases?.[0]?.moped_phase.phase_id;
-        /**
-         * Get percentage of milestones completed
-         */
-        const milestonesTotal = project.project.moped_proj_milestones.length;
-        const milestonesCompleted =
-          project.project.moped_proj_milestones.filter(
-            (milestone) => milestone.completed === true
-          ).length;
-        project["completed_milestones_percentage"] = !!milestonesTotal
-          ? (milestonesCompleted / milestonesTotal) * 100
-          : 0;
-
-        project["id"] = project.project.project_id;
-
-        // project status update equivalent to most recent project note
-        // html is parsed before being rendered in the DashboardStatusModal component
-        project["status_update"] = "";
-        if (project?.project?.moped_proj_notes?.length) {
-          const note = project.project.moped_proj_notes[0]["project_note"];
-          project["status_update"] = note ? note : "";
-        }
-      });
-    }
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: johnclary
body: nice!
created_at: 2025-04-21T15:37:33Z
author_association: MEMBER
diff_hunk: @@ -315,32 +320,22 @@ const DashboardView = () => {
                   ) : TABS[activeTab].label === "Saved views" ? (
                     <UserSavedViewsTable handleSnackbar={handleSnackbar} />
                   ) : (
-                    <MaterialTable
-                      columns={columns}
-                      data={selectedData}
-                      localization={{
-                        body: {
-                          emptyDataSourceMessage: (
-                            <Typography>
-                              {TABS[activeTab].label === "Following" &&
-                                "No projects to display. You have not followed any current projects."}
-                              {TABS[activeTab].label === "My projects" &&
-                                "No projects to display. You are not listed as a Team Member on any current projects."}
-                            </Typography>
-                          ),
-                        },
-                      }}
-                      options={{
-                        search: false,
-                        toolbar: false,
-                        tableLayout: "fixed",
-                        ...(selectedData.length < 51 && {
-                          paging: false,
-                        }),
-                        pageSize: 50,
-                        pageSizeOptions: [10, 50, 100],
-                        idSynonym: "project_id",
+                    <DataGridPro
+                      sx={dataGridProStyleOverrides}
+                      columns={dataGridColumns}
+                      rows={rows}
+                      autoHeight
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: johnclary
body: I assume this is related—just wanted to mention the note text is not vertically centered in the same way as other tables

<img width="1357" alt="Screenshot 2025-04-21 at 11 36 57 AM" src="https://github.com/user-attachments/assets/54002f91-82ed-4fc1-a7fc-b0ed9982a905" />

created_at: 2025-04-21T15:38:54Z
author_association: MEMBER
diff_hunk: @@ -100,19 +112,153 @@ const TABS = [
   },
 ];
 
+const UserGreeting = ({ classes, userName }) => {
+  const date = new Date();
+
+  /** Build custom user greeting
+   */
+  return (
+    <Grid className={classes.greeting}>
+      <Typography className={classes.greetingText}>
+        <strong>{`Good ${getTimeOfDay(date)}, ${userName}!`}</strong>
+      </Typography>
+      <Typography variant="h1" className={classes.date}>
+        {getCalendarDate(date)}
+      </Typography>
+    </Grid>
+  );
+};
+
+/** Hook that provides memoized column settings */
+const useColumns = ({ refetch, handleSnackbar, classes }) =>
+  useMemo(() => {
+    return [
+      {
+        headerName: "ID",
+        field: "project_id",
+        editable: false,
+        renderCell: ({ row }) => (
+          <div className={classes.tableRowDiv}>{row.project.project_id}</div>
+        ),
+        flex: 0.5,
+      },
+      {
+        headerName: "Full name",
+        field: "project_name_full",
+        editable: false,
+        renderCell: ({ row }) => (
+          <div className={classes.tableRowDiv}>
+            <Link
+              component={RouterLink}
+              to={`/moped/projects/${row.project.project_id}`}
+              sx={{
+                overflow: "hidden",
+                textOverflow: "ellipsis",
+                display: "block",
+              }}
+            >
+              {row.project.project_name_full}
+            </Link>
+          </div>
+        ),
+        flex: 4,
+      },
+      {
+        headerName: "Status",
+        field: "phase_name",
+        editable: false,
+        renderCell: ({ row }) => (
+          <div className={classes.tableRowDiv}>
+            <DashboardTimelineModal
+              table="phases"
+              projectId={row.project.project_id}
+              projectName={row.project.project_name_full}
+              dashboardRefetch={refetch}
+              handleSnackbar={handleSnackbar}
+            >
+              <ProjectStatusBadge
+                phaseName={
+                  row.project.moped_proj_phases?.[0]?.moped_phase.phase_name
+                }
+                phaseKey={
+                  row.project.moped_proj_phases?.[0]?.moped_phase.phase_key
+                }
+                condensed
+                clickable
+              />
+            </DashboardTimelineModal>
+          </div>
+        ),
+        flex: 2,
+      },
+      {
+        headerName: "Status update",
+        field: "status_update",
+        editable: false,
+        renderCell: ({ row }) => (
+          // Display status update (from Project details page), i.e., most recent note
+          <div className={classes.tableRowDiv}>
+            <DashboardStatusModal
+              projectId={row.project.project_id}
+              projectName={row.project.project_name_full}
+              currentPhaseId={
+                row.project.moped_proj_phases?.[0]?.moped_phase.phase_id
+              }
+              modalParent="dashboard"
+              statusUpdate={
+                row.project.moped_proj_notes?.[0]?.project_note ?? ""
+              }
+              queryRefetch={refetch}
+              handleSnackbar={handleSnackbar}
+              classes={classes}
+            >
+              {parse(
+                String(row.project.moped_proj_notes?.[0]?.project_note ?? "")
+              )}
+            </DashboardStatusModal>
+          </div>
+        ),
+        flex: 4,
+      },
+      {
+        headerName: "Milestones",
+        field: "completed_milestones_percentage",
+        renderCell: ({ row }) => (
+          // Display percentage of milestone completed, or 0 if no milestones saved
+          <div className={classes.milestoneProgressDiv}>
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: mddilley
body: could you update to use `tableRowDiv` here? that way each row has the same spacing from the top of the row and we'll have one less class?
created_at: 2025-04-22T17:34:01Z
author_association: COLLABORATOR
diff_hunk: @@ -79,6 +79,18 @@ const useStyles = makeStyles((theme) => ({
   tooltipIcon: {
     fontSize: "20px",
   },
+  tableRowDiv: {
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: mddilley
body: love this wrapper class that makes these all level with each other!
created_at: 2025-04-22T17:35:07Z
author_association: COLLABORATOR
diff_hunk: @@ -315,31 +341,18 @@ const DashboardView = () => {
                   ) : TABS[activeTab].label === "Saved views" ? (
                     <UserSavedViewsTable handleSnackbar={handleSnackbar} />
                   ) : (
-                    <MaterialTable
-                      columns={columns}
-                      data={selectedData}
-                      localization={{
-                        body: {
-                          emptyDataSourceMessage: (
-                            <Typography>
-                              {TABS[activeTab].label === "Following" &&
-                                "No projects to display. You have not followed any current projects."}
-                              {TABS[activeTab].label === "My projects" &&
-                                "No projects to display. You are not listed as a Team Member on any current projects."}
-                            </Typography>
-                          ),
-                        },
-                      }}
-                      options={{
-                        search: false,
-                        toolbar: false,
-                        tableLayout: "fixed",
-                        ...(selectedData.length < 51 && {
-                          paging: false,
-                        }),
-                        pageSize: 50,
-                        pageSizeOptions: [10, 50, 100],
-                        idSynonym: "project_id",
+                    <DataGridPro
+                      sx={dataGridProStyleOverrides}
+                      columns={dataGridColumns}
+                      rows={rows}
+                      getRowHeight={() => "auto"}
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: mddilley
body: could you add the `autoHeight` prop too? I noticed that the empty table collapses and hides the "no rows" text.

<img width="818" alt="Screenshot 2025-04-22 at 12 43 08 PM" src="https://github.com/user-attachments/assets/a3cb9a1c-a549-49cc-a738-029b13d8735b" />

created_at: 2025-04-22T17:44:00Z
author_association: COLLABORATOR
diff_hunk: @@ -100,19 +112,153 @@ const TABS = [
   },
 ];
 
+const UserGreeting = ({ classes, userName }) => {
+  const date = new Date();
+
+  /** Build custom user greeting
+   */
+  return (
+    <Grid className={classes.greeting}>
+      <Typography className={classes.greetingText}>
+        <strong>{`Good ${getTimeOfDay(date)}, ${userName}!`}</strong>
+      </Typography>
+      <Typography variant="h1" className={classes.date}>
+        {getCalendarDate(date)}
+      </Typography>
+    </Grid>
+  );
+};
+
+/** Hook that provides memoized column settings */
+const useColumns = ({ refetch, handleSnackbar, classes }) =>
+  useMemo(() => {
+    return [
+      {
+        headerName: "ID",
+        field: "project_id",
+        editable: false,
+        renderCell: ({ row }) => (
+          <div className={classes.tableRowDiv}>{row.project.project_id}</div>
+        ),
+        flex: 0.5,
+      },
+      {
+        headerName: "Full name",
+        field: "project_name_full",
+        editable: false,
+        renderCell: ({ row }) => (
+          <div className={classes.tableRowDiv}>
+            <Link
+              component={RouterLink}
+              to={`/moped/projects/${row.project.project_id}`}
+              sx={{
+                overflow: "hidden",
+                textOverflow: "ellipsis",
+                display: "block",
+              }}
+            >
+              {row.project.project_name_full}
+            </Link>
+          </div>
+        ),
+        flex: 4,
+      },
+      {
+        headerName: "Status",
+        field: "phase_name",
+        editable: false,
+        renderCell: ({ row }) => (
+          <div className={classes.tableRowDiv}>
+            <DashboardTimelineModal
+              table="phases"
+              projectId={row.project.project_id}
+              projectName={row.project.project_name_full}
+              dashboardRefetch={refetch}
+              handleSnackbar={handleSnackbar}
+            >
+              <ProjectStatusBadge
+                phaseName={
+                  row.project.moped_proj_phases?.[0]?.moped_phase.phase_name
+                }
+                phaseKey={
+                  row.project.moped_proj_phases?.[0]?.moped_phase.phase_key
+                }
+                condensed
+                clickable
+              />
+            </DashboardTimelineModal>
+          </div>
+        ),
+        flex: 2,
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: mddilley
body: Love the responsiveness on the text columns! ❤️ For this status one, could you update to use `width` to keep it from overlapping with text? Maybe 200 or something around there.

<img width="546" alt="Screenshot 2025-04-22 at 12 44 50 PM" src="https://github.com/user-attachments/assets/bc6a5700-5b79-482b-a1d0-607c5c375489" />

created_at: 2025-04-22T17:46:32Z
author_association: COLLABORATOR
diff_hunk: @@ -32,7 +31,7 @@ const DashboardStatusModal = ({
 
   return (
     <>
-      <Typography
+      <div
path: moped-editor/src/views/dashboard/DashboardStatusModal.js
user: login: mddilley
body: I noticed removing the `Typography` causes a regression where status updates no longer have the Roboto font? could you change this back or restore the font through the class if this reverting causes other issues? 🙏

w/o Typography:
<img width="224" alt="Screenshot 2025-04-22 at 12 59 26 PM" src="https://github.com/user-attachments/assets/bdbf8800-23ea-4c56-b114-68ea7a720890" />

with Typography:
<img width="224" alt="Screenshot 2025-04-22 at 12 59 58 PM" src="https://github.com/user-attachments/assets/91532f6f-e3ba-4410-984f-eeaa5ac39c8a" />

created_at: 2025-04-22T17:58:56Z
author_association: COLLABORATOR
diff_hunk: @@ -315,32 +320,22 @@ const DashboardView = () => {
                   ) : TABS[activeTab].label === "Saved views" ? (
                     <UserSavedViewsTable handleSnackbar={handleSnackbar} />
                   ) : (
-                    <MaterialTable
-                      columns={columns}
-                      data={selectedData}
-                      localization={{
-                        body: {
-                          emptyDataSourceMessage: (
-                            <Typography>
-                              {TABS[activeTab].label === "Following" &&
-                                "No projects to display. You have not followed any current projects."}
-                              {TABS[activeTab].label === "My projects" &&
-                                "No projects to display. You are not listed as a Team Member on any current projects."}
-                            </Typography>
-                          ),
-                        },
-                      }}
-                      options={{
-                        search: false,
-                        toolbar: false,
-                        tableLayout: "fixed",
-                        ...(selectedData.length < 51 && {
-                          paging: false,
-                        }),
-                        pageSize: 50,
-                        pageSizeOptions: [10, 50, 100],
-                        idSynonym: "project_id",
+                    <DataGridPro
+                      sx={dataGridProStyleOverrides}
+                      columns={dataGridColumns}
+                      rows={rows}
+                      autoHeight
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: tillyw
body: this is because i didn't push my final formatting updates, sorry about that!
created_at: 2025-04-22T18:34:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,7 +31,7 @@ const DashboardStatusModal = ({
 
   return (
     <>
-      <Typography
+      <div
path: moped-editor/src/views/dashboard/DashboardStatusModal.js
user: login: tillyw
body: i removed it to get the font to match the project name font on the dashboard but i didn't consider how it would affect the project view! i've opted to increase the project name font size on the dashboard instead
created_at: 2025-04-22T19:13:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,19 +106,154 @@ const TABS = [
   },
 ];
 
+const UserGreeting = ({ classes, userName }) => {
+  const date = new Date();
+
+  /** Build custom user greeting
+   */
+  return (
+    <Grid className={classes.greeting}>
+      <Typography className={classes.greetingText}>
+        <strong>{`Good ${getTimeOfDay(date)}, ${userName}!`}</strong>
+      </Typography>
+      <Typography variant="h1" className={classes.date}>
+        {getCalendarDate(date)}
+      </Typography>
+    </Grid>
+  );
+};
+
+/** Hook that provides memoized column settings */
+const useColumns = ({ refetch, handleSnackbar, classes }) =>
+  useMemo(() => {
+    return [
+      {
+        headerName: "ID",
+        field: "project_id",
+        editable: false,
+        renderCell: ({ row }) => (
+          <div className={classes.tableRowDiv}>{row.project_id}</div>
+        ),
+        flex: 0.5,
+      },
+      {
+        headerName: "Full name",
+        field: "project_name_full",
+        editable: false,
+        valueGetter: (value) => value.trim(),
+        renderCell: ({ row }) => (
+          <div className={classes.tableRowDiv}>
+            <Link
+              component={RouterLink}
+              to={`/moped/projects/${row.project_id}`}
+              sx={{
+                overflow: "hidden",
+                textOverflow: "ellipsis",
+                display: "block",
+                fontSize: 16,
+              }}
+            >
+              {row.project_name_full}
+            </Link>
+          </div>
+        ),
+        flex: 4,
+      },
+      {
+        headerName: "Status",
+        field: "moped_proj_phases",
+        editable: false,
+        valueGetter: (value) => value[0]?.moped_phase.phase_name,
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: roseeichelmann
body: "Unknown" technically isnt sorting alphabetically correctly here, this is how i handled this in a diff pr i worked on migrating the subprojects table to datagrid pro!  https://github.com/cityofaustin/atd-moped/commit/175e779ddef40ae6546fb760a22d88b076b20094
created_at: 2025-04-23T19:33:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,19 +106,154 @@ const TABS = [
   },
 ];
 
+const UserGreeting = ({ classes, userName }) => {
+  const date = new Date();
+
+  /** Build custom user greeting
+   */
+  return (
+    <Grid className={classes.greeting}>
+      <Typography className={classes.greetingText}>
+        <strong>{`Good ${getTimeOfDay(date)}, ${userName}!`}</strong>
+      </Typography>
+      <Typography variant="h1" className={classes.date}>
+        {getCalendarDate(date)}
+      </Typography>
+    </Grid>
+  );
+};
+
+/** Hook that provides memoized column settings */
+const useColumns = ({ refetch, handleSnackbar, classes }) =>
+  useMemo(() => {
+    return [
+      {
+        headerName: "ID",
+        field: "project_id",
+        editable: false,
+        renderCell: ({ row }) => (
+          <div className={classes.tableRowDiv}>{row.project_id}</div>
+        ),
+        flex: 0.5,
+      },
+      {
+        headerName: "Full name",
+        field: "project_name_full",
+        editable: false,
+        valueGetter: (value) => value.trim(),
+        renderCell: ({ row }) => (
+          <div className={classes.tableRowDiv}>
+            <Link
+              component={RouterLink}
+              to={`/moped/projects/${row.project_id}`}
+              sx={{
+                overflow: "hidden",
+                textOverflow: "ellipsis",
+                display: "block",
+                fontSize: 16,
+              }}
+            >
+              {row.project_name_full}
+            </Link>
+          </div>
+        ),
+        flex: 4,
+      },
+      {
+        headerName: "Status",
+        field: "moped_proj_phases",
+        editable: false,
+        valueGetter: (value) => value[0]?.moped_phase.phase_name,
path: moped-editor/src/views/dashboard/DashboardView.js
user: login: tillyw
body: thanks for catching that!! i've implemented your fix :)
created_at: 2025-04-23T20:32:08Z
author_association: CONTRIBUTOR

PR comments:
user: login: tillyw
created_at: 2025-04-18T20:50:36Z
body: i've made some enhancements and reworked the vertical spacing of the column content and it's ready for rereview!

<img width="1920" alt="Screenshot 2025-04-18 at 3 37 52 PM" src="https://github.com/user-attachments/assets/def8372b-a45a-4db5-908b-d0cc26b45372" />

<img width="1791" alt="Screenshot 2025-04-18 at 3 36 38 PM" src="https://github.com/user-attachments/assets/7eb0e0b1-6747-41fa-820b-eb9933dc8170" />
author_association: CONTRIBUTOR
user: login: mddilley
created_at: 2025-04-21T17:36:38Z
body: @tillyw Could you push your latest commits? This doesn't seem to match up with the screenshots you shared with me and posted as updates in this PR. Thank you!

Also, could you look into the sorting issue that John surfaced in his review?
author_association: COLLABORATOR
user: login: tillyw
created_at: 2025-04-23T00:18:49Z
body: i fixed the sorting issue! it was due to the rows containing nested project data, so i mapped out the nested project data before setting the rows in DataGridPro.

note: i disabled sorting for the 'status update' column because those fields contain nested html tags that get pretty complicated, and i didn't see a use case for sorting by most recent note. happy to reconsider this if y'all think it would be helpful to have!
author_association: CONTRIBUTOR
user: login: tillyw
created_at: 2025-04-23T15:12:45Z
body: > all the updates look good and the DashboardView code is in a much better place now than what's in prod. (sounds like I think its dead, but you know what i really mean)
> 
> I agree that I dont think its necessary to be able to sort the project status update alphabetically. Sorting it on timestamp of when last updated could be a helpful future enhancement, but we'd need to surface the timestamp in this table too.
> 
> When I sorted by project name, I noticed that project 17 and project 3601 both have leading empty spaces in their project name. @mddilley do you think we should clean up project names leading spaces? dont know if its affecting sorting on agol too

i can add a trim method to the project name sorting valueGetter!
author_association: CONTRIBUTOR
user: login: tillyw
created_at: 2025-04-23T22:45:49Z
body: > Looks amazing! Thanks for adding these final touches. 🙏 Can't wait to have this final table out in the wild! 🚀 🚢 🙌
> 
> Forgot to mention that a hidden improvement in this code is that it is now much easier to see which status or milestone was updated after editing in the modals since DataGrid Pro highlights cells. This has been a note in the QA spreadsheet for a while that we get for free. 🙌
> 
>  Screen.Recording.2025-04-23.at.5.29.33.PM.mov

yesss i can see how that would be really helpful for keeping track of which project you're editing!
author_association: CONTRIBUTOR
user: login: mddilley
created_at: 2025-04-24T17:51:30Z
body: Merging this one to continue testing. Thanks @tillyw for helping test this fix! 🙏

For everyone else, see [this thread](https://austininnovation.slack.com/archives/CNUEPKLB1/p1745513893713209)
author_association: COLLABORATOR