Skip to content

Commit ce31c4c

Browse files
authored
Merge pull request #2048 from trillium/trilliumsmith/ts.2046
feat: Add On/Offboard Visibility feature for project pages
2 parents 21ff213 + 247160e commit ce31c4c

File tree

8 files changed

+293
-44
lines changed

8 files changed

+293
-44
lines changed

backend/controllers/project.controller.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,26 @@ ProjectController.bulkUpdateManagedByUsers = async function (req, res) {
130130
}
131131
};
132132

133+
// Update onboard/offboard visibility for a project
134+
ProjectController.updateOnboardOffboardVisibility = async function (req, res) {
135+
const { ProjectId } = req.params;
136+
const { onboardOffboardVisible } = req.body;
137+
138+
try {
139+
const project = await Project.findByIdAndUpdate(
140+
ProjectId,
141+
{ onboardOffboardVisible },
142+
{ new: true }
143+
);
144+
145+
if (!project) {
146+
return res.status(404).send({ message: 'Project not found' });
147+
}
148+
149+
return res.status(200).send(project);
150+
} catch (err) {
151+
return res.status(400).send({ message: 'Error updating visibility', error: err.message });
152+
}
153+
};
154+
133155
module.exports = ProjectController;

backend/models/project.model.js

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,49 +16,51 @@ Idea for the future: programmingLanguages, numberGithubContributions (pull these
1616
*/
1717

1818
const projectSchema = mongoose.Schema({
19-
name: { type: String, trim: true },
20-
description: { type: String, trim: true },
21-
githubIdentifier: { type: String, trim: true },
22-
projectStatus: { type: String }, // Active, Completed, or Paused
23-
location: { type: String, trim: true }, // DTLA, Westside, South LA, or Remote (hacknight)
24-
//teamMembers: { type: String }, // commented since we should be able to get this from Project Team Members table
25-
createdDate: { type: Date, default: Date.now }, // date/time project was created
26-
completedDate: { type: Date }, // only if Status = Completed, date/time completed
27-
githubUrl: { type: String, trim: true }, // link to main repo
28-
slackUrl: { type: String, trim: true }, // link to Slack channel
29-
googleDriveUrl: { type: String, trim: true },
30-
googleDriveId: { type: String },
31-
hflaWebsiteUrl: { type: String, trim: true },
32-
videoConferenceLink: { type: String },
33-
lookingDescription: { type: String }, // narrative on what the project is looking for
34-
recruitingCategories: [{ type: String }], // same as global Skills picklist
35-
partners: [{ type: String }], // any third-party partners on the project, e.g. City of LA
36-
managedByUsers: [{ type: String }] // Which users may manage this project.
19+
name: { type: String, trim: true },
20+
description: { type: String, trim: true },
21+
githubIdentifier: { type: String, trim: true },
22+
projectStatus: { type: String }, // Active, Completed, or Paused
23+
location: { type: String, trim: true }, // DTLA, Westside, South LA, or Remote (hacknight)
24+
//teamMembers: { type: String }, // commented since we should be able to get this from Project Team Members table
25+
createdDate: { type: Date, default: Date.now }, // date/time project was created
26+
completedDate: { type: Date }, // only if Status = Completed, date/time completed
27+
githubUrl: { type: String, trim: true }, // link to main repo
28+
slackUrl: { type: String, trim: true }, // link to Slack channel
29+
googleDriveUrl: { type: String, trim: true },
30+
googleDriveId: { type: String },
31+
hflaWebsiteUrl: { type: String, trim: true },
32+
videoConferenceLink: { type: String },
33+
lookingDescription: { type: String }, // narrative on what the project is looking for
34+
recruitingCategories: [{ type: String }], // same as global Skills picklist
35+
partners: [{ type: String }], // any third-party partners on the project, e.g. City of LA
36+
managedByUsers: [{ type: String }], // Which users may manage this project.
37+
onboardOffboardVisible: { type: Boolean, default: true }, // Whether onboarding/offboarding forms are visible on the project page
3738
});
3839

39-
projectSchema.methods.serialize = function() {
40-
return {
41-
id: this._id,
42-
name: this.name,
43-
description: this.description,
44-
githubIdentifier: this.githubIdentifier,
45-
// owner: this.owner,
46-
projectStatus: this.projectStatus,
47-
location: this.location,
48-
//teamMembers: this.teamMembers,
49-
createdDate: this.createdDate,
50-
completedDate: this.completedDate,
51-
githubUrl: this.githubUrl,
52-
slackUrl: this.slackUrl,
53-
googleDriveUrl: this.googleDriveUrl,
54-
googleDriveId: this.googleDriveId,
55-
hflaWebsiteUrl: this.hflaWebsiteUrl,
56-
videoConferenceLink: this.videoConferenceLink,
57-
lookingDescription: this.lookingDescription,
58-
recruitingCategories: this.recruitingCategories,
59-
partners: this.partners,
60-
managedByUsers: this.managedByUsers
61-
};
40+
projectSchema.methods.serialize = function () {
41+
return {
42+
id: this._id,
43+
name: this.name,
44+
description: this.description,
45+
githubIdentifier: this.githubIdentifier,
46+
// owner: this.owner,
47+
projectStatus: this.projectStatus,
48+
location: this.location,
49+
//teamMembers: this.teamMembers,
50+
createdDate: this.createdDate,
51+
completedDate: this.completedDate,
52+
githubUrl: this.githubUrl,
53+
slackUrl: this.slackUrl,
54+
googleDriveUrl: this.googleDriveUrl,
55+
googleDriveId: this.googleDriveId,
56+
hflaWebsiteUrl: this.hflaWebsiteUrl,
57+
videoConferenceLink: this.videoConferenceLink,
58+
lookingDescription: this.lookingDescription,
59+
recruitingCategories: this.recruitingCategories,
60+
partners: this.partners,
61+
managedByUsers: this.managedByUsers,
62+
onboardOffboardVisible: this.onboardOffboardVisible,
63+
};
6264
};
6365

6466
const Project = mongoose.model('Project', projectSchema);

backend/routers/projects.router.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,7 @@ router.patch('/:ProjectId', AuthUtil.verifyCookie, ProjectController.updateManag
2222
// Bulk update for editing project members
2323
router.post('/bulk-updates', AuthUtil.verifyCookie, ProjectController.bulkUpdateManagedByUsers);
2424

25+
// Update onboard/offboard visibility for a project
26+
router.patch('/:ProjectId/visibility', AuthUtil.verifyCookie, ProjectController.updateOnboardOffboardVisibility);
27+
2528
module.exports = router;

client/src/App.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import UserWelcome from './pages/UserWelcome';
2929
// Added User Permission Search component
3030
import UserPermissionSearch from './pages/UserPermissionSearch';
3131
import UserPermission from './pages/UserPermission';
32+
import OnboardOffboardVisibility from './pages/OnboardOffboardVisibility';
3233

3334
import { Box, ThemeProvider } from '@mui/material';
3435
import theme from './theme';
@@ -70,6 +71,11 @@ const routes = [
7071
name: 'useradmin',
7172
Component: UserPermission,
7273
},
74+
{
75+
path: '/projects/visibility',
76+
name: 'onboardoffboardvisibility',
77+
Component: withAuth(OnboardOffboardVisibility),
78+
},
7379
{
7480
path: '/projects/:projectId',
7581
name: 'project',

client/src/api/ProjectApiService.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,21 @@ class ProjectApiService {
148148
alert('Server not responding. Please refresh the page.');
149149
}
150150
}
151+
152+
async updateOnboardOffboardVisibility(projectId, onboardOffboardVisible) {
153+
const url = `${this.baseProjectUrl}${projectId}/visibility`;
154+
try {
155+
const res = await fetch(url, {
156+
method: 'PATCH',
157+
headers: this.headers,
158+
body: JSON.stringify({ onboardOffboardVisible }),
159+
});
160+
return await res.json();
161+
} catch (error) {
162+
console.error(`updateOnboardOffboardVisibility error: ${error}`);
163+
alert('Server not responding. Please refresh the page.');
164+
}
165+
}
151166
}
152167

153168
export default ProjectApiService;

client/src/components/manageProjects/editProject.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,10 @@ const EditProject = ({
108108
setFormData={setFormData}
109109
/>
110110

111-
<TitledBoxIFrame projectName={projectToEdit.name} />
111+
{/* Only show onboarding/offboarding forms if visibility is enabled */}
112+
{projectToEdit.onboardOffboardVisible !== false && (
113+
<TitledBoxIFrame projectName={projectToEdit.name} />
114+
)}
112115

113116
{/* Insert Project Members (Event Editors) here */}
114117
<EditProjectMembers projectToEdit={projectToEdit} />
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { useState, useEffect } from 'react';
2+
import ProjectApiService from '../api/ProjectApiService';
3+
import TitledBox from '../components/parts/boxes/TitledBox';
4+
import {
5+
Box,
6+
CircularProgress,
7+
Typography,
8+
Switch,
9+
Tooltip,
10+
} from '@mui/material';
11+
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
12+
13+
/**
14+
* On/Offboard Visibility Page
15+
*
16+
* Allows Admins and Super Admins to control whether the onboarding/offboarding
17+
* forms are visible on individual project pages.
18+
*
19+
* Only accessible to users with accessLevel 'admin' or 'superadmin'.
20+
*/
21+
export default function OnboardOffboardVisibility({ auth }) {
22+
const [projects, setProjects] = useState(null);
23+
const [projectApiService] = useState(new ProjectApiService());
24+
const [loading, setLoading] = useState(false);
25+
26+
const user = auth?.user;
27+
28+
// Fetch all projects on component mount
29+
useEffect(() => {
30+
async function fetchAllProjects() {
31+
try {
32+
const projectData = await projectApiService.fetchProjects();
33+
// Sort projects alphabetically
34+
const sortedProjects = projectData.sort((a, b) =>
35+
a.name?.localeCompare(b.name)
36+
);
37+
setProjects(sortedProjects);
38+
} catch (error) {
39+
console.error('Error fetching projects:', error);
40+
}
41+
}
42+
43+
fetchAllProjects();
44+
}, [projectApiService]);
45+
46+
// Handle toggle switch change
47+
const handleVisibilityToggle = async (projectId, currentValue) => {
48+
setLoading(true);
49+
try {
50+
await projectApiService.updateOnboardOffboardVisibility(
51+
projectId,
52+
!currentValue
53+
);
54+
55+
// Update local state
56+
setProjects((prevProjects) =>
57+
prevProjects.map((project) =>
58+
project._id === projectId
59+
? { ...project, onboardOffboardVisible: !currentValue }
60+
: project
61+
)
62+
);
63+
} catch (error) {
64+
console.error('Error updating visibility:', error);
65+
} finally {
66+
setLoading(false);
67+
}
68+
};
69+
70+
// Show loading spinner while projects are being fetched
71+
if (!projects) {
72+
return (
73+
<Box sx={{ textAlign: 'center', pt: 10 }}>
74+
<CircularProgress />
75+
</Box>
76+
);
77+
}
78+
79+
// Only allow admin or superadmin access
80+
if (user?.accessLevel !== 'admin' && user?.accessLevel !== 'superadmin') {
81+
return (
82+
<Box sx={{ px: 1, py: 3 }}>
83+
<Typography variant="h2" textAlign="center" color="error">
84+
Access Denied
85+
</Typography>
86+
<Typography textAlign="center" sx={{ mt: 2 }}>
87+
You do not have permission to view this page.
88+
</Typography>
89+
</Box>
90+
);
91+
}
92+
93+
return (
94+
<Box sx={{ px: 1, py: 2 }}>
95+
{/* Page Title */}
96+
<Box sx={{ my: 3 }}>
97+
<Typography variant="h1" textAlign="center">
98+
On / Offboard Visibility
99+
</Typography>
100+
</Box>
101+
102+
{/* Projects Table in TitledBox */}
103+
<TitledBox
104+
title="Projects"
105+
badge={
106+
<Box
107+
sx={{
108+
display: 'flex',
109+
alignItems: 'center',
110+
gap: 1,
111+
minWidth: '150px',
112+
justifyContent: 'center',
113+
}}
114+
>
115+
<Typography
116+
sx={{
117+
fontWeight: 'bold',
118+
fontSize: '16px',
119+
}}
120+
>
121+
Visibility
122+
</Typography>
123+
<Tooltip
124+
title="This feature allows Admins and Super Admins to control whether the onboarding/offboarding forms are visible on project pages. Setting a project's visibility to 'No' hides these forms from all users on the associated project's project management page."
125+
arrow
126+
placement="left"
127+
>
128+
<InfoOutlinedIcon
129+
sx={{
130+
fontSize: '20px',
131+
color: '#666',
132+
cursor: 'pointer',
133+
'&:hover': { color: '#333' },
134+
}}
135+
/>
136+
</Tooltip>
137+
</Box>
138+
}
139+
childrenBoxSx={{ p: 0 }}
140+
>
141+
{/* Project List */}
142+
<Box sx={{ maxHeight: '60vh', overflowY: 'auto' }}>
143+
{projects.map((project) => (
144+
<Box
145+
key={project._id}
146+
sx={{
147+
display: 'grid',
148+
gridTemplateColumns: '1fr auto',
149+
px: 2,
150+
py: 1.5,
151+
borderBottom: '1px solid #e0e0e0',
152+
'&:hover': { backgroundColor: '#e8e8e8' },
153+
}}
154+
>
155+
<Typography
156+
sx={{ fontSize: '14px', display: 'flex', alignItems: 'center' }}
157+
>
158+
{project.name}
159+
</Typography>
160+
<Box
161+
sx={{
162+
display: 'flex',
163+
alignItems: 'center',
164+
justifyContent: 'center',
165+
gap: 1,
166+
minWidth: '150px',
167+
}}
168+
>
169+
<Typography sx={{ fontSize: '14px', minWidth: '35px' }}>
170+
{project.onboardOffboardVisible !== false ? 'Yes' : 'No'}
171+
</Typography>
172+
<Switch
173+
checked={project.onboardOffboardVisible !== false}
174+
onChange={() =>
175+
handleVisibilityToggle(
176+
project._id,
177+
project.onboardOffboardVisible !== false
178+
)
179+
}
180+
disabled={loading}
181+
color="primary"
182+
/>
183+
</Box>
184+
</Box>
185+
))}
186+
</Box>
187+
</TitledBox>
188+
</Box>
189+
);
190+
}

client/src/pages/ProjectList.jsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,23 @@ export default function ProjectList({ auth }) {
8383
</Box>
8484

8585
{(user?.accessLevel === 'admin' || user?.accessLevel === 'superadmin') && (
86-
<Box sx={{ textAlign: 'center' }}>
86+
<Box sx={{ display: 'grid', gridTemplateColumns: 'max-content', gap: 2, justifyContent: 'center', mx: 'auto' }}>
8787
<Button
8888
component={Link}
8989
to="/projects/create"
9090
variant="secondary"
91-
sx={{ mb: 3, px: 4 }}
91+
sx={{ px: 4 }}
9292
>
9393
Add a New Project
9494
</Button>
95+
<Button
96+
component={Link}
97+
to="/projects/visibility"
98+
variant="secondary"
99+
sx={{ px: 4 }}
100+
>
101+
On / Offboard Visibility
102+
</Button>
95103
</Box>
96104
)}
97105

0 commit comments

Comments
 (0)