From 12ff85a0e065fca51f927c2fb81cd3726445fab2 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Wed, 23 Oct 2024 01:04:51 -0600 Subject: [PATCH 01/76] wip build out datagridpro alternative --- .../views/projects/projectView/ProjectTeam.js | 2 + .../ProjectTeamTableDataGridPro.js | 65 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 moped-editor/src/views/projects/projectView/ProjectTeamTableDataGridPro.js diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam.js b/moped-editor/src/views/projects/projectView/ProjectTeam.js index 372b12078e..3aa2ac8a21 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam.js @@ -1,5 +1,6 @@ import React from "react"; import ProjectTeamTable from "./ProjectTeamTable"; +import ProjectTeamTableDataGridPro from "./ProjectTeamTableDataGridPro"; import { useParams } from "react-router-dom"; import { CardContent, Grid } from "@mui/material"; @@ -12,6 +13,7 @@ const ProjectTeam = () => { + diff --git a/moped-editor/src/views/projects/projectView/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeamTableDataGridPro.js new file mode 100644 index 0000000000..d3a8a8b06f --- /dev/null +++ b/moped-editor/src/views/projects/projectView/ProjectTeamTableDataGridPro.js @@ -0,0 +1,65 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { DataGridPro } from '@mui/x-data-grid-pro'; +import { useDemoData } from '@mui/x-data-grid-generator'; +import { useGridApiContext } from "@mui/x-data-grid-pro"; +import { useQuery, useMutation } from "@apollo/client"; +import theme from "src/theme"; + +import { TEAM_QUERY } from "../../../queries/project"; + +const ProjectTeamTableDataGridPro = ({ projectId }) => { + const { loading, error, data, refetch } = useQuery(TEAM_QUERY, { + variables: { projectId }, + fetchPolicy: "no-cache", + }); + + if (loading) return

Loading...

; + if (error) return

Error: {error.message}

; + + console.log(data.moped_project_by_pk.moped_proj_personnel); + + const rows = data.moped_project_by_pk.moped_proj_personnel + + const columns = [ + { field: 'moped_user', headerName: 'Name', width: 150, + valueGetter: (user) => { + return user.moped_user.first_name + ' ' + user.moped_user.last_name; + } + }, + { field: 'moped_user', + headerName: 'Workgroup', + valueGetter: (user) => { + console.log(user); + return user.moped_workgroup.workgroup_name; + }, + width: 150 }, + { + field: 'moped_proj_personnel_roles', + headerName: 'Role', + width: 200, + valueGetter: (roles) => { + if (roles.length === 0) { + return ''; + } + const roleNames = roles.map(role => role.moped_project_role.project_role_name); + return roleNames.join(', '); + } + }, + { field: 'notes', headerName: 'Notes', width: 150 }, + ]; + + return ( + + row.project_personnel_id} // Use project_personnel_id as the unique id + loading={loading} + rowHeight={38} + /> + + ); +}; + +export default ProjectTeamTableDataGridPro; From 50aa46fcee6acdf8ebd94e61cd1a9bcf6c640b64 Mon Sep 17 00:00:00 2001 From: tillyw Date: Wed, 30 Oct 2024 19:08:19 -0500 Subject: [PATCH 02/76] adds moped_user_saved_views_to_database --- .../down.sql | 1 + .../up.sql | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 moped-database/migrations/1730324794396_add_moped_user_saved_views_table/down.sql create mode 100644 moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql diff --git a/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/down.sql b/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/down.sql new file mode 100644 index 0000000000..fc02ddbb81 --- /dev/null +++ b/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/down.sql @@ -0,0 +1 @@ +DROP TABLE moped_user_saved_views; diff --git a/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql b/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql new file mode 100644 index 0000000000..fd92361027 --- /dev/null +++ b/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql @@ -0,0 +1,25 @@ +CREATE TABLE public.moped_user_saved_views ( + id serial NOT NULL, + description text, + url text NOT NULL, + query_filters jsonb, + created_by_user_id int4 NOT NULL, + updated_by_user_id int4 NOT NULL, + is_deleted boolean NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +ALTER TABLE moped_user_saved_views +ADD CONSTRAINT fk_moped_user_saved_views_created_by FOREIGN KEY (created_by_user_id) REFERENCES moped_users (user_id), +ADD CONSTRAINT fk_moped_user_saved_views_updated_by FOREIGN KEY (updated_by_user_id) REFERENCES moped_users (user_id); + +-- Adding comments for audit fields +COMMENT ON COLUMN moped_user_saved_views.created_at IS 'Timestamp of when the view was created'; +COMMENT ON COLUMN moped_user_saved_views.created_by_user_id IS 'User ID of the creator of the view'; +COMMENT ON COLUMN moped_user_saved_views.updated_by_user_id IS 'User ID of the last updater of the view'; +COMMENT ON COLUMN moped_user_saved_views.updated_at IS 'Timestamp of the last update of the view'; + +-- Adding comments for moped_user_saved_views constraints +COMMENT ON CONSTRAINT fk_moped_user_saved_views_created_by ON moped_user_saved_views IS 'Foreign key constraint linking created_by_user_id to moped_users table.'; +COMMENT ON CONSTRAINT fk_moped_user_saved_views_updated_by ON moped_user_saved_views IS 'Foreign key constraint linking updated_by_user_id to moped_users table.'; From 4ced18af17211836f92ba72eef65f463aa6f8343 Mon Sep 17 00:00:00 2001 From: tillyw Date: Wed, 30 Oct 2024 19:19:52 -0500 Subject: [PATCH 03/76] adds updated at trigger --- .../down.sql | 2 ++ .../1730324794396_add_moped_user_saved_views_table/up.sql | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/down.sql b/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/down.sql index fc02ddbb81..42b035448d 100644 --- a/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/down.sql +++ b/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/down.sql @@ -1 +1,3 @@ +DROP TRIGGER IF EXISTS set_moped_user_saved_views_updated_at ON moped_user_saved_views; + DROP TABLE moped_user_saved_views; diff --git a/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql b/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql index fd92361027..cc72b92783 100644 --- a/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql +++ b/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql @@ -23,3 +23,10 @@ COMMENT ON COLUMN moped_user_saved_views.updated_at IS 'Timestamp of the last up -- Adding comments for moped_user_saved_views constraints COMMENT ON CONSTRAINT fk_moped_user_saved_views_created_by ON moped_user_saved_views IS 'Foreign key constraint linking created_by_user_id to moped_users table.'; COMMENT ON CONSTRAINT fk_moped_user_saved_views_updated_by ON moped_user_saved_views IS 'Foreign key constraint linking updated_by_user_id to moped_users table.'; + +CREATE TRIGGER set_moped_user_saved_views_updated_at +BEFORE INSERT OR UPDATE ON moped_user_saved_views +FOR EACH ROW +EXECUTE FUNCTION public.set_updated_at(); + +COMMENT ON TRIGGER set_moped_user_saved_views_updated_at ON public.moped_user_saved_views IS 'Trigger to set updated_at timestamp for each insert or update on moped_user_saved_views'; From eb610724725f2a444353f148402d3e56fbd28563 Mon Sep 17 00:00:00 2001 From: tillyw Date: Wed, 30 Oct 2024 19:24:20 -0500 Subject: [PATCH 04/76] track table --- moped-database/metadata/tables.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/moped-database/metadata/tables.yaml b/moped-database/metadata/tables.yaml index 7e2050c8b5..df513d7f4b 100644 --- a/moped-database/metadata/tables.yaml +++ b/moped-database/metadata/tables.yaml @@ -5185,6 +5185,9 @@ - role: moped-viewer permission: filter: {} +- table: + name: moped_user_saved_views + schema: public - table: name: moped_users schema: public From 2e97dbfb6499764896ee35144ccd639331e0bc4c Mon Sep 17 00:00:00 2001 From: tillyw Date: Fri, 1 Nov 2024 18:47:13 -0500 Subject: [PATCH 05/76] adds permissions and triggers --- moped-database/metadata/tables.yaml | 138 ++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/moped-database/metadata/tables.yaml b/moped-database/metadata/tables.yaml index df513d7f4b..996dca5552 100644 --- a/moped-database/metadata/tables.yaml +++ b/moped-database/metadata/tables.yaml @@ -5188,6 +5188,144 @@ - table: name: moped_user_saved_views schema: public + insert_permissions: + - role: moped-admin + permission: + check: {} + set: + created_by_user_id: x-hasura-x-hasura-user-db-id + updated_by_user_id: x-hasura-x-hasura-user-db-id + columns: + - description + - is_deleted + - query_filters + - url + comment: "" + - role: moped-editor + permission: + check: {} + set: + created_by_user_id: x-hasura-x-hasura-user-db-id + updated_by_user_id: x-hasura-x-hasura-user-db-id + columns: + - description + - is_deleted + - query_filters + - url + comment: "" + select_permissions: + - role: moped-admin + permission: + columns: + - is_deleted + - id + - query_filters + - description + - url + - updated_at + - created_at + - created_by_user_id + - updated_by_user_id + filter: {} + comment: "" + - role: moped-editor + permission: + columns: + - is_deleted + - id + - query_filters + - description + - url + - updated_at + - created_at + - created_by_user_id + - updated_by_user_id + filter: {} + comment: "" + - role: moped-viewer + permission: + columns: + - is_deleted + - id + - query_filters + - description + - url + - updated_at + - created_at + - created_by_user_id + - updated_by_user_id + filter: {} + comment: "" + update_permissions: + - role: moped-admin + permission: + columns: + - description + - is_deleted + - query_filters + - url + filter: {} + check: null + set: + updated_by_user_id: x-hasura-x-hasura-user-db-id + comment: "" + - role: moped-editor + permission: + columns: + - description + - is_deleted + - query_filters + - url + filter: {} + check: null + set: + updated_by_user_id: x-hasura-x-hasura-user-db-id + comment: "" + event_triggers: + - name: activity_log_moped_user_saved_views + definition: + enable_manual: false + insert: + columns: '*' + update: + columns: '*' + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_ENDPOINT + headers: + - name: x-hasura-admin-secret + value_from_env: ACTIVITY_LOG_API_SECRET + request_transform: + body: + action: transform + template: |- + { + "query": "mutation InsertActivity($object: moped_activity_log_insert_input!) { insert_moped_activity_log_one(object: $object) { activity_id } }", + "variables": { + "object": { + "record_id": {{ $body.event.data.new.id }}, + "record_type": {{ $body.table.name }}, + "activity_id": {{ $body.id }}, + "record_data": {"event": {{ $body.event }}}, + "description": [{"newSchema": "true"}], + "operation_type": {{ $body.event.op }}, + "updated_by_user_id": {{ $session_variables?['x-hasura-user-db-id'] ?? 1}} + } + } + } + method: POST + query_params: {} + template_engine: Kriti + version: 2 + cleanup_config: + batch_size: 10000 + clean_invocation_logs: false + clear_older_than: 168 + paused: true + schedule: 0 0 * * * + timeout: 60 - table: name: moped_users schema: public From bf5d732f5337e9ae2867d0b4cba15aa651929e02 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Mon, 4 Nov 2024 21:11:24 -0700 Subject: [PATCH 06/76] Get data rendering in table --- .../views/projects/projectView/ProjectTeam.js | 2 +- .../ProjectTeamTableDataGridPro.js | 128 ++++++++++++++++++ .../ProjectTeam/ProjectTeamToolbar.js | 25 ++++ .../ProjectTeamTableDataGridPro.js | 65 --------- 4 files changed, 154 insertions(+), 66 deletions(-) create mode 100644 moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js create mode 100644 moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamToolbar.js delete mode 100644 moped-editor/src/views/projects/projectView/ProjectTeamTableDataGridPro.js diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam.js b/moped-editor/src/views/projects/projectView/ProjectTeam.js index 3aa2ac8a21..5272329a32 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam.js @@ -1,6 +1,6 @@ import React from "react"; import ProjectTeamTable from "./ProjectTeamTable"; -import ProjectTeamTableDataGridPro from "./ProjectTeamTableDataGridPro"; +import ProjectTeamTableDataGridPro from "./ProjectTeam/ProjectTeamTableDataGridPro"; import { useParams } from "react-router-dom"; import { CardContent, Grid } from "@mui/material"; diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js new file mode 100644 index 0000000000..4a7b662ef5 --- /dev/null +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js @@ -0,0 +1,128 @@ +import * as React from 'react'; +import { useState } from "react"; +import { Box, Icon, Link } from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; + +import { DataGridPro } from '@mui/x-data-grid-pro'; +import { useDemoData } from '@mui/x-data-grid-generator'; +import { useGridApiContext } from "@mui/x-data-grid-pro"; +import { useQuery, useMutation } from "@apollo/client"; +import theme from "src/theme"; +import { TEAM_QUERY, DELETE_PROJECT_PERSONNEL } from "src/queries/project"; +import dataGridProStyleOverrides from 'src/styles/dataGridProStylesOverrides'; +import ProjectTeamToolbar from './ProjectTeamToolbar'; + + + + +const useStyles = makeStyles((theme) => ({ + infoIcon: { + fontSize: "1.25rem", + verticalAlign: "sub", + color: theme.palette.text.primary, + "&:hover": { + color: theme.palette.primary.main, + }, + }, +})); + +const ProjectTeamTableDataGridPro = ({ projectId }) => { + const classes = useStyles(); + const { loading, error, data, refetch } = useQuery(TEAM_QUERY, { + variables: { projectId }, + fetchPolicy: "no-cache", + }); + + + const [editTeamMember, setEditTeamMember] = useState(null); + const [deleteTeamMember, { loading: deleteInProgress }] = useMutation(DELETE_PROJECT_PERSONNEL); + + if (loading) return

Loading...

; + if (error) return

Error: {error.message}

; + + + console.log(data.moped_project_by_pk.moped_proj_personnel); + + // Set the rows but add the workgroup name to the row data + // instead of being nested under moped_user. + const rows = data.moped_project_by_pk.moped_proj_personnel.map(personnel => ({ + ...personnel, + moped_workgroup: personnel.moped_user.moped_workgroup + })); + + const onClickAddTeamMember = () => { + console.log('add team member'); + return setEditTeamMember({ project_id: projectId }); + } + + + const columns = [ + { + field: 'moped_user', + headerName: 'Name', + width: 150, + valueGetter: (user) => { + return user ? `${user.first_name} ${user.last_name}` : ''; + } + }, + { + field: 'moped_workgroup', + headerName: 'Workgroup', + valueGetter: (workgroup) => workgroup?.workgroup_name, + width: 150 + }, + { + field: 'moped_proj_personnel_roles', + headerName: ( + + Role{" "} + + info_outline + + + ), + width: 200, + valueGetter: (roles) => { + if (roles.length === 0) { + return ''; + } + const roleNames = roles.map(role => role.moped_project_role.project_role_name); + return roleNames.join(', '); + } + }, + { field: 'notes', headerName: 'Notes', width: 150 }, + ]; + + return ( + + row.project_personnel_id} // Use project_personnel_id as the unique id + disableRowSelectionOnClick + disableColumnMenu + getRowHeight={() => 'auto'} + hideFooter + localeText={{ noRowsLabel: 'No team members found' }} + loading={loading} + slots={{ + toolbar: ProjectTeamToolbar, + }} + slotProps={{ + toolbar: { + addAction: onClickAddTeamMember, + }, + }} + /> + + ); +}; + +export default ProjectTeamTableDataGridPro; diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamToolbar.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamToolbar.js new file mode 100644 index 0000000000..7a9203c727 --- /dev/null +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamToolbar.js @@ -0,0 +1,25 @@ +import { Box, Typography } from "@mui/material"; +import ButtonDropdownMenu from "src/components/ButtonDropdownMenu"; +import { Button } from "@mui/material"; +import AddCircleIcon from '@mui/icons-material/AddCircle'; + +/** Custom toolbar title that resembles material table titles */ +const ProjectTeamToolbar = ({ addAction, setIsDialogOpen }) => ( + + + Project Team + +
+ +
+
+); + +export default ProjectTeamToolbar; \ No newline at end of file diff --git a/moped-editor/src/views/projects/projectView/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeamTableDataGridPro.js deleted file mode 100644 index d3a8a8b06f..0000000000 --- a/moped-editor/src/views/projects/projectView/ProjectTeamTableDataGridPro.js +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import { DataGridPro } from '@mui/x-data-grid-pro'; -import { useDemoData } from '@mui/x-data-grid-generator'; -import { useGridApiContext } from "@mui/x-data-grid-pro"; -import { useQuery, useMutation } from "@apollo/client"; -import theme from "src/theme"; - -import { TEAM_QUERY } from "../../../queries/project"; - -const ProjectTeamTableDataGridPro = ({ projectId }) => { - const { loading, error, data, refetch } = useQuery(TEAM_QUERY, { - variables: { projectId }, - fetchPolicy: "no-cache", - }); - - if (loading) return

Loading...

; - if (error) return

Error: {error.message}

; - - console.log(data.moped_project_by_pk.moped_proj_personnel); - - const rows = data.moped_project_by_pk.moped_proj_personnel - - const columns = [ - { field: 'moped_user', headerName: 'Name', width: 150, - valueGetter: (user) => { - return user.moped_user.first_name + ' ' + user.moped_user.last_name; - } - }, - { field: 'moped_user', - headerName: 'Workgroup', - valueGetter: (user) => { - console.log(user); - return user.moped_workgroup.workgroup_name; - }, - width: 150 }, - { - field: 'moped_proj_personnel_roles', - headerName: 'Role', - width: 200, - valueGetter: (roles) => { - if (roles.length === 0) { - return ''; - } - const roleNames = roles.map(role => role.moped_project_role.project_role_name); - return roleNames.join(', '); - } - }, - { field: 'notes', headerName: 'Notes', width: 150 }, - ]; - - return ( - - row.project_personnel_id} // Use project_personnel_id as the unique id - loading={loading} - rowHeight={38} - /> - - ); -}; - -export default ProjectTeamTableDataGridPro; From 42e3441aa7879c8ae3e3e2d9e21b8ceb8337970e Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Tue, 5 Nov 2024 01:06:30 -0700 Subject: [PATCH 07/76] wip, table CRUD actions --- .../ProjectTeamTableDataGridPro.js | 94 +++++++++++++++++-- 1 file changed, 84 insertions(+), 10 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js index 4a7b662ef5..196ac97fcf 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js @@ -1,19 +1,23 @@ import * as React from 'react'; -import { useState } from "react"; +import { useState, useMemo } from "react"; import { Box, Icon, Link } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; -import { DataGridPro } from '@mui/x-data-grid-pro'; -import { useDemoData } from '@mui/x-data-grid-generator'; -import { useGridApiContext } from "@mui/x-data-grid-pro"; +import { DataGridPro, GridRowModes, GridActionsCellItem, useGridApiContext } from '@mui/x-data-grid-pro'; import { useQuery, useMutation } from "@apollo/client"; import theme from "src/theme"; -import { TEAM_QUERY, DELETE_PROJECT_PERSONNEL } from "src/queries/project"; +import { + TEAM_QUERY, + UPDATE_PROJECT_PERSONNEL, + INSERT_PROJECT_PERSONNEL, + DELETE_PROJECT_PERSONNEL +} from "src/queries/project"; import dataGridProStyleOverrides from 'src/styles/dataGridProStylesOverrides'; import ProjectTeamToolbar from './ProjectTeamToolbar'; +import { EditOutlined as EditOutlinedIcon, DeleteOutline as DeleteOutlineIcon, Check as CheckIcon, Close as CloseIcon } from '@mui/icons-material'; - +import { defaultEditColumnIconStyle } from 'src/styles/dataGridProStylesOverrides'; const useStyles = makeStyles((theme) => ({ infoIcon: { @@ -55,8 +59,20 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { return setEditTeamMember({ project_id: projectId }); } + const onClickEditTeamMember = (projectPersonnelId) => { + console.log('edit team member', projectPersonnelId); + return setEditTeamMember({ project_personnel_id: projectPersonnelId }); + } - const columns = [ + const useColumns = ({ + rowModesModel, + handleEditClick, + handleSaveClick, + handleCancelClick, + handleDeleteOpen, + }) => + useMemo(() => { + return [ { field: 'moped_user', headerName: 'Name', @@ -73,7 +89,8 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { }, { field: 'moped_proj_personnel_roles', - headerName: ( + headerName: 'Role', + renderHeader: () => ( Role{" "} { } }, { field: 'notes', headerName: 'Notes', width: 150 }, - ]; + { + headerName: '', + field: 'edit', + hideable: false, + filterable: false, + sortable: false, + editable: false, + type: "actions", + getActions: ({ id }) => { + const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; + if (isInEditMode) { + return [ + } + label="Save" + sx={{ + color: "primary.main", + }} + onClick={handleSaveClick(id)} + />, + } + label="Cancel" + className="textPrimary" + onClick={handleCancelClick(id)} + color="inherit" + />, + ]; + } + return [ + } + label="Edit" + className="textPrimary" + onClick={handleEditClick(id)} + color="inherit" + />, + } + label="Delete" + onClick={() => handleDeleteOpen(id)} + color="inherit" + />, + ]; + }, + } + ] + }, + [rowModesModel, handleEditClick, handleSaveClick, handleCancelClick, handleDeleteOpen] + ); + + const dataGridColumns = useColumns({ + rowModesModel, + handleEditClick, + handleSaveClick, + handleCancelClick, + handleDeleteOpen, + }); return ( row.project_personnel_id} // Use project_personnel_id as the unique id From 4678bb9b8526ac1439606df537e73ebd8809581e Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Tue, 5 Nov 2024 01:22:17 -0700 Subject: [PATCH 08/76] Add placeholder actions --- .../ProjectTeamTableDataGridPro.js | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js index 196ac97fcf..9074258b75 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js @@ -1,11 +1,12 @@ import * as React from 'react'; -import { useState, useMemo } from "react"; -import { Box, Icon, Link } from '@mui/material'; +import { useState, useMemo, useEffect } from "react"; +import { Box, Icon, Link, CircularProgress } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; import { DataGridPro, GridRowModes, GridActionsCellItem, useGridApiContext } from '@mui/x-data-grid-pro'; import { useQuery, useMutation } from "@apollo/client"; import theme from "src/theme"; +import { defaultEditColumnIconStyle } from "src/utils/dataGridHelpers"; import { TEAM_QUERY, UPDATE_PROJECT_PERSONNEL, @@ -17,7 +18,6 @@ import ProjectTeamToolbar from './ProjectTeamToolbar'; import { EditOutlined as EditOutlinedIcon, DeleteOutline as DeleteOutlineIcon, Check as CheckIcon, Close as CloseIcon } from '@mui/icons-material'; -import { defaultEditColumnIconStyle } from 'src/styles/dataGridProStylesOverrides'; const useStyles = makeStyles((theme) => ({ infoIcon: { @@ -39,20 +39,26 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { const [editTeamMember, setEditTeamMember] = useState(null); + const [deleteTeamMemberId, setDeleteTeamMemberId] = useState(null); const [deleteTeamMember, { loading: deleteInProgress }] = useMutation(DELETE_PROJECT_PERSONNEL); - if (loading) return

Loading...

; - if (error) return

Error: {error.message}

; - - - console.log(data.moped_project_by_pk.moped_proj_personnel); + const [rows, setRows] = useState([]); + const [rowModesModel, setRowModesModel] = useState({}); + + useEffect(() => { + if (data?.moped_project_by_pk?.moped_proj_personnel?.length > 0) { + // Set the rows but add the workgroup name to the row data + // instead of being nested under moped_user. + setRows(data.moped_project_by_pk.moped_proj_personnel.map(personnel => ({ + ...personnel, + moped_workgroup: personnel.moped_user.moped_workgroup + }))); + } else { + setRows([]); // Reset rows when no data is available + } + }, [data]); - // Set the rows but add the workgroup name to the row data - // instead of being nested under moped_user. - const rows = data.moped_project_by_pk.moped_proj_personnel.map(personnel => ({ - ...personnel, - moped_workgroup: personnel.moped_user.moped_workgroup - })); + const onClickAddTeamMember = () => { console.log('add team member'); @@ -64,7 +70,24 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { return setEditTeamMember({ project_personnel_id: projectPersonnelId }); } + const handleEditClick = (id) => () => { + setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); + }; + + const handleSaveClick = (id) => () => { + setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); + }; + + const handleCancelClick = (id) => () => { + setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); + }; + + const handleDeleteOpen = (id) => { + setDeleteTeamMemberId(id); + }; + const useColumns = ({ + data, rowModesModel, handleEditClick, handleSaveClick, @@ -160,7 +183,7 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { } ] }, - [rowModesModel, handleEditClick, handleSaveClick, handleCancelClick, handleDeleteOpen] + [data, rowModesModel, handleEditClick, handleSaveClick, handleCancelClick, handleDeleteOpen] ); const dataGridColumns = useColumns({ @@ -171,6 +194,8 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { handleDeleteOpen, }); + if (loading || !data) return ; + return ( Date: Tue, 5 Nov 2024 02:01:48 -0700 Subject: [PATCH 09/76] add copies of useful components and util to other dir maybe we move this and update their references in other DataGridPro instances? --- .../DataGridPro/DataGridTextField.js | 57 ++++++++++++++ .../LookupAutocompleteComponent.js | 77 +++++++++++++++++++ moped-editor/src/utils/helpers.js | 14 ++++ 3 files changed, 148 insertions(+) create mode 100644 moped-editor/src/components/DataGridPro/DataGridTextField.js create mode 100644 moped-editor/src/components/DataGridPro/LookupAutocompleteComponent.js create mode 100644 moped-editor/src/utils/helpers.js diff --git a/moped-editor/src/components/DataGridPro/DataGridTextField.js b/moped-editor/src/components/DataGridPro/DataGridTextField.js new file mode 100644 index 0000000000..8356aaeb4e --- /dev/null +++ b/moped-editor/src/components/DataGridPro/DataGridTextField.js @@ -0,0 +1,57 @@ +import React from "react"; +import { TextField } from "@mui/material"; +import { useGridApiContext } from "@mui/x-data-grid-pro"; + +/** + * @param {Integer} id - Data Grid row id + * @param {String} value - field value + * @param {String} field - name of field + * @param {Boolean} hasFocus - does this field have focus + * @param {String} helperText - optional helper text + * @param {Boolean} error - toggles error style in textfield + * @return {JSX.Element} + */ +const DataGridTextField = ({ + id, + value, + field, + hasFocus, + helperText, + error, +}) => { + const apiRef = useGridApiContext(); + const ref = React.useRef(null); + + React.useEffect(() => { + if (hasFocus) { + ref.current.focus(); + } + }, [hasFocus]); + + const handleChange = (event, newValue) => { + const { value: inputValue } = event.target; + + apiRef.current.setEditCellValue({ + id, + field, + value: inputValue, + }); + }; + + return ( + + ); +}; + +export default DataGridTextField; diff --git a/moped-editor/src/components/DataGridPro/LookupAutocompleteComponent.js b/moped-editor/src/components/DataGridPro/LookupAutocompleteComponent.js new file mode 100644 index 0000000000..27827211ba --- /dev/null +++ b/moped-editor/src/components/DataGridPro/LookupAutocompleteComponent.js @@ -0,0 +1,77 @@ +import React from "react"; +import { Autocomplete, TextField } from "@mui/material"; +import { useGridApiContext } from "@mui/x-data-grid-pro"; +import makeStyles from "@mui/styles/makeStyles"; +import { getLookupValueByID } from "src/utils/helpers"; +import CustomPopper from "src/components/CustomPopper"; + +const useStyles = makeStyles((theme) => ({ + autocompleteLookupInput: { + minWidth: "200px", + alignContent: "center", + padding: theme.spacing(1), + }, +})); + +/** + * Component for dropdown select using a lookup table as options + * @param {Number} id - row id in Data Grid + * @param {string} value - Field value + * @param {string} field - name of Field + * @param {Boolean} hasFocus - is field focused + * @param {String} name - name of lookup table + * @param {Array|Objects} lookupTable - the lookup table data + * @returns {React component} + */ +const LookupAutocompleteComponent = ({ + id, + value, + field, + hasFocus, + name, + lookupTable, +}) => { + const classes = useStyles(); + const apiRef = useGridApiContext(); + const ref = React.useRef(null); + + React.useEffect(() => { + if (hasFocus) { + ref.current.focus(); + } + }, [hasFocus]); + + const handleChange = (event, newValue) => { + apiRef.current.setEditCellValue({ + id, + field, + value: newValue ? newValue[`${name}_id`] : null, + }); + }; + + return ( + ( + + )} + getOptionLabel={(option) => + // if our value is a string, just return the string instead of accessing the name + typeof option === "string" ? option : option[`${name}_name`] + } + isOptionEqualToValue={(value, option) => value[`${name}_name`] === option} + onChange={handleChange} + /> + ); +}; + +export default LookupAutocompleteComponent; diff --git a/moped-editor/src/utils/helpers.js b/moped-editor/src/utils/helpers.js new file mode 100644 index 0000000000..2fdf1b2583 --- /dev/null +++ b/moped-editor/src/utils/helpers.js @@ -0,0 +1,14 @@ +/** + * Get lookup value for a given table using a record ID and returning a name + * @param {Array|Object} lookupTable - Lookup table + * @param {string} attribute - Prefix version of attribute name relying on the pattern of _id and _name + * @param {number} id - ID used to find target record in lookup table + * @return {string} - Name of attribute in the given row. + */ +export const getLookupValueByID = (lookupTable, attribute, id) => { + if (!id) return null; + + return lookupTable.find((item) => item[`${attribute}_id`] === id)[ + `${attribute}_name` + ]; +}; From be369c48f42ebf7a100ac0c61d402e6cfff91a24 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Tue, 5 Nov 2024 02:01:56 -0700 Subject: [PATCH 10/76] more CRUD prep --- .../ProjectTeamTableDataGridPro.js | 191 +++++++++++------- 1 file changed, 123 insertions(+), 68 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js index 9074258b75..d68c49c7cf 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js @@ -1,9 +1,9 @@ import * as React from 'react'; -import { useState, useMemo, useEffect } from "react"; +import { useState, useMemo, useEffect, useCallback } from "react"; import { Box, Icon, Link, CircularProgress } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; -import { DataGridPro, GridRowModes, GridActionsCellItem, useGridApiContext } from '@mui/x-data-grid-pro'; +import { DataGridPro, GridRowModes, GridActionsCellItem, useGridApiRef } from '@mui/x-data-grid-pro'; import { useQuery, useMutation } from "@apollo/client"; import theme from "src/theme"; import { defaultEditColumnIconStyle } from "src/utils/dataGridHelpers"; @@ -18,6 +18,10 @@ import ProjectTeamToolbar from './ProjectTeamToolbar'; import { EditOutlined as EditOutlinedIcon, DeleteOutline as DeleteOutlineIcon, Check as CheckIcon, Close as CloseIcon } from '@mui/icons-material'; +import { useUser } from 'src/auth/user'; + +import LookupAutocompleteComponent from 'src/components/DataGridPro/LookupAutocompleteComponent'; +import DataGridTextField from 'src/components/DataGridPro/DataGridTextField'; const useStyles = makeStyles((theme) => ({ infoIcon: { @@ -30,62 +34,6 @@ const useStyles = makeStyles((theme) => ({ }, })); -const ProjectTeamTableDataGridPro = ({ projectId }) => { - const classes = useStyles(); - const { loading, error, data, refetch } = useQuery(TEAM_QUERY, { - variables: { projectId }, - fetchPolicy: "no-cache", - }); - - - const [editTeamMember, setEditTeamMember] = useState(null); - const [deleteTeamMemberId, setDeleteTeamMemberId] = useState(null); - const [deleteTeamMember, { loading: deleteInProgress }] = useMutation(DELETE_PROJECT_PERSONNEL); - - const [rows, setRows] = useState([]); - const [rowModesModel, setRowModesModel] = useState({}); - - useEffect(() => { - if (data?.moped_project_by_pk?.moped_proj_personnel?.length > 0) { - // Set the rows but add the workgroup name to the row data - // instead of being nested under moped_user. - setRows(data.moped_project_by_pk.moped_proj_personnel.map(personnel => ({ - ...personnel, - moped_workgroup: personnel.moped_user.moped_workgroup - }))); - } else { - setRows([]); // Reset rows when no data is available - } - }, [data]); - - - - const onClickAddTeamMember = () => { - console.log('add team member'); - return setEditTeamMember({ project_id: projectId }); - } - - const onClickEditTeamMember = (projectPersonnelId) => { - console.log('edit team member', projectPersonnelId); - return setEditTeamMember({ project_personnel_id: projectPersonnelId }); - } - - const handleEditClick = (id) => () => { - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); - }; - - const handleSaveClick = (id) => () => { - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); - }; - - const handleCancelClick = (id) => () => { - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); - }; - - const handleDeleteOpen = (id) => { - setDeleteTeamMemberId(id); - }; - const useColumns = ({ data, rowModesModel, @@ -93,26 +41,37 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { handleSaveClick, handleCancelClick, handleDeleteOpen, + classes }) => useMemo(() => { return [ { - field: 'moped_user', headerName: 'Name', - width: 150, + field: 'moped_user', + width: 200, + editable: true, valueGetter: (user) => { return user ? `${user.first_name} ${user.last_name}` : ''; - } + }, + renderEditCell: (props) => ( + + ) }, { - field: 'moped_workgroup', headerName: 'Workgroup', + field: 'moped_workgroup', + width: 200, valueGetter: (workgroup) => workgroup?.workgroup_name, - width: 150 }, { - field: 'moped_proj_personnel_roles', headerName: 'Role', + field: 'moped_proj_personnel_roles', + width: 200, + editable: true, renderHeader: () => ( Role{" "} @@ -125,16 +84,28 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { ), - width: 200, valueGetter: (roles) => { if (roles.length === 0) { return ''; } const roleNames = roles.map(role => role.moped_project_role.project_role_name); return roleNames.join(', '); - } + }, + renderEditCell: (props) => ( + + ) + }, + { + headerName: 'Notes', + field: 'notes', + width: 200, + editable: true, + renderEditCell: (props) => }, - { field: 'notes', headerName: 'Notes', width: 150 }, { headerName: '', field: 'edit', @@ -183,15 +154,97 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { } ] }, - [data, rowModesModel, handleEditClick, handleSaveClick, handleCancelClick, handleDeleteOpen] + [ + data, + rowModesModel, + handleEditClick, + handleSaveClick, + handleCancelClick, + handleDeleteOpen, + classes + ] ); + +const ProjectTeamTableDataGridPro = ({ projectId }) => { + const apiRef = useGridApiRef(); + const classes = useStyles(); + const { user } = useUser(); + + const { loading, error, data, refetch } = useQuery(TEAM_QUERY, { + variables: { projectId }, + fetchPolicy: "no-cache", + }); + + const [addProjectPersonnel] = useMutation(INSERT_PROJECT_PERSONNEL); + const [updateProjectPersonnel] = useMutation(UPDATE_PROJECT_PERSONNEL); + const [deleteProjectPersonnel] = useMutation(DELETE_PROJECT_PERSONNEL); + + const [editTeamMember, setEditTeamMember] = useState(null); + const [deleteTeamMemberId, setDeleteTeamMemberId] = useState(null); + + const [rows, setRows] = useState([]); + const [rowModesModel, setRowModesModel] = useState({}); + + useEffect(() => { + if (data?.moped_project_by_pk?.moped_proj_personnel?.length > 0) { + // Set the rows but add the workgroup name to the row data + // instead of being nested under moped_user. + setRows(data.moped_project_by_pk.moped_proj_personnel.map(personnel => ({ + ...personnel, + moped_workgroup: personnel.moped_user.moped_workgroup + }))); + } else { + setRows([]); // Reset rows when no data is available + } + }, [data]); + + + + const onClickAddTeamMember = () => { + console.log('add team member'); + return setEditTeamMember({ project_id: projectId }); + } + + const onClickEditTeamMember = (projectPersonnelId) => { + console.log('edit team member', projectPersonnelId); + return setEditTeamMember({ project_personnel_id: projectPersonnelId }); + } + + const handleEditClick = useCallback((id) => () => { + console.log('edit click', id); + setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); + }, [rowModesModel]); + + const handleSaveClick = useCallback((id) => () => { + console.log('save click', id); + setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); + }, [rowModesModel]); + + const handleCancelClick = useCallback((id) => () => { + console.log('cancel click', id); + setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); + }, [rowModesModel]); + + const handleDeleteOpen = useCallback((id) => { + console.log('delete open', id); + setDeleteTeamMemberId(id); + }, [deleteTeamMemberId]); + + const processRowUpdate = useCallback((newRow, oldRow) => { + console.log('process row update', newRow, oldRow); + return newRow; + }, []); + + const dataGridColumns = useColumns({ + data, rowModesModel, handleEditClick, handleSaveClick, handleCancelClick, handleDeleteOpen, + classes }); if (loading || !data) return ; @@ -200,6 +253,7 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { { slotProps={{ toolbar: { addAction: onClickAddTeamMember, + classes: classes, }, }} /> From b53c2238fc95355f764f4982ce45bd4499a03e4a Mon Sep 17 00:00:00 2001 From: tillyw Date: Thu, 7 Nov 2024 13:27:29 -0600 Subject: [PATCH 11/76] addressed requested changes --- moped-database/metadata/tables.yaml | 45 ------------------- .../up.sql | 13 ++++-- 2 files changed, 9 insertions(+), 49 deletions(-) diff --git a/moped-database/metadata/tables.yaml b/moped-database/metadata/tables.yaml index 996dca5552..b9be1b09cf 100644 --- a/moped-database/metadata/tables.yaml +++ b/moped-database/metadata/tables.yaml @@ -5281,51 +5281,6 @@ set: updated_by_user_id: x-hasura-x-hasura-user-db-id comment: "" - event_triggers: - - name: activity_log_moped_user_saved_views - definition: - enable_manual: false - insert: - columns: '*' - update: - columns: '*' - retry_conf: - interval_sec: 10 - num_retries: 0 - timeout_sec: 60 - webhook_from_env: HASURA_ENDPOINT - headers: - - name: x-hasura-admin-secret - value_from_env: ACTIVITY_LOG_API_SECRET - request_transform: - body: - action: transform - template: |- - { - "query": "mutation InsertActivity($object: moped_activity_log_insert_input!) { insert_moped_activity_log_one(object: $object) { activity_id } }", - "variables": { - "object": { - "record_id": {{ $body.event.data.new.id }}, - "record_type": {{ $body.table.name }}, - "activity_id": {{ $body.id }}, - "record_data": {"event": {{ $body.event }}}, - "description": [{"newSchema": "true"}], - "operation_type": {{ $body.event.op }}, - "updated_by_user_id": {{ $session_variables?['x-hasura-user-db-id'] ?? 1}} - } - } - } - method: POST - query_params: {} - template_engine: Kriti - version: 2 - cleanup_config: - batch_size: 10000 - clean_invocation_logs: false - clear_older_than: 168 - paused: true - schedule: 0 0 * * * - timeout: 60 - table: name: moped_users schema: public diff --git a/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql b/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql index cc72b92783..f63323a706 100644 --- a/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql +++ b/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql @@ -1,13 +1,13 @@ CREATE TABLE public.moped_user_saved_views ( id serial NOT NULL, - description text, + description text NOT NULL, url text NOT NULL, query_filters jsonb, created_by_user_id int4 NOT NULL, updated_by_user_id int4 NOT NULL, - is_deleted boolean NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now() + updated_at timestamptz NOT NULL DEFAULT now(), + is_deleted boolean NOT NULL DEFAULT false ); ALTER TABLE moped_user_saved_views @@ -15,10 +15,15 @@ ADD CONSTRAINT fk_moped_user_saved_views_created_by FOREIGN KEY (created_by_user ADD CONSTRAINT fk_moped_user_saved_views_updated_by FOREIGN KEY (updated_by_user_id) REFERENCES moped_users (user_id); -- Adding comments for audit fields -COMMENT ON COLUMN moped_user_saved_views.created_at IS 'Timestamp of when the view was created'; +-- TO DO: add comments to all fields +COMMENT ON COLUMN moped_user_saved_views.description IS 'Description entered by the creator of the view'; +COMMENT ON COLUMN moped_user_saved_views.url IS 'URL string associated with the view (may break if database fields or operators are changed)'; +COMMENT ON COLUMN moped_user_saved_views.query_filters IS 'JSON blob of filters that make up the query'; COMMENT ON COLUMN moped_user_saved_views.created_by_user_id IS 'User ID of the creator of the view'; COMMENT ON COLUMN moped_user_saved_views.updated_by_user_id IS 'User ID of the last updater of the view'; +COMMENT ON COLUMN moped_user_saved_views.created_at IS 'Timestamp of when the view was created'; COMMENT ON COLUMN moped_user_saved_views.updated_at IS 'Timestamp of the last update of the view'; +COMMENT ON COLUMN moped_user_saved_views.query_filters IS 'Boolean indicating whether the view has been soft deleted and thereby not rendered in the UI'; -- Adding comments for moped_user_saved_views constraints COMMENT ON CONSTRAINT fk_moped_user_saved_views_created_by ON moped_user_saved_views IS 'Foreign key constraint linking created_by_user_id to moped_users table.'; From 7508871848232c41c4dce4c20c24ac0d5381de68 Mon Sep 17 00:00:00 2001 From: tillyw Date: Thu, 7 Nov 2024 13:35:56 -0600 Subject: [PATCH 12/76] remove comment --- .../1730324794396_add_moped_user_saved_views_table/up.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql b/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql index f63323a706..682d346f2c 100644 --- a/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql +++ b/moped-database/migrations/1730324794396_add_moped_user_saved_views_table/up.sql @@ -15,7 +15,6 @@ ADD CONSTRAINT fk_moped_user_saved_views_created_by FOREIGN KEY (created_by_user ADD CONSTRAINT fk_moped_user_saved_views_updated_by FOREIGN KEY (updated_by_user_id) REFERENCES moped_users (user_id); -- Adding comments for audit fields --- TO DO: add comments to all fields COMMENT ON COLUMN moped_user_saved_views.description IS 'Description entered by the creator of the view'; COMMENT ON COLUMN moped_user_saved_views.url IS 'URL string associated with the view (may break if database fields or operators are changed)'; COMMENT ON COLUMN moped_user_saved_views.query_filters IS 'JSON blob of filters that make up the query'; From 78fed515c71c7ab9755cb83296807c9dce6f954f Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Tue, 12 Nov 2024 00:21:07 -0700 Subject: [PATCH 13/76] wip crud. cancel works --- .../ProjectTeamRoleMultiselect.js | 32 +- .../ProjectTeamTableDataGridPro.js | 349 +++++++++++------- .../ProjectTeam/TeamAutocompleteComponent.js | 92 +++++ 3 files changed, 331 insertions(+), 142 deletions(-) rename moped-editor/src/views/projects/projectView/{ => ProjectTeam}/ProjectTeamRoleMultiselect.js (74%) create mode 100644 moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js diff --git a/moped-editor/src/views/projects/projectView/ProjectTeamRoleMultiselect.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js similarity index 74% rename from moped-editor/src/views/projects/projectView/ProjectTeamRoleMultiselect.js rename to moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js index 081db3afcc..b283dc2486 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeamRoleMultiselect.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js @@ -11,6 +11,8 @@ import { Typography, } from "@mui/material"; import makeStyles from '@mui/styles/makeStyles'; +import { useGridApiContext } from "@mui/x-data-grid-pro"; + const useStyles = makeStyles((theme) => ({ formControl: { @@ -24,22 +26,39 @@ const useStyles = makeStyles((theme) => ({ }, })); -const ProjectTeamRoleMultiselect = ({ roles, value, onChange }) => { + + + +const ProjectTeamRoleMultiselect = ({ id, field, roles, value }) => { + const [selectedValues, setSelectedValues] = React.useState(Array.isArray(value) ? value : []); const classes = useStyles(); + const apiRef = useGridApiContext(); + const ref = React.useRef(null); + + const handleChange = (event) => { + const newValue = event.target.value; + setSelectedValues(newValue); + apiRef.current.setEditCellValue({ + id, + field, + value: newValue, + }); + }; + return ( } renderValue={() => { const selectedRoles = roles.filter((role) => - value.includes(role.project_role_id) + selectedValues.includes(role.project_role_id) ); return selectedRoles.map(({ project_role_name }) => ( {project_role_name} @@ -59,9 +78,8 @@ const ProjectTeamRoleMultiselect = ({ roles, value, onChange }) => { project_role_name, project_role_description, }) => { - const isChecked = value.includes(project_role_id); + const isChecked = selectedValues.includes(project_role_id); return ( - // ListItemClasses ({ infoIcon: { @@ -34,137 +38,165 @@ const useStyles = makeStyles((theme) => ({ }, })); - const useColumns = ({ - data, - rowModesModel, - handleEditClick, - handleSaveClick, - handleCancelClick, - handleDeleteOpen, - classes - }) => - useMemo(() => { - return [ - { - headerName: 'Name', - field: 'moped_user', - width: 200, - editable: true, - valueGetter: (user) => { - return user ? `${user.first_name} ${user.last_name}` : ''; - }, - renderEditCell: (props) => ( - - ) - }, - { - headerName: 'Workgroup', - field: 'moped_workgroup', - width: 200, - valueGetter: (workgroup) => workgroup?.workgroup_name, +const useTeamNameLookup = (data) => + useMemo(() => { + if (!data) { + return {}; + } + return data.moped_users.reduce((obj, item) => { + obj[item.user_id] = `${item.first_name} ${item.last_name}`; + return obj; + }, {}); + }, [data]); + +const useRoleNameLookup = (data) => + useMemo(() => { + if (!data) { + return {}; + } + return data.moped_project_roles.reduce((obj, item) => { + obj[item.project_role_id] = item.project_role_name; + return obj; + }, {}); + }, [data]); + +const useColumns = ({ + data, + rowModesModel, + handleEditClick, + handleSaveClick, + handleCancelClick, + handleDeleteOpen, + classes, + teamNameLookup, + roleNameLookup +}) => + useMemo(() => { + return [ + { + headerName: 'Name', + field: 'moped_user', + width: 200, + editable: true, + valueGetter: (user) => { + return user ? `${user.first_name} ${user.last_name}` : ''; }, - { - headerName: 'Role', - field: 'moped_proj_personnel_roles', - width: 200, - editable: true, - renderHeader: () => ( - - Role{" "} - - info_outline - - - ), - valueGetter: (roles) => { - if (roles.length === 0) { - return ''; - } - const roleNames = roles.map(role => role.moped_project_role.project_role_name); - return roleNames.join(', '); - }, - renderEditCell: (props) => ( - - ) + renderEditCell: (props) => ( + + ) }, - { - headerName: 'Notes', - field: 'notes', - width: 200, - editable: true, - renderEditCell: (props) => + { + headerName: 'Workgroup', + field: 'moped_workgroup', + width: 200, + valueGetter: (workgroup) => workgroup?.workgroup_name, + }, + { + headerName: 'Role', + field: 'moped_proj_personnel_roles', + width: 200, + editable: true, + renderHeader: () => ( + + Role{" "} + + info_outline + + + ), + valueGetter: (roles) => { + if (roles.length === 0) { + return ''; + } + const roleNames = roles.map(role => role.moped_project_role.project_role_name); + return roleNames.join(', '); }, - { - headerName: '', - field: 'edit', - hideable: false, - filterable: false, - sortable: false, - editable: false, - type: "actions", - getActions: ({ id }) => { - const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; - if (isInEditMode) { - return [ - } - label="Save" - sx={{ - color: "primary.main", - }} - onClick={handleSaveClick(id)} - />, - } - label="Cancel" - className="textPrimary" - onClick={handleCancelClick(id)} - color="inherit" - />, - ]; - } + renderEditCell: (props) => { + console.log('render edit cell', props); + return ( + + ); + } + }, + { + headerName: 'Notes', + field: 'notes', + width: 200, + editable: true, + renderEditCell: (props) => + }, + { + headerName: '', + field: 'edit', + hideable: false, + filterable: false, + sortable: false, + editable: false, + type: "actions", + getActions: ({ id }) => { + const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; + if (isInEditMode) { return [ } - label="Edit" - className="textPrimary" - onClick={handleEditClick(id)} - color="inherit" + icon={} + label="Save" + sx={{ + color: "primary.main", + }} + onClick={handleSaveClick(id)} />, } - label="Delete" - onClick={() => handleDeleteOpen(id)} + icon={} + label="Cancel" + className="textPrimary" + onClick={handleCancelClick(id, 'project_personnel_id')} color="inherit" />, ]; - }, - } - ] + } + return [ + } + label="Edit" + className="textPrimary" + onClick={handleEditClick(id)} + color="inherit" + />, + } + label="Delete" + onClick={() => handleDeleteOpen(id)} + color="inherit" + />, + ]; }, - [ - data, - rowModesModel, - handleEditClick, - handleSaveClick, - handleCancelClick, - handleDeleteOpen, - classes + } ] - ); - + }, + [ + data, + rowModesModel, + handleEditClick, + handleSaveClick, + handleCancelClick, + handleDeleteOpen, + classes, + teamNameLookup, + roleNameLookup + ] +); const ProjectTeamTableDataGridPro = ({ projectId }) => { const apiRef = useGridApiRef(); @@ -199,8 +231,8 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { } }, [data]); - - + const teamNameLookup = useTeamNameLookup(data); + const roleNameLookup = useRoleNameLookup(data); const onClickAddTeamMember = () => { console.log('add team member'); return setEditTeamMember({ project_id: projectId }); @@ -221,19 +253,50 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); }, [rowModesModel]); - const handleCancelClick = useCallback((id) => () => { - console.log('cancel click', id); - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); - }, [rowModesModel]); + const handleCancelClick = (id, tableId) => () => { + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View, ignoreModifications: true }, + }); + const editedRow = rows.find((row) => row[tableId] === id); + if (editedRow.isNew) { + setRows(rows.filter((row) => row.id !== id)); + } + }; const handleDeleteOpen = useCallback((id) => { console.log('delete open', id); setDeleteTeamMemberId(id); }, [deleteTeamMemberId]); - const processRowUpdate = useCallback((newRow, oldRow) => { - console.log('process row update', newRow, oldRow); - return newRow; + const processRowUpdate = useCallback((updatedRow, originalRow) => { + console.log('process row update', updatedRow, originalRow); + const updatedRowData = { ...updatedRow }; + delete updatedRowData.__typename; + + // tk: handle empty strings + // tk: handle if the row is new + + const hasRowChanged = !isEqual(updatedRow, originalRow); + if (!hasRowChanged) { + return Promise.resolve(updatedRow); + } else { + console.log('updatedRowData', updatedRowData); + return updateProjectPersonnel({ variables: updatedRowData }) + .then(() => refetch()) + .then(() => updatedRow) + .catch((error) => { + console.error(error.message); + }); + } + }, []); + + const handleProcessUpdateError = useCallback((error) => { + console.log('process row update error', error); + }, []); + + const handleTabKeyDown = useCallback((params, event) => { + console.log('tab key down', params, event); }, []); @@ -244,26 +307,41 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { handleSaveClick, handleCancelClick, handleDeleteOpen, - classes + classes, + teamNameLookup, + roleNameLookup }); if (loading || !data) return ; + console.log('dataGridColumns', dataGridColumns); + console.log('rows', rows); + console.log('data', data); + return ( - + + row.project_personnel_id} // Use project_personnel_id as the unique id + getRowId={(row) => row.project_personnel_id} + editMode="row" + rowModesModel={rowModesModel} + onRowModesModelChange={setRowModesModel} + processRowUpdate={processRowUpdate} + onProcessRowUpdateError={handleProcessUpdateError} disableRowSelectionOnClick - disableColumnMenu + toolbar + density="comfortable" getRowHeight={() => 'auto'} hideFooter + onCellKeyDown={handleTabKeyDown} localeText={{ noRowsLabel: 'No team members found' }} + disableColumnMenu loading={loading} slots={{ toolbar: ProjectTeamToolbar, @@ -275,7 +353,8 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { }, }} /> - + + ); }; diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js b/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js new file mode 100644 index 0000000000..e9d6882299 --- /dev/null +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js @@ -0,0 +1,92 @@ +import React from "react"; +import { + Autocomplete, + TextField, + FormHelperText, + FormControl, +} from "@mui/material"; +import { useGridApiContext } from "@mui/x-data-grid-pro"; + +const TeamAutocompleteComponent = ({ + id, + value, + field, + hasFocus, + nameLookup, + error, + name, +}) => { + const apiRef = useGridApiContext(); + const ref = React.useRef(null); + + React.useEffect(() => { + if (hasFocus) { + ref.current.focus(); + } + }, [hasFocus]); + + const handleChange = (event, newValue) => { + console.log("event", event); + console.log("newValue", newValue); + apiRef.current.setEditCellValue({ + id, + field, + value: newValue ?? null, + }); + }; + + console.log(nameLookup); + const options = Object.keys(nameLookup); + + const isOptionEqualToValue = (option, value) => { + if (option === value) { + return true; + } + + if (nameLookup[option] === value) { + return true; + } + + if (String(option) === String(value)) { + return true; + } + + return false; + }; + + const getOptionLabel = (option) => { + if (typeof option === 'string' && !nameLookup[option]) { + return option; + } + return nameLookup[option] || ''; + }; + + return ( + + isOptionEqualToValue(option, value)} + value={value} + sx={{ paddingTop: "8px" }} + onChange={handleChange} + renderInput={(params) => ( + + )} + /> + Required + + ); +}; + +export default TeamAutocompleteComponent; From 7b4f5b65caa0d593ac96e06f4e4ddaead65c2ea7 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Tue, 12 Nov 2024 00:37:54 -0700 Subject: [PATCH 14/76] get notes editing working --- .../ProjectTeamTableDataGridPro.js | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js index 6df765f109..e34387288d 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js @@ -112,12 +112,18 @@ const useColumns = ({
), - valueGetter: (roles) => { - if (roles.length === 0) { - return ''; - } - const roleNames = roles.map(role => role.moped_project_role.project_role_name); - return roleNames.join(', '); + valueGetter: (params) => { + const roles = params.value || []; + // Add null check and filter out deleted roles + if (!Array.isArray(roles)) return ''; + + const activeRoles = roles.filter(role => !role.is_deleted); + if (activeRoles.length === 0) return ''; + + return activeRoles + .map(role => role.moped_project_role?.project_role_name) + .filter(Boolean) // Remove any undefined values + .join(', '); }, renderEditCell: (props) => { console.log('render edit cell', props); @@ -271,25 +277,45 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { const processRowUpdate = useCallback((updatedRow, originalRow) => { console.log('process row update', updatedRow, originalRow); - const updatedRowData = { ...updatedRow }; - delete updatedRowData.__typename; + + // Get the original role IDs + const originalRoleIds = originalRow.moped_proj_personnel_roles + .filter(role => !role.is_deleted) + .map(role => role.id); + + // Get the new role IDs from the updated row + const newRoleIds = updatedRow.roleIds || []; - // tk: handle empty strings - // tk: handle if the row is new + // Format the data according to the mutation's expected structure + const variables = { + id: updatedRow.project_personnel_id, + updatePersonnelObject: { + notes: updatedRow.notes, + user_id: updatedRow.moped_user?.user_id, + }, + // IDs of roles to be deleted (roles that were in original but not in new) + deleteIds: originalRoleIds, + // New role assignments + addRolesObjects: newRoleIds.map(roleId => ({ + project_role_id: roleId, + project_personnel_id: updatedRow.project_personnel_id + })) + }; const hasRowChanged = !isEqual(updatedRow, originalRow); if (!hasRowChanged) { return Promise.resolve(updatedRow); } else { - console.log('updatedRowData', updatedRowData); - return updateProjectPersonnel({ variables: updatedRowData }) + console.log('mutation variables:', variables); + return updateProjectPersonnel({ variables }) .then(() => refetch()) .then(() => updatedRow) .catch((error) => { console.error(error.message); + throw error; // Re-throw the error to be handled by handleProcessRowUpdateError }); } - }, []); + }, [updateProjectPersonnel, refetch]); const handleProcessUpdateError = useCallback((error) => { console.log('process row update error', error); @@ -314,10 +340,6 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { if (loading || !data) return ; - console.log('dataGridColumns', dataGridColumns); - console.log('rows', rows); - console.log('data', data); - return ( From 63e6cff95b8b64aa31843cebd86c1fd3b773a110 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Tue, 12 Nov 2024 00:44:20 -0700 Subject: [PATCH 15/76] gets name editing --- .../ProjectTeamTableDataGridPro.js | 9 +++-- .../projectView/ProjectTeamRoleMultiselect.js | 37 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 moped-editor/src/views/projects/projectView/ProjectTeamRoleMultiselect.js diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js index e34387288d..effe19a307 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js @@ -286,16 +286,19 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { // Get the new role IDs from the updated row const newRoleIds = updatedRow.roleIds || []; + // Extract user_id properly - handle both string and object cases + const userId = typeof updatedRow.moped_user === 'string' + ? parseInt(updatedRow.moped_user) + : updatedRow.moped_user?.user_id; + // Format the data according to the mutation's expected structure const variables = { id: updatedRow.project_personnel_id, updatePersonnelObject: { notes: updatedRow.notes, - user_id: updatedRow.moped_user?.user_id, + user_id: userId, }, - // IDs of roles to be deleted (roles that were in original but not in new) deleteIds: originalRoleIds, - // New role assignments addRolesObjects: newRoleIds.map(roleId => ({ project_role_id: roleId, project_personnel_id: updatedRow.project_personnel_id diff --git a/moped-editor/src/views/projects/projectView/ProjectTeamRoleMultiselect.js b/moped-editor/src/views/projects/projectView/ProjectTeamRoleMultiselect.js new file mode 100644 index 0000000000..95b6271bcb --- /dev/null +++ b/moped-editor/src/views/projects/projectView/ProjectTeamRoleMultiselect.js @@ -0,0 +1,37 @@ +import React from "react"; +import { FormControl, FormHelperText } from "@mui/material"; +import Select from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import Checkbox from "@mui/material/Checkbox"; +import ListItemText from "@mui/material/ListItemText"; + +const ProjectTeamRoleMultiselect = ({ value, onChange, roles }) => { + return ( + + + Required + + ); +}; + +export default ProjectTeamRoleMultiselect; \ No newline at end of file From 99d5157a5a55e3031e29ab33a6b8e47bdb24e048 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Mon, 18 Nov 2024 23:06:56 -0700 Subject: [PATCH 16/76] wip: edit mostly working --- .../ProjectTeam/ProjectTeamRoleMultiselect.js | 1 + .../ProjectTeamTableDataGridPro.js | 117 ++++++++++++------ .../ProjectTeam/TeamAutocompleteComponent.js | 12 +- .../projects/projectView/ProjectTeamTable.js | 14 ++- 4 files changed, 96 insertions(+), 48 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js index b283dc2486..a13822e81f 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js @@ -31,6 +31,7 @@ const useStyles = makeStyles((theme) => ({ const ProjectTeamRoleMultiselect = ({ id, field, roles, value }) => { const [selectedValues, setSelectedValues] = React.useState(Array.isArray(value) ? value : []); + console.log('selectedValues', selectedValues); const classes = useStyles(); const apiRef = useGridApiContext(); const ref = React.useRef(null); diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js index effe19a307..f9e3954c69 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js @@ -1,7 +1,7 @@ import * as React from 'react'; import { useState, useMemo, useEffect, useCallback } from "react"; import isEqual from "lodash/isEqual"; -import { Box, Icon, Link, CircularProgress, TextField, Autocomplete } from '@mui/material'; +import { Box, Icon, Link, CircularProgress, Typography, TextField } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; import { DataGridPro, GridRowModes, GridActionsCellItem, useGridApiRef } from '@mui/x-data-grid-pro'; @@ -112,21 +112,20 @@ const useColumns = ({ ), - valueGetter: (params) => { - const roles = params.value || []; - // Add null check and filter out deleted roles - if (!Array.isArray(roles)) return ''; + renderCell: (params) => { + // Filter out deleted roles and map to Typography components + const roleElements = params.row.moped_proj_personnel_roles + .filter(role => !role.is_deleted) + .map(role => ( + + {role.moped_project_role?.project_role_name} + + )); - const activeRoles = roles.filter(role => !role.is_deleted); - if (activeRoles.length === 0) return ''; - - return activeRoles - .map(role => role.moped_project_role?.project_role_name) - .filter(Boolean) // Remove any undefined values - .join(', '); + return {roleElements}; }, renderEditCell: (props) => { - console.log('render edit cell', props); + console.log('renderEditCell value', props.row); return ( { }, [deleteTeamMemberId]); const processRowUpdate = useCallback((updatedRow, originalRow) => { - console.log('process row update', updatedRow, originalRow); + console.log('process row update:', 'updatedRow', updatedRow, 'originalRow', originalRow); - // Get the original role IDs - const originalRoleIds = originalRow.moped_proj_personnel_roles - .filter(role => !role.is_deleted) - .map(role => role.id); + // Ensure project_personnel_id is an integer + const personnelId = parseInt(updatedRow.project_personnel_id); + console.log('personnelId', personnelId); + if (!personnelId) { + console.error('Invalid project_personnel_id:', updatedRow.project_personnel_id); + throw new Error('Invalid project_personnel_id'); + } - // Get the new role IDs from the updated row - const newRoleIds = updatedRow.roleIds || []; + // Extract user_id properly + let userId; + if (updatedRow.moped_user?.user_id) { + // Case: New selection from autocomplete + userId = parseInt(updatedRow.moped_user.user_id); + } else if (originalRow.moped_user?.user_id) { + // Case: No change to user + userId = parseInt(originalRow.moped_user.user_id); + } else { + console.error('Invalid user data:', updatedRow.moped_user); + throw new Error('Invalid user data'); + } + console.log('userId', userId); + + // Check if the roles have changed + const haveRolesChanged = !isEqual(updatedRow.moped_proj_personnel_roles, originalRow.moped_proj_personnel_roles); + console.log('haveRolesChanged', haveRolesChanged); - // Extract user_id properly - handle both string and object cases - const userId = typeof updatedRow.moped_user === 'string' - ? parseInt(updatedRow.moped_user) - : updatedRow.moped_user?.user_id; + let rolesToAdd = []; + let originalRoleIds = []; + let newRoleIds = []; + + if (haveRolesChanged) { + // Get the original role IDs + const originalRoleIds = (originalRow.moped_proj_personnel_roles || []) + .filter(role => !role.is_deleted) + .map(role => role.id); // Get the junction table IDs for deletion + console.log('originalRoleIds', originalRoleIds); + + // Get the new role IDs from roleIds + newRoleIds = (updatedRow.moped_proj_personnel_roles || []); + console.log('newRoleIds', newRoleIds); + + // Get the existing role IDs for comparison + const existingRoleIds = originalRow.moped_proj_personnel_roles + .map(role => role.project_role_id); + console.log('existingRoleIds', existingRoleIds); + + // Only add roles that don't already exist + rolesToAdd = newRoleIds.filter(id => !existingRoleIds.includes(id)); + console.log('rolesToAdd', rolesToAdd); + } + + // get notes + const notes = updatedRow.notes; + console.log('notes', notes); - // Format the data according to the mutation's expected structure const variables = { - id: updatedRow.project_personnel_id, + id: personnelId, updatePersonnelObject: { notes: updatedRow.notes, - user_id: userId, + user_id: userId }, deleteIds: originalRoleIds, addRolesObjects: newRoleIds.map(roleId => ({ project_role_id: roleId, - project_personnel_id: updatedRow.project_personnel_id + project_personnel_id: personnelId })) }; + console.log('Mutation variables:', variables); + const hasRowChanged = !isEqual(updatedRow, originalRow); if (!hasRowChanged) { return Promise.resolve(updatedRow); - } else { - console.log('mutation variables:', variables); - return updateProjectPersonnel({ variables }) - .then(() => refetch()) - .then(() => updatedRow) - .catch((error) => { - console.error(error.message); - throw error; // Re-throw the error to be handled by handleProcessRowUpdateError - }); } + + return updateProjectPersonnel({ variables }) + .then(() => refetch()) + .then(() => updatedRow) + .catch((error) => { + console.error('Mutation error:', error); + throw error; + }); }, [updateProjectPersonnel, refetch]); const handleProcessUpdateError = useCallback((error) => { @@ -325,7 +366,7 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { }, []); const handleTabKeyDown = useCallback((params, event) => { - console.log('tab key down', params, event); + // console.log('tab key down', params, event); }, []); diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js b/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js index e9d6882299..a072605e5a 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js @@ -26,8 +26,6 @@ const TeamAutocompleteComponent = ({ }, [hasFocus]); const handleChange = (event, newValue) => { - console.log("event", event); - console.log("newValue", newValue); apiRef.current.setEditCellValue({ id, field, @@ -35,7 +33,6 @@ const TeamAutocompleteComponent = ({ }); }; - console.log(nameLookup); const options = Object.keys(nameLookup); const isOptionEqualToValue = (option, value) => { @@ -74,7 +71,14 @@ const TeamAutocompleteComponent = ({ isOptionEqualToValue={(option, value) => isOptionEqualToValue(option, value)} value={value} sx={{ paddingTop: "8px" }} - onChange={handleChange} + onChange={(event, newValue) => { + handleChange(event, newValue ? { + user_id: newValue.user_id, + first_name: newValue.first_name, + last_name: newValue.last_name, + // Include other necessary user fields + } : null); + }} renderInput={(params) => ( { newData, oldData, }); - return updateProjectPersonnel({ - variables: { + const variables = { updatePersonnelObject: payload, id: project_personnel_id, - deleteIds: roleIdsToDelete, - addRolesObjects: rolesToAdd, - }, - }).then(() => refetch()); + deleteIds: roleIdsToDelete, + addRolesObjects: rolesToAdd, + } + + console.log('variables', variables); + + return updateProjectPersonnel({ variables }).then(() => refetch()); }, onRowDelete: (oldData) => { const id = oldData.project_personnel_id; From 8e979dd8994370d4ac6522415f4e6a3b85108de4 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Tue, 19 Nov 2024 14:36:57 -0700 Subject: [PATCH 17/76] fix issue with selectedvalues empty array --- .../ProjectTeam/ProjectTeamRoleMultiselect.js | 9 +++++++-- .../ProjectTeam/ProjectTeamTableDataGridPro.js | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js index a13822e81f..1431d81bc9 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js @@ -30,8 +30,13 @@ const useStyles = makeStyles((theme) => ({ const ProjectTeamRoleMultiselect = ({ id, field, roles, value }) => { - const [selectedValues, setSelectedValues] = React.useState(Array.isArray(value) ? value : []); - console.log('selectedValues', selectedValues); + console.log('value', value); + const rolesArray = value.map((role) => role.project_role_id); + console.log('rolesArray', rolesArray); + const [selectedValues, setSelectedValues] = React.useState(rolesArray || []); + + + const classes = useStyles(); const apiRef = useGridApiContext(); const ref = React.useRef(null); diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js index f9e3954c69..ba27153014 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js @@ -129,7 +129,7 @@ const useColumns = ({ return ( ); From f34d7b6c9a8b71ffb55f2a6cbe86bcde17cbac14 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Tue, 19 Nov 2024 20:38:23 -0700 Subject: [PATCH 18/76] get role updates and note updates working together --- .../ProjectTeam/ProjectTeamRoleMultiselect.js | 10 +- .../ProjectTeamTableDataGridPro.js | 124 +++++++++++++----- 2 files changed, 95 insertions(+), 39 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js index 1431d81bc9..437817344b 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js @@ -30,20 +30,18 @@ const useStyles = makeStyles((theme) => ({ const ProjectTeamRoleMultiselect = ({ id, field, roles, value }) => { - console.log('value', value); const rolesArray = value.map((role) => role.project_role_id); - console.log('rolesArray', rolesArray); const [selectedValues, setSelectedValues] = React.useState(rolesArray || []); - - const classes = useStyles(); const apiRef = useGridApiContext(); const ref = React.useRef(null); const handleChange = (event) => { - const newValue = event.target.value; - setSelectedValues(newValue); + const valueIds = event.target.value; + const newValue = roles.filter((role) => valueIds.includes(role.project_role_id)); + const rolesArray = newValue.map((role) => role.project_role_id); + setSelectedValues(rolesArray); apiRef.current.setEditCellValue({ id, field, diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js index ba27153014..e5622cb2c5 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js @@ -125,7 +125,7 @@ const useColumns = ({ return {roleElements}; }, renderEditCell: (props) => { - console.log('renderEditCell value', props.row); + console.log('renderEditCell props value', props); return ( { const teamNameLookup = useTeamNameLookup(data); const roleNameLookup = useRoleNameLookup(data); + + /** + * Construct a moped_project_personnel object that can be passed to an insert mutation + * @param {Object} newData - a table row object with { moped_user, notes, roleIds } + * @param {integer} projectId - the project ID + * @return {Ojbect} a moped_project_personnel object: { user_id, notes, moped_proj_personnel_roles: { project_role_id } } + */ +const getNewPersonnelPayload = ({ + newData: { + moped_user: { user_id }, + notes, + roleIds, + }, + projectId: project_id, +}) => { + const payload = { notes, project_id, user_id }; + const personnelRoles = roleIds.map((roleId) => ({ + project_role_id: roleId, + })); + payload["moped_proj_personnel_roles"] = { data: personnelRoles }; + return payload; +}; + +/** + * Construct a moped_project_personnel object that can be passed to an update mutation + * @param {Object} newData - a table row object with { moped_user, notes, roleIds } + * @param {integer} projectId - the project ID + * @return {Ojbect} a moped_project_personnel object: { user_id, notes } <- observe that `moped_proj_personnel_roles` + * is handled separately + */ +const getEditPersonnelPayload = (newData) => { + // and the new values + console.log('getEditPersonnelPayload', newData); + const { + moped_user: { user_id }, + notes, + } = newData; + return { user_id, notes }; +}; + +/** + * Constructs payload objects for adding and removing moped_proj_personnel_roles + * @param {Object} newData - a table row object with the new values + * @param {Object} oldData - a table row object with the old values + * @return {[[Object], [Int]]} - an array of new personnel role objects, and an array of existing + * personnel role objects to delete + */ +const getEditRolesPayload = (newData, oldData) => { + console.log('getEditRolesPayload', newData, oldData); + + const { project_personnel_id } = oldData; + + // get an array of moped_proj_personnel_roles IDs to delete + const projRoleIdsToDelete = oldData.moped_proj_personnel_roles + .filter((projRole) => { + const roleId = projRole.moped_project_role.project_role_id; + return !newData.roleIds.includes(roleId); + }) + .map((projRole) => projRole.id); + + // contruct an array of new moped_proj_personnel_roles objects + const existingRoleIds = oldData.moped_proj_personnel_roles.map( + ({ moped_project_role }) => moped_project_role.project_role_id + ); + const roleIdsToAdd = newData.roleIds.filter( + (roleId) => !existingRoleIds.includes(roleId) + ); + const rolesToAddPayload = roleIdsToAdd.map((newRoleId) => ({ + project_personnel_id, + project_role_id: newRoleId, + })); + console.log('rolesToAddPayload', rolesToAddPayload); + console.log('projRoleIdsToDelete', projRoleIdsToDelete); + return [rolesToAddPayload, projRoleIdsToDelete]; +}; + const onClickAddTeamMember = () => { console.log('add team member'); return setEditTeamMember({ project_id: projectId }); @@ -299,34 +375,22 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { } console.log('userId', userId); + // Update roleIds to be in sync with moped_proj_personnel_roles + console.log('updatedRow.moped_proj_personnel_roles', updatedRow.moped_proj_personnel_roles); + updatedRow.roleIds = updatedRow.moped_proj_personnel_roles.map( + (role) => role.project_role_id + ); + console.log('Updated roleIds:', updatedRow.roleIds); + // Check if the roles have changed const haveRolesChanged = !isEqual(updatedRow.moped_proj_personnel_roles, originalRow.moped_proj_personnel_roles); console.log('haveRolesChanged', haveRolesChanged); - let rolesToAdd = []; - let originalRoleIds = []; - let newRoleIds = []; - - if (haveRolesChanged) { - // Get the original role IDs - const originalRoleIds = (originalRow.moped_proj_personnel_roles || []) - .filter(role => !role.is_deleted) - .map(role => role.id); // Get the junction table IDs for deletion - console.log('originalRoleIds', originalRoleIds); - - // Get the new role IDs from roleIds - newRoleIds = (updatedRow.moped_proj_personnel_roles || []); - console.log('newRoleIds', newRoleIds); + const payload = getEditPersonnelPayload(updatedRow); + const [rolesToAdd, roleIdsToDelete] = getEditRolesPayload(updatedRow, originalRow); - // Get the existing role IDs for comparison - const existingRoleIds = originalRow.moped_proj_personnel_roles - .map(role => role.project_role_id); - console.log('existingRoleIds', existingRoleIds); - - // Only add roles that don't already exist - rolesToAdd = newRoleIds.filter(id => !existingRoleIds.includes(id)); - console.log('rolesToAdd', rolesToAdd); - } + console.log('rolesToAdd', rolesToAdd); + console.log('roleIdsToDelete', roleIdsToDelete); // get notes const notes = updatedRow.notes; @@ -334,15 +398,9 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { const variables = { id: personnelId, - updatePersonnelObject: { - notes: updatedRow.notes, - user_id: userId - }, - deleteIds: originalRoleIds, - addRolesObjects: newRoleIds.map(roleId => ({ - project_role_id: roleId, - project_personnel_id: personnelId - })) + updatePersonnelObject: payload, + deleteIds: roleIdsToDelete, + addRolesObjects: rolesToAdd }; console.log('Mutation variables:', variables); From 84505579b3612c4732f17ff8e6aea920ae0faf93 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Wed, 20 Nov 2024 14:27:04 -0700 Subject: [PATCH 19/76] get names edit working --- .../ProjectTeamTableDataGridPro.js | 53 +++++++++++-------- .../ProjectTeam/TeamAutocompleteComponent.js | 27 ++++------ 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js index e5622cb2c5..5cb90cd79f 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js @@ -81,14 +81,17 @@ const useColumns = ({ valueGetter: (user) => { return user ? `${user.first_name} ${user.last_name}` : ''; }, - renderEditCell: (props) => ( + renderEditCell: (props) => { + return( - ) - }, + /> + ); + } + }, { headerName: 'Workgroup', field: 'moped_workgroup', @@ -125,7 +128,6 @@ const useColumns = ({ return {roleElements}; }, renderEditCell: (props) => { - console.log('renderEditCell props value', props); return ( { setDeleteTeamMemberId(id); }, [deleteTeamMemberId]); - const processRowUpdate = useCallback((updatedRow, originalRow) => { - console.log('process row update:', 'updatedRow', updatedRow, 'originalRow', originalRow); + const processRowUpdate = useCallback((updatedRow, originalRow, params, data) => { + console.log('process row update:', 'updatedRow', updatedRow, 'originalRow', originalRow, 'params', params); // Ensure project_personnel_id is an integer const personnelId = parseInt(updatedRow.project_personnel_id); - console.log('personnelId', personnelId); + if (!personnelId) { console.error('Invalid project_personnel_id:', updatedRow.project_personnel_id); throw new Error('Invalid project_personnel_id'); @@ -363,34 +365,39 @@ const getEditRolesPayload = (newData, oldData) => { // Extract user_id properly let userId; - if (updatedRow.moped_user?.user_id) { - // Case: New selection from autocomplete - userId = parseInt(updatedRow.moped_user.user_id); - } else if (originalRow.moped_user?.user_id) { - // Case: No change to user - userId = parseInt(originalRow.moped_user.user_id); + const userObject = data.moped_users.find(user => { + if (typeof updatedRow.moped_user === 'string') { + return `${user.first_name} ${user.last_name}` === updatedRow.moped_user; + } else { + return user.user_id === updatedRow.moped_user.user_id; + } + }); + + if (userObject) { + userId = userObject.user_id; + updatedRow.moped_user = userObject; // Update with full user object } else { console.error('Invalid user data:', updatedRow.moped_user); throw new Error('Invalid user data'); } - console.log('userId', userId); + // Update roleIds to be in sync with moped_proj_personnel_roles - console.log('updatedRow.moped_proj_personnel_roles', updatedRow.moped_proj_personnel_roles); updatedRow.roleIds = updatedRow.moped_proj_personnel_roles.map( (role) => role.project_role_id ); - console.log('Updated roleIds:', updatedRow.roleIds); - // Check if the roles have changed - const haveRolesChanged = !isEqual(updatedRow.moped_proj_personnel_roles, originalRow.moped_proj_personnel_roles); - console.log('haveRolesChanged', haveRolesChanged); + // // Check if the roles have changed + // const haveRolesChanged = !isEqual(updatedRow.moped_proj_personnel_roles, originalRow.moped_proj_personnel_roles); const payload = getEditPersonnelPayload(updatedRow); const [rolesToAdd, roleIdsToDelete] = getEditRolesPayload(updatedRow, originalRow); - console.log('rolesToAdd', rolesToAdd); - console.log('roleIdsToDelete', roleIdsToDelete); + // get update name + payload.user_id = userId; + + const fullMopedUserObject = data.moped_users.find(user => user.user_id === userId); + updatedRow.moped_user = fullMopedUserObject; // get notes const notes = updatedRow.notes; @@ -456,7 +463,7 @@ const getEditRolesPayload = (newData, oldData) => { editMode="row" rowModesModel={rowModesModel} onRowModesModelChange={setRowModesModel} - processRowUpdate={processRowUpdate} + processRowUpdate={(updatedRow, originalRow, params) => processRowUpdate(updatedRow, originalRow, params, data)} onProcessRowUpdateError={handleProcessUpdateError} disableRowSelectionOnClick toolbar diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js b/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js index a072605e5a..a79ff808e2 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js @@ -26,29 +26,25 @@ const TeamAutocompleteComponent = ({ }, [hasFocus]); const handleChange = (event, newValue) => { + const personnelValue = nameLookup[newValue]; apiRef.current.setEditCellValue({ id, field, - value: newValue ?? null, + value: personnelValue ?? null, }); }; const options = Object.keys(nameLookup); const isOptionEqualToValue = (option, value) => { - if (option === value) { - return true; - } - - if (nameLookup[option] === value) { - return true; - } - - if (String(option) === String(value)) { + + const keyForValue = Object.keys(nameLookup).find(key => nameLookup[key] === value); + + if (option === keyForValue) { return true; + } else { + return false; } - - return false; }; const getOptionLabel = (option) => { @@ -72,12 +68,7 @@ const TeamAutocompleteComponent = ({ value={value} sx={{ paddingTop: "8px" }} onChange={(event, newValue) => { - handleChange(event, newValue ? { - user_id: newValue.user_id, - first_name: newValue.first_name, - last_name: newValue.last_name, - // Include other necessary user fields - } : null); + handleChange(event, newValue); }} renderInput={(params) => ( Date: Wed, 20 Nov 2024 15:48:37 -0700 Subject: [PATCH 20/76] handle different cases for isOptionEqualToValue --- .../ProjectTeam/TeamAutocompleteComponent.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js b/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js index a79ff808e2..df211cb296 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js @@ -37,10 +37,16 @@ const TeamAutocompleteComponent = ({ const options = Object.keys(nameLookup); const isOptionEqualToValue = (option, value) => { + // if the value is a number, use the idFromValue nameLookup to find if option is equal to Value + // If the value is an object, use the user_id to find if option is equal to Value + let idFromValue; + if (typeof value === 'string') { + idFromValue = Object.keys(nameLookup).find(key => nameLookup[key] === value); + } else if (typeof value === 'object') { + idFromValue = value.user_id + } - const keyForValue = Object.keys(nameLookup).find(key => nameLookup[key] === value); - - if (option === keyForValue) { + if (Number(option) === Number(idFromValue)) { return true; } else { return false; From 4f9deb356283e6613f5a31fce83ba765e171b9d2 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Thu, 21 Nov 2024 17:34:51 -0700 Subject: [PATCH 21/76] add delete personnel mutation --- .../projectView/ProjectTeam/ProjectTeamTableDataGridPro.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js index 5cb90cd79f..bc13fb3cdd 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js @@ -350,6 +350,9 @@ const getEditRolesPayload = (newData, oldData) => { const handleDeleteOpen = useCallback((id) => { console.log('delete open', id); setDeleteTeamMemberId(id); + return deleteProjectPersonnel({ + variables: { id }, + }).then(() => refetch()); }, [deleteTeamMemberId]); const processRowUpdate = useCallback((updatedRow, originalRow, params, data) => { From 35d8304a7b6db5b6d539eb20b7bd6b4acac5ed09 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Fri, 22 Nov 2024 01:14:26 -0700 Subject: [PATCH 22/76] add insert functionality --- .../ProjectTeamTableDataGridPro.js | 63 +++++++++++++++---- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js index bc13fb3cdd..a11e286c44 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js @@ -1,6 +1,8 @@ import * as React from 'react'; import { useState, useMemo, useEffect, useCallback } from "react"; import isEqual from "lodash/isEqual"; +import { v4 as uuidv4 } from "uuid"; + import { Box, Icon, Link, CircularProgress, Typography, TextField } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; @@ -215,7 +217,7 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { fetchPolicy: "no-cache", }); - const [addProjectPersonnel] = useMutation(INSERT_PROJECT_PERSONNEL); + const [insertProjectPersonnel] = useMutation(INSERT_PROJECT_PERSONNEL); const [updateProjectPersonnel] = useMutation(UPDATE_PROJECT_PERSONNEL); const [deleteProjectPersonnel] = useMutation(DELETE_PROJECT_PERSONNEL); @@ -245,7 +247,7 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { * Construct a moped_project_personnel object that can be passed to an insert mutation * @param {Object} newData - a table row object with { moped_user, notes, roleIds } * @param {integer} projectId - the project ID - * @return {Ojbect} a moped_project_personnel object: { user_id, notes, moped_proj_personnel_roles: { project_role_id } } + * @return {Object} a moped_project_personnel object: { user_id, notes, moped_proj_personnel_roles: { project_role_id } } */ const getNewPersonnelPayload = ({ newData: { @@ -267,7 +269,7 @@ const getNewPersonnelPayload = ({ * Construct a moped_project_personnel object that can be passed to an update mutation * @param {Object} newData - a table row object with { moped_user, notes, roleIds } * @param {integer} projectId - the project ID - * @return {Ojbect} a moped_project_personnel object: { user_id, notes } <- observe that `moped_proj_personnel_roles` + * @return {Object} a moped_project_personnel object: { user_id, notes } <- observe that `moped_proj_personnel_roles` * is handled separately */ const getEditPersonnelPayload = (newData) => { @@ -318,7 +320,24 @@ const getEditRolesPayload = (newData, oldData) => { const onClickAddTeamMember = () => { console.log('add team member'); - return setEditTeamMember({ project_id: projectId }); + const id = uuidv4(); + setRows((oldRows) => [ + { + id, + name: null, + moped_workgroup: null, + moped_proj_personnel_roles: [], + notes: null, + isNew: true, + roleIds: [], + project_personnel_id: id, + }, + ...oldRows, + ]); + setRowModesModel((oldModel) => ({ + ...oldModel, + [id]: { mode: GridRowModes.Edit, fieldToFocus: "project_personnel_id" }, + })); } const onClickEditTeamMember = (projectPersonnelId) => { @@ -356,17 +375,9 @@ const getEditRolesPayload = (newData, oldData) => { }, [deleteTeamMemberId]); const processRowUpdate = useCallback((updatedRow, originalRow, params, data) => { - console.log('process row update:', 'updatedRow', updatedRow, 'originalRow', originalRow, 'params', params); - - // Ensure project_personnel_id is an integer - const personnelId = parseInt(updatedRow.project_personnel_id); - if (!personnelId) { - console.error('Invalid project_personnel_id:', updatedRow.project_personnel_id); - throw new Error('Invalid project_personnel_id'); - } + console.log('updatedRow.isNew', updatedRow.isNew); - // Extract user_id properly let userId; const userObject = data.moped_users.find(user => { if (typeof updatedRow.moped_user === 'string') { @@ -384,6 +395,31 @@ const getEditRolesPayload = (newData, oldData) => { throw new Error('Invalid user data'); } + if (updatedRow.isNew) { + const payload = getNewPersonnelPayload({ newData: updatedRow, projectId }); + + return insertProjectPersonnel({ + variables: { + object: payload, + }, + }) + .then(() => refetch()) + .then(() => updatedRow) + .catch((error) => { + console.error('Mutation error:', error); + throw error; + }); + } else { + // Ensure project_personnel_id is an integer + const personnelId = parseInt(updatedRow.project_personnel_id); + + if (!personnelId) { + console.error('Invalid project_personnel_id:', updatedRow.project_personnel_id); + throw new Error('Invalid project_personnel_id'); + } + + + // Update roleIds to be in sync with moped_proj_personnel_roles updatedRow.roleIds = updatedRow.moped_proj_personnel_roles.map( @@ -427,6 +463,7 @@ const getEditRolesPayload = (newData, oldData) => { console.error('Mutation error:', error); throw error; }); + } }, [updateProjectPersonnel, refetch]); const handleProcessUpdateError = useCallback((error) => { From ccdb982b03a82d810231100fc70093907555d36d Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Mon, 2 Dec 2024 16:21:13 -0600 Subject: [PATCH 23/76] update styles and clean up file organization --- .../{ => ProjectTeam}/ProjectTeam.js | 4 +- .../ProjectTeam/ProjectTeamRoleMultiselect.js | 4 - ...ableDataGridPro.js => ProjectTeamTable.js} | 70 +--- .../ProjectTeam/ProjectTeamToolbar.js | 4 +- .../projects/projectView/ProjectTeamTable.js | 391 ------------------ .../views/projects/projectView/ProjectView.js | 2 +- 6 files changed, 25 insertions(+), 450 deletions(-) rename moped-editor/src/views/projects/projectView/{ => ProjectTeam}/ProjectTeam.js (69%) rename moped-editor/src/views/projects/projectView/ProjectTeam/{ProjectTeamTableDataGridPro.js => ProjectTeamTable.js} (90%) delete mode 100644 moped-editor/src/views/projects/projectView/ProjectTeamTable.js diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeam.js similarity index 69% rename from moped-editor/src/views/projects/projectView/ProjectTeam.js rename to moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeam.js index 5272329a32..ac7a1abca3 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeam.js @@ -1,6 +1,5 @@ import React from "react"; -import ProjectTeamTable from "./ProjectTeamTable"; -import ProjectTeamTableDataGridPro from "./ProjectTeam/ProjectTeamTableDataGridPro"; +import ProjectTeamTableDataGridPro from "./ProjectTeamTable"; import { useParams } from "react-router-dom"; import { CardContent, Grid } from "@mui/material"; @@ -12,7 +11,6 @@ const ProjectTeam = () => { - diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js index 437817344b..cb40e8d6f1 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js @@ -26,16 +26,12 @@ const useStyles = makeStyles((theme) => ({ }, })); - - - const ProjectTeamRoleMultiselect = ({ id, field, roles, value }) => { const rolesArray = value.map((role) => role.project_role_id); const [selectedValues, setSelectedValues] = React.useState(rolesArray || []); const classes = useStyles(); const apiRef = useGridApiContext(); - const ref = React.useRef(null); const handleChange = (event) => { const valueIds = event.target.value; diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js similarity index 90% rename from moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js rename to moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js index a11e286c44..2545c69556 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTableDataGridPro.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js @@ -5,10 +5,12 @@ import { v4 as uuidv4 } from "uuid"; import { Box, Icon, Link, CircularProgress, Typography, TextField } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; +import { EditOutlined as EditOutlinedIcon, DeleteOutline as DeleteOutlineIcon, Check as CheckIcon, Close as CloseIcon } from '@mui/icons-material'; import { DataGridPro, GridRowModes, GridActionsCellItem, useGridApiRef } from '@mui/x-data-grid-pro'; import { useQuery, useMutation } from "@apollo/client"; -import theme from "src/theme"; +import ApolloErrorHandler from 'src/components/ApolloErrorHandler'; + import { defaultEditColumnIconStyle } from "src/utils/dataGridHelpers"; import { TEAM_QUERY, @@ -19,25 +21,21 @@ import { import dataGridProStyleOverrides from 'src/styles/dataGridProStylesOverrides'; import ProjectTeamToolbar from './ProjectTeamToolbar'; import ProjectTeamRoleMultiselect from './ProjectTeamRoleMultiselect'; - -import { EditOutlined as EditOutlinedIcon, DeleteOutline as DeleteOutlineIcon, Check as CheckIcon, Close as CloseIcon } from '@mui/icons-material'; - -import { useUser } from 'src/auth/user'; - -import LookupAutocompleteComponent from 'src/components/DataGridPro/LookupAutocompleteComponent'; import TeamAutocompleteComponent from './TeamAutocompleteComponent'; import DataGridTextField from 'src/components/DataGridPro/DataGridTextField'; -import ApolloErrorHandler from 'src/components/ApolloErrorHandler'; const useStyles = makeStyles((theme) => ({ infoIcon: { - fontSize: "1.25rem", + fontSize: "1rem", verticalAlign: "sub", color: theme.palette.text.primary, "&:hover": { color: theme.palette.primary.main, }, }, + roleHeader: { + fontWeight: 500, + }, })); const useTeamNameLookup = (data) => @@ -78,7 +76,7 @@ const useColumns = ({ { headerName: 'Name', field: 'moped_user', - width: 200, + width: 250, editable: true, valueGetter: (user) => { return user ? `${user.first_name} ${user.last_name}` : ''; @@ -106,7 +104,7 @@ const useColumns = ({ width: 200, editable: true, renderHeader: () => ( - +
Role{" "} info_outline - +
), renderCell: (params) => { // Filter out deleted roles and map to Typography components const roleElements = params.row.moped_proj_personnel_roles .filter(role => !role.is_deleted) .map(role => ( - + {role.moped_project_role?.project_role_name} )); @@ -137,6 +135,11 @@ const useColumns = ({ roles={data.moped_project_roles} /> ); + }, + preProcessEditCellProps: (params) => { + // Enforce required field + const hasError = !params.props.value || params.props.value.length === 0; + return { ...params.props, error: hasError }; } }, { @@ -207,10 +210,9 @@ const useColumns = ({ ] ); -const ProjectTeamTableDataGridPro = ({ projectId }) => { +const ProjectTeamTable = ({ projectId }) => { const apiRef = useGridApiRef(); const classes = useStyles(); - const { user } = useUser(); const { loading, error, data, refetch } = useQuery(TEAM_QUERY, { variables: { projectId }, @@ -221,11 +223,9 @@ const ProjectTeamTableDataGridPro = ({ projectId }) => { const [updateProjectPersonnel] = useMutation(UPDATE_PROJECT_PERSONNEL); const [deleteProjectPersonnel] = useMutation(DELETE_PROJECT_PERSONNEL); - const [editTeamMember, setEditTeamMember] = useState(null); - const [deleteTeamMemberId, setDeleteTeamMemberId] = useState(null); - const [rows, setRows] = useState([]); const [rowModesModel, setRowModesModel] = useState({}); + const [deleteTeamMemberId, setDeleteTeamMemberId] = useState(null); useEffect(() => { if (data?.moped_project_by_pk?.moped_proj_personnel?.length > 0) { @@ -273,8 +273,6 @@ const getNewPersonnelPayload = ({ * is handled separately */ const getEditPersonnelPayload = (newData) => { - // and the new values - console.log('getEditPersonnelPayload', newData); const { moped_user: { user_id }, notes, @@ -290,8 +288,6 @@ const getEditPersonnelPayload = (newData) => { * personnel role objects to delete */ const getEditRolesPayload = (newData, oldData) => { - console.log('getEditRolesPayload', newData, oldData); - const { project_personnel_id } = oldData; // get an array of moped_proj_personnel_roles IDs to delete @@ -302,7 +298,7 @@ const getEditRolesPayload = (newData, oldData) => { }) .map((projRole) => projRole.id); - // contruct an array of new moped_proj_personnel_roles objects + // construct an array of new moped_proj_personnel_roles objects const existingRoleIds = oldData.moped_proj_personnel_roles.map( ({ moped_project_role }) => moped_project_role.project_role_id ); @@ -313,13 +309,10 @@ const getEditRolesPayload = (newData, oldData) => { project_personnel_id, project_role_id: newRoleId, })); - console.log('rolesToAddPayload', rolesToAddPayload); - console.log('projRoleIdsToDelete', projRoleIdsToDelete); return [rolesToAddPayload, projRoleIdsToDelete]; }; const onClickAddTeamMember = () => { - console.log('add team member'); const id = uuidv4(); setRows((oldRows) => [ { @@ -340,18 +333,11 @@ const getEditRolesPayload = (newData, oldData) => { })); } - const onClickEditTeamMember = (projectPersonnelId) => { - console.log('edit team member', projectPersonnelId); - return setEditTeamMember({ project_personnel_id: projectPersonnelId }); - } - const handleEditClick = useCallback((id) => () => { - console.log('edit click', id); setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); }, [rowModesModel]); const handleSaveClick = useCallback((id) => () => { - console.log('save click', id); setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); }, [rowModesModel]); @@ -367,7 +353,6 @@ const getEditRolesPayload = (newData, oldData) => { }; const handleDeleteOpen = useCallback((id) => { - console.log('delete open', id); setDeleteTeamMemberId(id); return deleteProjectPersonnel({ variables: { id }, @@ -375,9 +360,6 @@ const getEditRolesPayload = (newData, oldData) => { }, [deleteTeamMemberId]); const processRowUpdate = useCallback((updatedRow, originalRow, params, data) => { - - console.log('updatedRow.isNew', updatedRow.isNew); - let userId; const userObject = data.moped_users.find(user => { if (typeof updatedRow.moped_user === 'string') { @@ -440,7 +422,6 @@ const getEditRolesPayload = (newData, oldData) => { // get notes const notes = updatedRow.notes; - console.log('notes', notes); const variables = { id: personnelId, @@ -449,8 +430,6 @@ const getEditRolesPayload = (newData, oldData) => { addRolesObjects: rolesToAdd }; - console.log('Mutation variables:', variables); - const hasRowChanged = !isEqual(updatedRow, originalRow); if (!hasRowChanged) { return Promise.resolve(updatedRow); @@ -467,14 +446,9 @@ const getEditRolesPayload = (newData, oldData) => { }, [updateProjectPersonnel, refetch]); const handleProcessUpdateError = useCallback((error) => { - console.log('process row update error', error); + console.error('process row update error', error); }, []); - const handleTabKeyDown = useCallback((params, event) => { - // console.log('tab key down', params, event); - }, []); - - const dataGridColumns = useColumns({ data, rowModesModel, @@ -510,13 +484,13 @@ const getEditRolesPayload = (newData, oldData) => { density="comfortable" getRowHeight={() => 'auto'} hideFooter - onCellKeyDown={handleTabKeyDown} localeText={{ noRowsLabel: 'No team members found' }} disableColumnMenu loading={loading} slots={{ toolbar: ProjectTeamToolbar, }} + initialState={{ pinnedColumns: { right: ["edit"] } }} slotProps={{ toolbar: { addAction: onClickAddTeamMember, @@ -529,4 +503,4 @@ const getEditRolesPayload = (newData, oldData) => { ); }; -export default ProjectTeamTableDataGridPro; +export default ProjectTeamTable; diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamToolbar.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamToolbar.js index 7a9203c727..ab914cb9af 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamToolbar.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamToolbar.js @@ -1,10 +1,8 @@ import { Box, Typography } from "@mui/material"; -import ButtonDropdownMenu from "src/components/ButtonDropdownMenu"; import { Button } from "@mui/material"; import AddCircleIcon from '@mui/icons-material/AddCircle'; -/** Custom toolbar title that resembles material table titles */ -const ProjectTeamToolbar = ({ addAction, setIsDialogOpen }) => ( +const ProjectTeamToolbar = ({ addAction }) => ( Project Team diff --git a/moped-editor/src/views/projects/projectView/ProjectTeamTable.js b/moped-editor/src/views/projects/projectView/ProjectTeamTable.js deleted file mode 100644 index 27ed1d3fde..0000000000 --- a/moped-editor/src/views/projects/projectView/ProjectTeamTable.js +++ /dev/null @@ -1,391 +0,0 @@ -import React, { useMemo } from "react"; -import { useQuery, useMutation } from "@apollo/client"; - -// Material -import { - Button, - CircularProgress, - Icon, - Link, - TextField, - Typography, - FormControl, - FormHelperText, -} from "@mui/material"; -import { - AddCircle as AddCircleIcon, - DeleteOutline as DeleteOutlineIcon, - EditOutlined as EditOutlinedIcon, -} from "@mui/icons-material"; -import MaterialTable, { - MTableEditRow, - MTableAction, - MTableToolbar, -} from "@material-table/core"; -import Autocomplete from '@mui/material/Autocomplete'; - -import typography from "../../../theme/typography"; - -// Error Handler -import ApolloErrorHandler from "../../../components/ApolloErrorHandler"; - -import { - TEAM_QUERY, - UPDATE_PROJECT_PERSONNEL, - INSERT_PROJECT_PERSONNEL, - DELETE_PROJECT_PERSONNEL, -} from "../../../queries/project"; - -import ProjectTeamRoleMultiselect from "./ProjectTeamRoleMultiselect"; -import makeStyles from '@mui/styles/makeStyles'; -import { getUserFullName } from "src/utils/userNames"; - -const useStyles = makeStyles((theme) => ({ - infoIcon: { - fontSize: "1.25rem", - verticalAlign: "sub", - color: theme.palette.text.primary, - "&:hover": { - color: theme.palette.primary.main, - }, - }, - inactiveUserText: { - fontStyle: "italic", - }, -})); - -/** - * Adds a `roleIds` property to each team member - which we'll use as the i/o for the - * moped_project_role multiselect - * @param {Array} projPersonnel - An array of of moped_proj_personnel objects - * @return {string} the same array, with a `roleIds` prop added to each object - */ -const usePersonnel = (projPersonnel) => - useMemo(() => { - if (!projPersonnel) return projPersonnel; - projPersonnel.forEach((person) => { - const roleIds = person.moped_proj_personnel_roles.map( - ({ moped_project_role }) => moped_project_role.project_role_id - ); - person.roleIds = roleIds; - }); - return projPersonnel; - }, [projPersonnel]); - -/** - * Construct a moped_project_personnel object that can be passed to an insert mutation - * @param {Object} newData - a table row object with { moped_user, notes, roleIds } - * @param {integer} projectId - the project ID - * @return {Ojbect} a moped_project_personnel object: { user_id, notes, moped_proj_personnel_roles: { project_role_id } } - */ -const getNewPersonnelPayload = ({ - newData: { - moped_user: { user_id }, - notes, - roleIds, - }, - projectId: project_id, -}) => { - const payload = { notes, project_id, user_id }; - const personnelRoles = roleIds.map((roleId) => ({ - project_role_id: roleId, - })); - payload["moped_proj_personnel_roles"] = { data: personnelRoles }; - return payload; -}; - -/** - * Construct a moped_project_personnel object that can be passed to an update mutation - * @param {Object} newData - a table row object with { moped_user, notes, roleIds } - * @param {integer} projectId - the project ID - * @return {Ojbect} a moped_project_personnel object: { user_id, notes } <- observe that `moped_proj_personnel_roles` - * is handled separately - */ -const getEditPersonnelPayload = ({ newData }) => { - // and the new values - const { - moped_user: { user_id }, - notes, - } = newData; - return { user_id, notes }; -}; - -/** - * Constructs payload objects for adding and removing moped_proj_personnel_roles - * @param {Object} newData - a table row object with the new values - * @param {Object} oldData - a table row object with the old values - * @return {[[Object], [Int]]} - an array of new personnel role objects, and an array of existing - * personnel role objects to delete - */ -const getEditRolesPayload = ({ newData, oldData }) => { - const { project_personnel_id } = oldData; - - // get an array of moped_proj_personnel_roles IDs to delete - const projRoleIdsToDelete = oldData.moped_proj_personnel_roles - .filter((projRole) => { - const roleId = projRole.moped_project_role.project_role_id; - return !newData.roleIds.includes(roleId); - }) - .map((projRole) => projRole.id); - - // contruct an array of new moped_proj_personnel_roles objects - const existingRoleIds = oldData.moped_proj_personnel_roles.map( - ({ moped_project_role }) => moped_project_role.project_role_id - ); - const roleIdsToAdd = newData.roleIds.filter( - (roleId) => !existingRoleIds.includes(roleId) - ); - const rolesToAddPayload = roleIdsToAdd.map((newRoleId) => ({ - project_personnel_id, - project_role_id: newRoleId, - })); - return [rolesToAddPayload, projRoleIdsToDelete]; -}; - -const ProjectTeamTable = ({ projectId }) => { - const classes = useStyles(); - - const { loading, error, data, refetch } = useQuery(TEAM_QUERY, { - variables: { projectId }, - fetchPolicy: "no-cache", - }); - - const addActionRef = React.useRef(); - - const [insertProjectPersonnel] = useMutation(INSERT_PROJECT_PERSONNEL); - const [updateProjectPersonnel] = useMutation(UPDATE_PROJECT_PERSONNEL); - const [deleteProjecPersonnel] = useMutation(DELETE_PROJECT_PERSONNEL); - - const personnel = usePersonnel( - data?.moped_project_by_pk.moped_proj_personnel - ); - const roles = data?.moped_project_roles; - - const users = data?.moped_users; - - /** - * Column configuration for - */ - const columns = [ - { - title: "Name", - field: "moped_user", - render: (personnel) => { - const isDeleted = personnel?.moped_user.is_deleted; - const fullName = getUserFullName(personnel.moped_user); - - return isDeleted ? ( - {`${fullName} - Inactive`} - ) : ( - {fullName} - ); - }, - validate: (rowData) => !!rowData?.moped_user?.user_id, - editComponent: (props) => { - const isNewRow = !props.rowData?.project_personnel_id; - let userOptions = users; - if (isNewRow) { - // prevent same user from being added twice - const existingPersonnelUserIds = personnel.map( - (pers) => pers.moped_user.user_id - ); - userOptions = users.filter( - (user) => !existingPersonnelUserIds.includes(user.user_id) - ); - } - return ( - - getUserFullName(option)} - isOptionEqualToValue={(option, value) => - option.user_id === value.user_id - } - value={props.value || null} - onChange={(event, value) => props.onChange(value)} - renderInput={(params) => } - /> - Required - - ); - }, - }, - { - title: "Workgroup", - render: (personnel) => ( - - {personnel.moped_user.moped_workgroup.workgroup_name} - - ), - }, - { - title: ( - - Role{" "} - - info_outline - - - ), - field: "roleIds", - render: (personnel) => - personnel.moped_proj_personnel_roles.map( - ({ id, moped_project_role }) => ( - - {moped_project_role.project_role_name} - - ) - ), - validate: (rowData) => rowData?.roleIds?.length > 0, - editComponent: (props) => ( - - ), - }, - { - title: "Notes", - field: "notes", - render: (props) => (props.notes === "null" ? "" : props.notes), - editComponent: (props) => { - const val = props.value ?? ""; - return ( - props.onChange(e.target.value)} /> - ); - }, - }, - ]; - - if (loading || !data) return ; - - return ( - - ( - { - if (e.keyCode === 13) { - // Bypass default MaterialTable behavior of submitting the entire form when a user hits enter - // See https://github.com/mbrn/material-table/pull/2008#issuecomment-662529834 - } - }} - /> - ), - Action: (props) => { - // If isn't the add action - if ( - typeof props.action === typeof Function || - props.action.tooltip !== "Add" - ) { - return ; - } else { - return ( - - ); - } - }, - Toolbar: (props) => ( - // to have it align with table content -
- -
- ) - }} - data={personnel} - title={ - - Project team - - } - options={{ - paging: false, - search: false, - rowStyle: { fontFamily: typography.fontFamily }, - actionsColumnIndex: -1, - idSynonym: "project_personnel_id" - }} - localization={{ - header: { - actions: "", - }, - body: { - emptyDataSourceMessage: ( - - No team members to display - - ), - }, - }} - icons={{ Delete: DeleteOutlineIcon, Edit: EditOutlinedIcon }} - editable={{ - onRowAdd: (newData) => { - const payload = getNewPersonnelPayload({ newData, projectId }); - return insertProjectPersonnel({ - variables: { - object: payload, - }, - }).then(() => refetch()); - }, - onRowUpdate: async (newData, oldData) => { - const { project_personnel_id } = oldData; - const payload = getEditPersonnelPayload({ - newData, - }); - const [rolesToAdd, roleIdsToDelete] = getEditRolesPayload({ - newData, - oldData, - }); - const variables = { - updatePersonnelObject: payload, - id: project_personnel_id, - deleteIds: roleIdsToDelete, - addRolesObjects: rolesToAdd, - } - - console.log('variables', variables); - - return updateProjectPersonnel({ variables }).then(() => refetch()); - }, - onRowDelete: (oldData) => { - const id = oldData.project_personnel_id; - return deleteProjecPersonnel({ - variables: { id }, - }).then(() => refetch()); - }, - }} - /> -
- ); -}; - -export default ProjectTeamTable; diff --git a/moped-editor/src/views/projects/projectView/ProjectView.js b/moped-editor/src/views/projects/projectView/ProjectView.js index 541c670042..d4a2027a79 100644 --- a/moped-editor/src/views/projects/projectView/ProjectView.js +++ b/moped-editor/src/views/projects/projectView/ProjectView.js @@ -42,7 +42,7 @@ import Page from "src/components/Page"; import ProjectSummary from "./ProjectSummary/ProjectSummary"; import MapView from "./ProjectComponents/index"; import ProjectFunding from "./ProjectFunding"; -import ProjectTeam from "./ProjectTeam"; +import ProjectTeam from "./ProjectTeam/ProjectTeam"; import ProjectTimeline from "./ProjectTimeline"; import ProjectNotes from "./ProjectNotes"; import ProjectFiles from "./ProjectFiles"; From 433a96c9145271e4cc0718c43304ac821a5d3850 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Mon, 2 Dec 2024 16:43:10 -0600 Subject: [PATCH 24/76] remove failed attempt to be DRY --- .../LookupAutocompleteComponent.js | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 moped-editor/src/components/DataGridPro/LookupAutocompleteComponent.js diff --git a/moped-editor/src/components/DataGridPro/LookupAutocompleteComponent.js b/moped-editor/src/components/DataGridPro/LookupAutocompleteComponent.js deleted file mode 100644 index 27827211ba..0000000000 --- a/moped-editor/src/components/DataGridPro/LookupAutocompleteComponent.js +++ /dev/null @@ -1,77 +0,0 @@ -import React from "react"; -import { Autocomplete, TextField } from "@mui/material"; -import { useGridApiContext } from "@mui/x-data-grid-pro"; -import makeStyles from "@mui/styles/makeStyles"; -import { getLookupValueByID } from "src/utils/helpers"; -import CustomPopper from "src/components/CustomPopper"; - -const useStyles = makeStyles((theme) => ({ - autocompleteLookupInput: { - minWidth: "200px", - alignContent: "center", - padding: theme.spacing(1), - }, -})); - -/** - * Component for dropdown select using a lookup table as options - * @param {Number} id - row id in Data Grid - * @param {string} value - Field value - * @param {string} field - name of Field - * @param {Boolean} hasFocus - is field focused - * @param {String} name - name of lookup table - * @param {Array|Objects} lookupTable - the lookup table data - * @returns {React component} - */ -const LookupAutocompleteComponent = ({ - id, - value, - field, - hasFocus, - name, - lookupTable, -}) => { - const classes = useStyles(); - const apiRef = useGridApiContext(); - const ref = React.useRef(null); - - React.useEffect(() => { - if (hasFocus) { - ref.current.focus(); - } - }, [hasFocus]); - - const handleChange = (event, newValue) => { - apiRef.current.setEditCellValue({ - id, - field, - value: newValue ? newValue[`${name}_id`] : null, - }); - }; - - return ( - ( - - )} - getOptionLabel={(option) => - // if our value is a string, just return the string instead of accessing the name - typeof option === "string" ? option : option[`${name}_name`] - } - isOptionEqualToValue={(value, option) => value[`${name}_name`] === option} - onChange={handleChange} - /> - ); -}; - -export default LookupAutocompleteComponent; From 0d997e2ba43f76f5ba4dfaa49ef028446d0f6f16 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Mon, 2 Dec 2024 16:49:04 -0600 Subject: [PATCH 25/76] consolidate import --- .../{ => components/DataGridPro}/utils/helpers.js | 2 +- .../ProjectFunding/LookupAutocompleteComponent.js | 2 +- .../ProjectFunding/ProjectFundingTable.js | 2 +- .../projectView/ProjectFunding/utils/helpers.js | 14 -------------- 4 files changed, 3 insertions(+), 17 deletions(-) rename moped-editor/src/{ => components/DataGridPro}/utils/helpers.js (99%) delete mode 100644 moped-editor/src/views/projects/projectView/ProjectFunding/utils/helpers.js diff --git a/moped-editor/src/utils/helpers.js b/moped-editor/src/components/DataGridPro/utils/helpers.js similarity index 99% rename from moped-editor/src/utils/helpers.js rename to moped-editor/src/components/DataGridPro/utils/helpers.js index 2fdf1b2583..4e881767d1 100644 --- a/moped-editor/src/utils/helpers.js +++ b/moped-editor/src/components/DataGridPro/utils/helpers.js @@ -11,4 +11,4 @@ export const getLookupValueByID = (lookupTable, attribute, id) => { return lookupTable.find((item) => item[`${attribute}_id`] === id)[ `${attribute}_name` ]; -}; +}; \ No newline at end of file diff --git a/moped-editor/src/views/projects/projectView/ProjectFunding/LookupAutocompleteComponent.js b/moped-editor/src/views/projects/projectView/ProjectFunding/LookupAutocompleteComponent.js index 1995c0dc25..f512692e10 100644 --- a/moped-editor/src/views/projects/projectView/ProjectFunding/LookupAutocompleteComponent.js +++ b/moped-editor/src/views/projects/projectView/ProjectFunding/LookupAutocompleteComponent.js @@ -2,7 +2,7 @@ import React from "react"; import { Autocomplete, TextField } from "@mui/material"; import { useGridApiContext } from "@mui/x-data-grid-pro"; import makeStyles from "@mui/styles/makeStyles"; -import { getLookupValueByID } from "./utils/helpers"; +import { getLookupValueByID } from "src/components/DataGridPro/utils/helpers"; import CustomPopper from "src/components/CustomPopper"; const useStyles = makeStyles((theme) => ({ diff --git a/moped-editor/src/views/projects/projectView/ProjectFunding/ProjectFundingTable.js b/moped-editor/src/views/projects/projectView/ProjectFunding/ProjectFundingTable.js index ba7a0139e6..4f83a6523f 100644 --- a/moped-editor/src/views/projects/projectView/ProjectFunding/ProjectFundingTable.js +++ b/moped-editor/src/views/projects/projectView/ProjectFunding/ProjectFundingTable.js @@ -42,7 +42,7 @@ import LookupAutocompleteComponent from "./LookupAutocompleteComponent"; import FundAutocompleteComponent from "./FundAutocompleteComponent"; import dataGridProStyleOverrides from "src/styles/dataGridProStylesOverrides"; import DeleteConfirmationModal from "../DeleteConfirmationModal"; -import { getLookupValueByID } from "./utils/helpers"; +import { getLookupValueByID } from "src/components/DataGridPro/utils/helpers"; import { defaultEditColumnIconStyle } from "src/utils/dataGridHelpers"; const useStyles = makeStyles((theme) => ({ diff --git a/moped-editor/src/views/projects/projectView/ProjectFunding/utils/helpers.js b/moped-editor/src/views/projects/projectView/ProjectFunding/utils/helpers.js deleted file mode 100644 index 2fdf1b2583..0000000000 --- a/moped-editor/src/views/projects/projectView/ProjectFunding/utils/helpers.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Get lookup value for a given table using a record ID and returning a name - * @param {Array|Object} lookupTable - Lookup table - * @param {string} attribute - Prefix version of attribute name relying on the pattern of _id and _name - * @param {number} id - ID used to find target record in lookup table - * @return {string} - Name of attribute in the given row. - */ -export const getLookupValueByID = (lookupTable, attribute, id) => { - if (!id) return null; - - return lookupTable.find((item) => item[`${attribute}_id`] === id)[ - `${attribute}_name` - ]; -}; From 71e4bc3189faa8ef29009e93e2a8f7f8a7a06edd Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Mon, 2 Dec 2024 16:50:51 -0600 Subject: [PATCH 26/76] update component name --- .../src/views/projects/projectView/ProjectTeam/ProjectTeam.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeam.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeam.js index ac7a1abca3..372b12078e 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeam.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeam.js @@ -1,5 +1,5 @@ import React from "react"; -import ProjectTeamTableDataGridPro from "./ProjectTeamTable"; +import ProjectTeamTable from "./ProjectTeamTable"; import { useParams } from "react-router-dom"; import { CardContent, Grid } from "@mui/material"; @@ -11,7 +11,7 @@ const ProjectTeam = () => { - + From eb41e94196b5928653f55a04f8aa54de7c472a3b Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Mon, 2 Dec 2024 16:55:17 -0600 Subject: [PATCH 27/76] fix console warning --- .../projectView/ProjectTeam/TeamAutocompleteComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js b/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js index df211cb296..f9292f2555 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js @@ -71,7 +71,7 @@ const TeamAutocompleteComponent = ({ options={options} getOptionLabel={getOptionLabel} isOptionEqualToValue={(option, value) => isOptionEqualToValue(option, value)} - value={value} + value={value || null} sx={{ paddingTop: "8px" }} onChange={(event, newValue) => { handleChange(event, newValue); From 447cf7e068c0a5937a7d1521d7f67c832a20cf8e Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Wed, 4 Dec 2024 20:45:09 -0600 Subject: [PATCH 28/76] PR feedback --- .../projects/projectView/ProjectTeam/ProjectTeamTable.js | 4 +--- .../projectView/ProjectTeam/TeamAutocompleteComponent.js | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js index 2545c69556..5345d201c4 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js @@ -3,7 +3,7 @@ import { useState, useMemo, useEffect, useCallback } from "react"; import isEqual from "lodash/isEqual"; import { v4 as uuidv4 } from "uuid"; -import { Box, Icon, Link, CircularProgress, Typography, TextField } from '@mui/material'; +import { Box, Icon, Link, CircularProgress, Typography } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; import { EditOutlined as EditOutlinedIcon, DeleteOutline as DeleteOutlineIcon, Check as CheckIcon, Close as CloseIcon } from '@mui/icons-material'; @@ -465,7 +465,6 @@ const getEditRolesPayload = (newData, oldData) => { return ( - { }, }} /> - ); }; diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js b/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js index f9292f2555..4f25c3dec0 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/TeamAutocompleteComponent.js @@ -6,6 +6,7 @@ import { FormControl, } from "@mui/material"; import { useGridApiContext } from "@mui/x-data-grid-pro"; +import { useTheme } from "@mui/material/styles"; const TeamAutocompleteComponent = ({ id, @@ -16,6 +17,7 @@ const TeamAutocompleteComponent = ({ error, name, }) => { + const theme = useTheme(); const apiRef = useGridApiContext(); const ref = React.useRef(null); @@ -63,7 +65,7 @@ const TeamAutocompleteComponent = ({ return ( Date: Thu, 5 Dec 2024 14:08:44 -0600 Subject: [PATCH 29/76] bump local graphql-engine version --- moped-database/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moped-database/docker-compose.yml b/moped-database/docker-compose.yml index 894fe1236b..e276799246 100644 --- a/moped-database/docker-compose.yml +++ b/moped-database/docker-compose.yml @@ -1,7 +1,7 @@ # Architecture Independent Docker Stack Components for Moped Development Environment services: hasura: - image: hasura/graphql-engine:v2.44.0 + image: hasura/graphql-engine:v2.45.0 depends_on: - moped-pgsql expose: From a4fd8f809d703c255d04e436739caca99c1cc7e1 Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Fri, 6 Dec 2024 13:44:02 -0600 Subject: [PATCH 30/76] fix role variable payload on insert mutation --- .../projectView/ProjectTeam/ProjectTeamTable.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js index 5345d201c4..81220a4f56 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js @@ -249,19 +249,21 @@ const ProjectTeamTable = ({ projectId }) => { * @param {integer} projectId - the project ID * @return {Object} a moped_project_personnel object: { user_id, notes, moped_proj_personnel_roles: { project_role_id } } */ + const getNewPersonnelPayload = ({ newData: { moped_user: { user_id }, notes, - roleIds, + moped_proj_personnel_roles }, projectId: project_id, }) => { const payload = { notes, project_id, user_id }; - const personnelRoles = roleIds.map((roleId) => ({ - project_role_id: roleId, + const personnelRoles = moped_proj_personnel_roles.map((roles) => ({ + project_role_id: roles.project_role_id, })); - payload["moped_proj_personnel_roles"] = { data: personnelRoles }; + + payload.moped_proj_personnel_roles = { data: personnelRoles }; return payload; }; @@ -408,9 +410,6 @@ const getEditRolesPayload = (newData, oldData) => { (role) => role.project_role_id ); - // // Check if the roles have changed - // const haveRolesChanged = !isEqual(updatedRow.moped_proj_personnel_roles, originalRow.moped_proj_personnel_roles); - const payload = getEditPersonnelPayload(updatedRow); const [rolesToAdd, roleIdsToDelete] = getEditRolesPayload(updatedRow, originalRow); From e18b46fdd90a22b4dc790b1f28bb7016972013fb Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Fri, 6 Dec 2024 13:50:47 -0600 Subject: [PATCH 31/76] remove cruft --- .../projectView/ProjectTeamRoleMultiselect.js | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 moped-editor/src/views/projects/projectView/ProjectTeamRoleMultiselect.js diff --git a/moped-editor/src/views/projects/projectView/ProjectTeamRoleMultiselect.js b/moped-editor/src/views/projects/projectView/ProjectTeamRoleMultiselect.js deleted file mode 100644 index 95b6271bcb..0000000000 --- a/moped-editor/src/views/projects/projectView/ProjectTeamRoleMultiselect.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; -import { FormControl, FormHelperText } from "@mui/material"; -import Select from "@mui/material/Select"; -import MenuItem from "@mui/material/MenuItem"; -import Checkbox from "@mui/material/Checkbox"; -import ListItemText from "@mui/material/ListItemText"; - -const ProjectTeamRoleMultiselect = ({ value, onChange, roles }) => { - return ( - - - Required - - ); -}; - -export default ProjectTeamRoleMultiselect; \ No newline at end of file From ce87dab825de8f914772b85dd2c612966a4c465f Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Fri, 6 Dec 2024 13:52:47 -0600 Subject: [PATCH 32/76] memoize --- .../projectView/ProjectTeam/ProjectTeamRoleMultiselect.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js index cb40e8d6f1..b9d5c829e2 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js @@ -27,7 +27,10 @@ const useStyles = makeStyles((theme) => ({ })); const ProjectTeamRoleMultiselect = ({ id, field, roles, value }) => { - const rolesArray = value.map((role) => role.project_role_id); + const rolesArray = React.useMemo( + () => value.map((role) => role.project_role_id), + [value] + ); const [selectedValues, setSelectedValues] = React.useState(rolesArray || []); const classes = useStyles(); From 4d369e0f738ece8b93eb9e2c436b8c7b3071c87e Mon Sep 17 00:00:00 2001 From: mateoclarke Date: Fri, 6 Dec 2024 14:01:33 -0600 Subject: [PATCH 33/76] Use sx attr instead of useStyles and style={{}} --- .../ProjectTeam/ProjectTeamRoleMultiselect.js | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js index b9d5c829e2..1063cebe8c 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamRoleMultiselect.js @@ -10,21 +10,9 @@ import { Select, Typography, } from "@mui/material"; -import makeStyles from '@mui/styles/makeStyles'; import { useGridApiContext } from "@mui/x-data-grid-pro"; - -const useStyles = makeStyles((theme) => ({ - formControl: { - margin: theme.spacing(1), - }, - noLabel: { - marginTop: theme.spacing(3), - }, - infoIcon: { - color: theme.palette.action.disabled, - }, -})); +import theme from "src/theme"; const ProjectTeamRoleMultiselect = ({ id, field, roles, value }) => { const rolesArray = React.useMemo( @@ -33,7 +21,6 @@ const ProjectTeamRoleMultiselect = ({ id, field, roles, value }) => { ); const [selectedValues, setSelectedValues] = React.useState(rolesArray || []); - const classes = useStyles(); const apiRef = useGridApiContext(); const handleChange = (event) => { @@ -49,10 +36,13 @@ const ProjectTeamRoleMultiselect = ({ id, field, roles, value }) => { }; return ( - + { PaperProps: { style: { width: "50%" } }, anchorOrigin: { vertical: "bottom", horizontal: "center" }, transformOrigin: { vertical: "top", horizontal: "center" }, - }}> + }} + > {roles.map( ({ project_role_id, diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js index 3c9ec688c4..7bcbc40bdb 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamTable.js @@ -2,26 +2,36 @@ import { useState, useMemo, useEffect, useCallback } from "react"; import isEqual from "lodash/isEqual"; import { v4 as uuidv4 } from "uuid"; -import { Box, Icon, Link, CircularProgress, Typography } from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import { EditOutlined as EditOutlinedIcon, DeleteOutline as DeleteOutlineIcon, Check as CheckIcon, Close as CloseIcon } from '@mui/icons-material'; - -import { DataGridPro, GridRowModes, GridActionsCellItem, useGridApiRef } from '@mui/x-data-grid-pro'; +import { Box, Icon, Link, CircularProgress, Typography } from "@mui/material"; +import makeStyles from "@mui/styles/makeStyles"; +import { + EditOutlined as EditOutlinedIcon, + DeleteOutline as DeleteOutlineIcon, + Check as CheckIcon, + Close as CloseIcon, +} from "@mui/icons-material"; + +import { + DataGridPro, + GridRowModes, + GridActionsCellItem, + useGridApiRef, +} from "@mui/x-data-grid-pro"; import { useQuery, useMutation } from "@apollo/client"; -import ApolloErrorHandler from 'src/components/ApolloErrorHandler'; +import ApolloErrorHandler from "src/components/ApolloErrorHandler"; import { defaultEditColumnIconStyle } from "src/utils/dataGridHelpers"; -import { - TEAM_QUERY, - UPDATE_PROJECT_PERSONNEL, - INSERT_PROJECT_PERSONNEL, - DELETE_PROJECT_PERSONNEL +import { + TEAM_QUERY, + UPDATE_PROJECT_PERSONNEL, + INSERT_PROJECT_PERSONNEL, + DELETE_PROJECT_PERSONNEL, } from "src/queries/project"; -import dataGridProStyleOverrides from 'src/styles/dataGridProStylesOverrides'; -import ProjectTeamToolbar from './ProjectTeamToolbar'; -import ProjectTeamRoleMultiselect from './ProjectTeamRoleMultiselect'; -import TeamAutocompleteComponent from './TeamAutocompleteComponent'; -import DataGridTextField from 'src/components/DataGridPro/DataGridTextField'; +import dataGridProStyleOverrides from "src/styles/dataGridProStylesOverrides"; +import ProjectTeamToolbar from "./ProjectTeamToolbar"; +import ProjectTeamRoleMultiselect from "./ProjectTeamRoleMultiselect"; +import TeamAutocompleteComponent from "./TeamAutocompleteComponent"; +import DataGridTextField from "src/components/DataGridPro/DataGridTextField"; const useStyles = makeStyles((theme) => ({ infoIcon: { @@ -37,7 +47,7 @@ const useStyles = makeStyles((theme) => ({ }, })); -const useTeamNameLookup = (data) => +const useTeamNameLookup = (data) => useMemo(() => { if (!data) { return {}; @@ -48,7 +58,7 @@ const useTeamNameLookup = (data) => }, {}); }, [data]); -const useRoleNameLookup = (data) => +const useRoleNameLookup = (data) => useMemo(() => { if (!data) { return {}; @@ -68,145 +78,147 @@ const useColumns = ({ handleDeleteOpen, classes, teamNameLookup, - roleNameLookup -}) => + roleNameLookup, +}) => useMemo(() => { return [ - { - headerName: 'Name', - field: 'moped_user', - width: 250, - editable: true, - valueGetter: (user) => { - return user ? `${user.first_name} ${user.last_name}` : ''; - }, - renderEditCell: (props) => { - return( - - ); - } - }, - { - headerName: 'Workgroup', - field: 'moped_workgroup', - width: 200, - valueGetter: (workgroup) => workgroup?.workgroup_name, - }, - { - headerName: 'Role', - field: 'moped_proj_personnel_roles', - width: 200, - editable: true, - renderHeader: () => ( -
- Role{" "} - - info_outline - -
- ), - renderCell: (params) => { - // Filter out deleted roles and map to Typography components - const roleElements = params.row.moped_proj_personnel_roles - .filter(role => !role.is_deleted) - .map(role => ( - - {role.moped_project_role?.project_role_name} - - )); - - return {roleElements}; - }, - renderEditCell: (props) => { - return ( - - ); - }, - preProcessEditCellProps: (params) => { - // Enforce required field - const hasError = !params.props.value || params.props.value.length === 0; - return { ...params.props, error: hasError }; - } - }, - { - headerName: 'Notes', - field: 'notes', - width: 200, - editable: true, - renderEditCell: (props) => - }, - { - headerName: '', - field: 'edit', - hideable: false, - filterable: false, - sortable: false, - editable: false, - type: "actions", - getActions: ({ id }) => { - const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; - if (isInEditMode) { - return [ - } - label="Save" - sx={{ - color: "primary.main", - }} - onClick={handleSaveClick(id)} - />, - } - label="Cancel" - className="textPrimary" - onClick={handleCancelClick(id, 'project_personnel_id')} - color="inherit" - />, - ]; - } - return [ - } - label="Edit" - className="textPrimary" - onClick={handleEditClick(id)} - color="inherit" - />, - } - label="Delete" - onClick={() => handleDeleteOpen(id)} - color="inherit" - />, - ]; - }, - } - ] - }, - [ - data, - rowModesModel, - handleEditClick, - handleSaveClick, - handleCancelClick, + { + headerName: "Name", + field: "moped_user", + width: 250, + editable: true, + valueGetter: (user) => { + return user ? `${user.first_name} ${user.last_name}` : ""; + }, + renderEditCell: (props) => { + return ( + + ); + }, + }, + { + headerName: "Workgroup", + field: "moped_workgroup", + width: 200, + valueGetter: (workgroup) => workgroup?.workgroup_name, + }, + { + headerName: "Role", + field: "moped_proj_personnel_roles", + width: 200, + editable: true, + renderHeader: () => ( +
+ Role{" "} + + info_outline + +
+ ), + renderCell: (params) => { + // Filter out deleted roles and map to Typography components + const roleElements = params.row.moped_proj_personnel_roles + .filter((role) => !role.is_deleted) + .map((role) => ( + + {role.moped_project_role?.project_role_name} + + )); + + return {roleElements}; + }, + renderEditCell: (props) => { + return ( + + ); + }, + preProcessEditCellProps: (params) => { + // Enforce required field + const hasError = + !params.props.value || params.props.value.length === 0; + return { ...params.props, error: hasError }; + }, + }, + { + headerName: "Notes", + field: "notes", + width: 200, + editable: true, + renderEditCell: (props) => , + }, + { + headerName: "", + field: "edit", + hideable: false, + filterable: false, + sortable: false, + editable: false, + type: "actions", + getActions: ({ id }) => { + const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; + if (isInEditMode) { + return [ + } + label="Save" + sx={{ + color: "primary.main", + }} + onClick={handleSaveClick(id)} + />, + } + label="Cancel" + className="textPrimary" + onClick={handleCancelClick(id, "project_personnel_id")} + color="inherit" + />, + ]; + } + return [ + } + label="Edit" + className="textPrimary" + onClick={handleEditClick(id)} + color="inherit" + />, + } + label="Delete" + onClick={() => handleDeleteOpen(id)} + color="inherit" + />, + ]; + }, + }, + ]; + }, [ + data, + rowModesModel, + handleEditClick, + handleSaveClick, + handleCancelClick, handleDeleteOpen, classes, - teamNameLookup - ] -); + teamNameLookup, + ]); const ProjectTeamTable = ({ projectId }) => { const apiRef = useGridApiRef(); @@ -226,12 +238,14 @@ const ProjectTeamTable = ({ projectId }) => { useEffect(() => { if (data?.moped_project_by_pk?.moped_proj_personnel?.length > 0) { - // Set the rows but add the workgroup name to the row data + // Set the rows but add the workgroup name to the row data // instead of being nested under moped_user. - setRows(data.moped_project_by_pk.moped_proj_personnel.map(personnel => ({ - ...personnel, - moped_workgroup: personnel.moped_user.moped_workgroup - }))); + setRows( + data.moped_project_by_pk.moped_proj_personnel.map((personnel) => ({ + ...personnel, + moped_workgroup: personnel.moped_user.moped_workgroup, + })) + ); } else { setRows([]); // Reset rows when no data is available } @@ -241,75 +255,75 @@ const ProjectTeamTable = ({ projectId }) => { const roleNameLookup = useRoleNameLookup(data); /** - * Construct a moped_project_personnel object that can be passed to an insert mutation - * @param {Object} newData - a table row object with { moped_user, notes, roleIds } - * @param {integer} projectId - the project ID - * @return {Object} a moped_project_personnel object: { user_id, notes, moped_proj_personnel_roles: { project_role_id } } - */ - -const getNewPersonnelPayload = ({ - newData: { - moped_user: { user_id }, - notes, - moped_proj_personnel_roles - }, - projectId: project_id, -}) => { - const payload = { notes, project_id, user_id }; - const personnelRoles = moped_proj_personnel_roles.map((roles) => ({ - project_role_id: roles.project_role_id, - })); - - payload.moped_proj_personnel_roles = { data: personnelRoles }; - return payload; -}; + * Construct a moped_project_personnel object that can be passed to an insert mutation + * @param {Object} newData - a table row object with { moped_user, notes, roleIds } + * @param {integer} projectId - the project ID + * @return {Object} a moped_project_personnel object: { user_id, notes, moped_proj_personnel_roles: { project_role_id } } + */ + + const getNewPersonnelPayload = ({ + newData: { + moped_user: { user_id }, + notes, + moped_proj_personnel_roles, + }, + projectId: project_id, + }) => { + const payload = { notes, project_id, user_id }; + const personnelRoles = moped_proj_personnel_roles.map((roles) => ({ + project_role_id: roles.project_role_id, + })); -/** - * Construct a moped_project_personnel object that can be passed to an update mutation - * @param {Object} newData - a table row object with { moped_user, notes, roleIds } - * @param {integer} projectId - the project ID - * @return {Object} a moped_project_personnel object: { user_id, notes } <- observe that `moped_proj_personnel_roles` - * is handled separately - */ -const getEditPersonnelPayload = (newData) => { - const { - moped_user: { user_id }, - notes, - } = newData; - return { user_id, notes }; -}; + payload.moped_proj_personnel_roles = { data: personnelRoles }; + return payload; + }; -/** - * Constructs payload objects for adding and removing moped_proj_personnel_roles - * @param {Object} newData - a table row object with the new values - * @param {Object} oldData - a table row object with the old values - * @return {[[Object], [Int]]} - an array of new personnel role objects, and an array of existing - * personnel role objects to delete - */ -const getEditRolesPayload = (newData, oldData) => { - const { project_personnel_id } = oldData; - - // get an array of moped_proj_personnel_roles IDs to delete - const projRoleIdsToDelete = oldData.moped_proj_personnel_roles - .filter((projRole) => { - const roleId = projRole.moped_project_role.project_role_id; - return !newData.roleIds.includes(roleId); - }) - .map((projRole) => projRole.id); - - // construct an array of new moped_proj_personnel_roles objects - const existingRoleIds = oldData.moped_proj_personnel_roles.map( - ({ moped_project_role }) => moped_project_role.project_role_id - ); - const roleIdsToAdd = newData.roleIds.filter( - (roleId) => !existingRoleIds.includes(roleId) - ); - const rolesToAddPayload = roleIdsToAdd.map((newRoleId) => ({ - project_personnel_id, - project_role_id: newRoleId, - })); - return [rolesToAddPayload, projRoleIdsToDelete]; -}; + /** + * Construct a moped_project_personnel object that can be passed to an update mutation + * @param {Object} newData - a table row object with { moped_user, notes, roleIds } + * @param {integer} projectId - the project ID + * @return {Object} a moped_project_personnel object: { user_id, notes } <- observe that `moped_proj_personnel_roles` + * is handled separately + */ + const getEditPersonnelPayload = (newData) => { + const { + moped_user: { user_id }, + notes, + } = newData; + return { user_id, notes }; + }; + + /** + * Constructs payload objects for adding and removing moped_proj_personnel_roles + * @param {Object} newData - a table row object with the new values + * @param {Object} oldData - a table row object with the old values + * @return {[[Object], [Int]]} - an array of new personnel role objects, and an array of existing + * personnel role objects to delete + */ + const getEditRolesPayload = (newData, oldData) => { + const { project_personnel_id } = oldData; + + // get an array of moped_proj_personnel_roles IDs to delete + const projRoleIdsToDelete = oldData.moped_proj_personnel_roles + .filter((projRole) => { + const roleId = projRole.moped_project_role.project_role_id; + return !newData.roleIds.includes(roleId); + }) + .map((projRole) => projRole.id); + + // construct an array of new moped_proj_personnel_roles objects + const existingRoleIds = oldData.moped_proj_personnel_roles.map( + ({ moped_project_role }) => moped_project_role.project_role_id + ); + const roleIdsToAdd = newData.roleIds.filter( + (roleId) => !existingRoleIds.includes(roleId) + ); + const rolesToAddPayload = roleIdsToAdd.map((newRoleId) => ({ + project_personnel_id, + project_role_id: newRoleId, + })); + return [rolesToAddPayload, projRoleIdsToDelete]; + }; const onClickAddTeamMember = () => { const id = uuidv4(); @@ -323,133 +337,168 @@ const getEditRolesPayload = (newData, oldData) => { isNew: true, roleIds: [], project_personnel_id: id, - }, + }, ...oldRows, ]); setRowModesModel((oldModel) => ({ ...oldModel, [id]: { mode: GridRowModes.Edit, fieldToFocus: "project_personnel_id" }, })); - } - - const handleEditClick = useCallback((id) => () => { - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); - }, [rowModesModel]); - - const handleSaveClick = useCallback((id) => () => { - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); - }, [rowModesModel]); - - const handleCancelClick = useCallback((id, tableId) => () => { - setRowModesModel({ - ...rowModesModel, - [id]: { mode: GridRowModes.View, ignoreModifications: true }, - }); - const editedRow = rows.find((row) => row[tableId] === id); - if (editedRow.isNew) { - setRows(rows.filter((row) => row.id !== id)); - } - }, [rowModesModel, rows]); + }; - const handleDeleteOpen = useCallback((id) => { - return deleteProjectPersonnel({ - variables: { id }, - }).then(() => refetch()); - }, [deleteProjectPersonnel, refetch]); + const handleEditClick = useCallback( + (id) => () => { + setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); + }, + [rowModesModel] + ); - const processRowUpdate = useCallback((updatedRow, originalRow, params, data) => { - let userId; + const handleSaveClick = useCallback( + (id) => () => { + setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); + }, + [rowModesModel] + ); - const userObject = data.moped_users.find(user => { - if (typeof updatedRow.moped_user === 'string') { - return `${user.first_name} ${user.last_name}` === updatedRow.moped_user; - } else { - return user.user_id === updatedRow.moped_user.user_id; + const handleCancelClick = useCallback( + (id, tableId) => () => { + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View, ignoreModifications: true }, + }); + const editedRow = rows.find((row) => row[tableId] === id); + if (editedRow.isNew) { + setRows(rows.filter((row) => row.id !== id)); } - }); - - if (userObject) { - userId = userObject.user_id; - updatedRow.moped_user = userObject; // Update with full user object - } else { - console.error('Invalid user data:', updatedRow.moped_user); - throw new Error('Invalid user data'); - } - - // normalize the updatedRow and originalRow - const normalizedUpdatedRow = { - ...updatedRow, - roleIds: updatedRow.moped_proj_personnel_roles.map(role => role.project_role_id), - moped_user: updatedRow.moped_user?.user_id || null - }; - const normalizedOriginalRow = { - ...originalRow, - roleIds: originalRow.moped_proj_personnel_roles.map(role => role.project_role_id), - moped_user: originalRow.moped_user?.user_id || null - }; - - // Check if the row has changed, including the roles array - const hasRowChanged = !isEqual(normalizedUpdatedRow, normalizedOriginalRow) - if (!hasRowChanged) { - return Promise.resolve(updatedRow); - } + }, + [rowModesModel, rows] + ); - if (updatedRow.isNew) { - const payload = getNewPersonnelPayload({ newData: updatedRow, projectId }); + const handleDeleteOpen = useCallback( + (id) => { + return deleteProjectPersonnel({ + variables: { id }, + }).then(() => refetch()); + }, + [deleteProjectPersonnel, refetch] + ); - return insertProjectPersonnel({ - variables: { - object: payload, - }, - }) - .then(() => refetch()) - .then(() => updatedRow) - .catch((error) => { - console.error('Mutation error:', error); - throw error; + const processRowUpdate = useCallback( + (updatedRow, originalRow, params, data) => { + let userId; + + const userObject = data.moped_users.find((user) => { + if (typeof updatedRow.moped_user === "string") { + return ( + `${user.first_name} ${user.last_name}` === updatedRow.moped_user + ); + } else { + return user.user_id === updatedRow.moped_user.user_id; + } }); - } else { - // Ensure project_personnel_id is an integer - const personnelId = parseInt(updatedRow.project_personnel_id); - - if (!personnelId) { - console.error('Invalid project_personnel_id:', updatedRow.project_personnel_id); - throw new Error('Invalid project_personnel_id'); - } - // Update roleIds to be in sync with moped_proj_personnel_roles - updatedRow.roleIds = updatedRow.moped_proj_personnel_roles.map( - (role) => role.project_role_id - ); - - const payload = getEditPersonnelPayload(updatedRow); - const [rolesToAdd, roleIdsToDelete] = getEditRolesPayload(updatedRow, originalRow); - - // get update name - payload.user_id = userId; - - const fullMopedUserObject = data.moped_users.find(user => user.user_id === userId); - updatedRow.moped_user = fullMopedUserObject; + if (userObject) { + userId = userObject.user_id; + updatedRow.moped_user = userObject; // Update with full user object + } else { + console.error("Invalid user data:", updatedRow.moped_user); + throw new Error("Invalid user data"); + } - const variables = { - id: personnelId, - updatePersonnelObject: payload, - deleteIds: roleIdsToDelete, - addRolesObjects: rolesToAdd - }; + // normalize the updatedRow and originalRow + const normalizedUpdatedRow = { + ...updatedRow, + roleIds: updatedRow.moped_proj_personnel_roles.map( + (role) => role.project_role_id + ), + moped_user: updatedRow.moped_user?.user_id || null, + }; + const normalizedOriginalRow = { + ...originalRow, + roleIds: originalRow.moped_proj_personnel_roles.map( + (role) => role.project_role_id + ), + moped_user: originalRow.moped_user?.user_id || null, + }; + + // Check if the row has changed, including the roles array + const hasRowChanged = !isEqual( + normalizedUpdatedRow, + normalizedOriginalRow + ); + if (!hasRowChanged) { + return Promise.resolve(updatedRow); + } - return updateProjectPersonnel({ variables }) - .then(() => refetch()) - .then(() => updatedRow) - .catch((error) => { - console.error('Mutation error:', error); - throw error; + if (updatedRow.isNew) { + const payload = getNewPersonnelPayload({ + newData: updatedRow, + projectId, }); - } - }, [updateProjectPersonnel, insertProjectPersonnel, projectId, refetch]); + + return insertProjectPersonnel({ + variables: { + object: payload, + }, + }) + .then(() => refetch()) + .then(() => updatedRow) + .catch((error) => { + console.error("Mutation error:", error); + throw error; + }); + } else { + // Ensure project_personnel_id is an integer + const personnelId = parseInt(updatedRow.project_personnel_id); + + if (!personnelId) { + console.error( + "Invalid project_personnel_id:", + updatedRow.project_personnel_id + ); + throw new Error("Invalid project_personnel_id"); + } + + // Update roleIds to be in sync with moped_proj_personnel_roles + updatedRow.roleIds = updatedRow.moped_proj_personnel_roles.map( + (role) => role.project_role_id + ); + + const payload = getEditPersonnelPayload(updatedRow); + const [rolesToAdd, roleIdsToDelete] = getEditRolesPayload( + updatedRow, + originalRow + ); + + // get update name + payload.user_id = userId; + + const fullMopedUserObject = data.moped_users.find( + (user) => user.user_id === userId + ); + updatedRow.moped_user = fullMopedUserObject; + + const variables = { + id: personnelId, + updatePersonnelObject: payload, + deleteIds: roleIdsToDelete, + addRolesObjects: rolesToAdd, + }; + + return updateProjectPersonnel({ variables }) + .then(() => refetch()) + .then(() => updatedRow) + .catch((error) => { + console.error("Mutation error:", error); + throw error; + }); + } + }, + [updateProjectPersonnel, insertProjectPersonnel, projectId, refetch] + ); const handleProcessUpdateError = useCallback((error) => { - console.error('process row update error', error); + console.error("process row update error", error); }, []); const dataGridColumns = useColumns({ @@ -461,18 +510,16 @@ const getEditRolesPayload = (newData, oldData) => { handleDeleteOpen, classes, teamNameLookup, - roleNameLookup + roleNameLookup, }); const processRowUpdateMemoized = useCallback( - (updatedRow, originalRow, params) => processRowUpdate(updatedRow, originalRow, params, data), + (updatedRow, originalRow, params) => + processRowUpdate(updatedRow, originalRow, params, data), [processRowUpdate, data] ); - const getRowIdMemoized = useCallback( - (row) => row.project_personnel_id, - [] - ); + const getRowIdMemoized = useCallback((row) => row.project_personnel_id, []); if (loading || !data) return ; @@ -494,9 +541,9 @@ const getEditRolesPayload = (newData, oldData) => { disableRowSelectionOnClick toolbar density="comfortable" - getRowHeight={() => 'auto'} + getRowHeight={() => "auto"} hideFooter - localeText={{ noRowsLabel: 'No team members found' }} + localeText={{ noRowsLabel: "No team members found" }} disableColumnMenu loading={loading} slots={{ diff --git a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamToolbar.js b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamToolbar.js index ab914cb9af..a70d3528bd 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamToolbar.js +++ b/moped-editor/src/views/projects/projectView/ProjectTeam/ProjectTeamToolbar.js @@ -1,12 +1,12 @@ import { Box, Typography } from "@mui/material"; import { Button } from "@mui/material"; -import AddCircleIcon from '@mui/icons-material/AddCircle'; +import AddCircleIcon from "@mui/icons-material/AddCircle"; const ProjectTeamToolbar = ({ addAction }) => ( - - Project Team - + + Project Team +