Skip to content

Commit ffdbfdc

Browse files
authored
Merge pull request #1105 from concord-consortium/allow-project-admins-to-see-projects
Allow project admins to see and manage list of projects they admin
2 parents 652cdc2 + 73a7b5b commit ffdbfdc

File tree

6 files changed

+112
-23
lines changed

6 files changed

+112
-23
lines changed

app/assets/javascripts/lara-typescript.js

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124041,7 +124041,7 @@ var ProjectListItem = function (_a) {
124041124041

124042124042
var handleDeleteButtonClick = function () {
124043124043
if (id) {
124044-
onDelete(id);
124044+
onDelete === null || onDelete === void 0 ? void 0 : onDelete(id);
124045124045
}
124046124046
};
124047124047

@@ -124053,7 +124053,7 @@ var ProjectListItem = function (_a) {
124053124053
}, title), React.createElement("menu", null, React.createElement("ul", null, React.createElement("li", null, React.createElement("button", {
124054124054
className: "textButton editButton",
124055124055
onClick: handleEditButtonClick
124056-
}, "Edit")), React.createElement("li", null, React.createElement("button", {
124056+
}, "Edit")), onDelete && React.createElement("li", null, React.createElement("button", {
124057124057
className: "textButton deleteButton",
124058124058
onClick: handleDeleteButtonClick
124059124059
}, "Delete")))), React.createElement("div", {
@@ -124250,20 +124250,27 @@ var convert_keys_1 = __webpack_require__(/*! ../../shared/convert-keys */ "./src
124250124250
__webpack_require__(/*! ./project-list.scss */ "./src/projects/components/project-list.scss");
124251124251

124252124252
var ProjectList = function () {
124253-
var _a = (0, react_1.useState)(null),
124254-
projects = _a[0],
124255-
setProjects = _a[1];
124253+
var _a = (0, react_1.useState)(false),
124254+
isAdmin = _a[0],
124255+
setIsAdmin = _a[1];
124256124256

124257-
var _b = (0, react_1.useState)(false),
124258-
projectsUpdated = _b[0],
124259-
setProjectsUpdated = _b[1];
124257+
var _b = (0, react_1.useState)(null),
124258+
projects = _b[0],
124259+
setProjects = _b[1];
124260124260

124261-
var _c = (0, react_1.useState)(undefined),
124262-
alertMessage = _c[0],
124263-
setAlertMessage = _c[1];
124261+
var _c = (0, react_1.useState)(false),
124262+
projectsUpdated = _c[0],
124263+
setProjectsUpdated = _c[1];
124264+
124265+
var _d = (0, react_1.useState)(undefined),
124266+
alertMessage = _d[0],
124267+
setAlertMessage = _d[1];
124264124268

124265124269
var urlParams = new URLSearchParams(window.location.search);
124266124270
var newProjectCreated = urlParams.get("newProjectCreated");
124271+
(0, react_1.useEffect)(function () {
124272+
checkUser();
124273+
}, []);
124267124274
(0, react_1.useEffect)(function () {
124268124275
getProjects();
124269124276
setProjectsUpdated(false);
@@ -124273,6 +124280,38 @@ var ProjectList = function () {
124273124280
}
124274124281
}, [projectsUpdated]);
124275124282

124283+
var checkUser = function () {
124284+
return __awaiter(void 0, void 0, void 0, function () {
124285+
var apiUrl, data;
124286+
return __generator(this, function (_a) {
124287+
switch (_a.label) {
124288+
case 0:
124289+
apiUrl = "/api/v1/user_check";
124290+
return [4
124291+
/*yield*/
124292+
, fetch(apiUrl, {
124293+
method: "GET",
124294+
headers: {
124295+
"Content-Type": "application/json"
124296+
},
124297+
credentials: "include"
124298+
}).then(function (response) {
124299+
return response.json();
124300+
}).catch(function (error) {
124301+
setAlertMessage(error);
124302+
})];
124303+
124304+
case 1:
124305+
data = _a.sent();
124306+
setIsAdmin(data.user.is_admin);
124307+
return [2
124308+
/*return*/
124309+
];
124310+
}
124311+
});
124312+
});
124313+
};
124314+
124276124315
var getProjects = function () {
124277124316
return __awaiter(void 0, void 0, void 0, function () {
124278124317
var apiUrl, data;
@@ -124385,7 +124424,7 @@ var ProjectList = function () {
124385124424
id: project.id,
124386124425
title: project.title,
124387124426
url: project.url,
124388-
onDelete: handleDeleteRequest
124427+
onDelete: isAdmin ? handleDeleteRequest : undefined
124389124428
});
124390124429
})));
124391124430
};

app/controllers/api/v1/projects_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ class Api::V1::ProjectsController < API::APIController
44

55
# GET /api/v1/projects
66
def index
7-
@projects = Project.all
8-
authorize! :manage, @projects
7+
@projects = Project.visible_to_user(current_user)
8+
authorize! :manage, Project
99
render json: {projects: @projects}
1010
end
1111

app/models/ability.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ def initialize(user)
5757
user.id == plugin.plugin_scope.user_id || user.project_admin_of?(plugin.plugin_scope.project)
5858
end
5959

60+
can :manage, Project do |project|
61+
user.project_admin_of?(project)
62+
end
63+
6064
can :create, Sequence
6165
can :duplicate, Sequence do |sequence|
6266
user.id == sequence.user_id || user.project_admin_of?(sequence.project) || ['public', 'hidden'].include?(sequence.publication_status)

lara-typescript/src/projects/components/project-list-item.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export interface IProjectListItemProps {
66
id?: number;
77
title: string;
88
url: string | undefined;
9-
onDelete: (id: number) => void;
9+
onDelete?: (id: number) => void;
1010
}
1111

1212
export const ProjectListItem: React.FC<IProjectListItemProps> = ({
@@ -22,7 +22,7 @@ export const ProjectListItem: React.FC<IProjectListItemProps> = ({
2222

2323
const handleDeleteButtonClick = () => {
2424
if (id) {
25-
onDelete(id);
25+
onDelete?.(id);
2626
}
2727
};
2828

@@ -32,7 +32,7 @@ export const ProjectListItem: React.FC<IProjectListItemProps> = ({
3232
<menu>
3333
<ul>
3434
<li><button className="textButton editButton" onClick={handleEditButtonClick}>Edit</button></li>
35-
<li><button className="textButton deleteButton" onClick={handleDeleteButtonClick}>Delete</button></li>
35+
{onDelete && <li><button className="textButton deleteButton" onClick={handleDeleteButtonClick}>Delete</button></li>}
3636
</ul>
3737
</menu>
3838
<div className="projectListItem__link">

lara-typescript/src/projects/components/project-list.spec.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,24 +58,50 @@ describe("Project list component", () => {
5858
expect(ProjectList).toBeDefined();
5959
});
6060

61-
it("renders a list of projects", async () => {
62-
fetch.mockResponseOnce(JSON.stringify({projects: [project1, project2]}));
61+
it("renders a list of projects with the delete option as an admin", async () => {
62+
fetch
63+
.mockResponseOnce(JSON.stringify({user: {is_admin: true}}))
64+
.mockResponseOnce(JSON.stringify({projects: [project1, project2]}));
6365
await act(async () => {
6466
ReactDOM.render(<ProjectList />, container);
6567
});
6668
const pageTitle = container.querySelector("h1") as HTMLHeadingElement;
6769
const projectList = container.querySelector(".projectList") as HTMLUListElement;
68-
expect(fetch).toHaveBeenCalledTimes(1);
70+
expect(fetch).toHaveBeenCalledTimes(2);
6971
expect(pageTitle.textContent).toBe("Projects");
7072
expect(projectList.children).toHaveLength(2);
7173
const projectListItem1 = projectList.children[0] as HTMLLIElement;
7274
const projectListItem2 = projectList.children[1] as HTMLLIElement;
7375
expect(projectListItem1.innerHTML).toContain("Test Project 1");
7476
expect(projectListItem2.innerHTML).toContain("Test Project 2");
77+
expect(projectListItem1.innerHTML).toContain("delete");
78+
expect(projectListItem2.innerHTML).toContain("delete");
79+
});
80+
81+
it("renders a list of projects without the delete option as a non-admin", async () => {
82+
fetch
83+
.mockResponseOnce(JSON.stringify({user: {is_admin: false}}))
84+
.mockResponseOnce(JSON.stringify({projects: [project1, project2]}));
85+
await act(async () => {
86+
ReactDOM.render(<ProjectList />, container);
87+
});
88+
const pageTitle = container.querySelector("h1") as HTMLHeadingElement;
89+
const projectList = container.querySelector(".projectList") as HTMLUListElement;
90+
expect(fetch).toHaveBeenCalledTimes(2);
91+
expect(pageTitle.textContent).toBe("Projects");
92+
expect(projectList.children).toHaveLength(2);
93+
const projectListItem1 = projectList.children[0] as HTMLLIElement;
94+
const projectListItem2 = projectList.children[1] as HTMLLIElement;
95+
expect(projectListItem1.innerHTML).toContain("Test Project 1");
96+
expect(projectListItem2.innerHTML).toContain("Test Project 2");
97+
expect(projectListItem1.innerHTML).not.toContain("delete");
98+
expect(projectListItem2.innerHTML).not.toContain("delete");
7599
});
76100

77101
it("renders an updated projects list after a project is deleted", async () => {
78-
fetch.mockResponseOnce(JSON.stringify({projects: [project1, project2]}));
102+
fetch
103+
.mockResponseOnce(JSON.stringify({user: {is_admin: true}}))
104+
.mockResponseOnce(JSON.stringify({projects: [project1, project2]}));
79105
await act(async () => {
80106
ReactDOM.render(<ProjectList />, container);
81107
});
@@ -88,7 +114,7 @@ describe("Project list component", () => {
88114
project1DeleteButton.dispatchEvent(new MouseEvent("click", {bubbles: true}));
89115
});
90116
expect(global.confirm).toHaveBeenCalledTimes(1);
91-
expect(fetch).toHaveBeenCalledTimes(4);
117+
expect(fetch).toHaveBeenCalledTimes(5);
92118
expect(projectList.children).toHaveLength(1);
93119
});
94120
});

lara-typescript/src/projects/components/project-list.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ import { snakeToCamelCaseKeys } from "../../shared/convert-keys";
77
import "./project-list.scss";
88

99
export const ProjectList: React.FC = () => {
10+
const [isAdmin, setIsAdmin] = useState(false);
1011
const [projects, setProjects] = useState<IProject[]|null>(null);
1112
const [projectsUpdated, setProjectsUpdated] = useState(false);
1213
const [alertMessage, setAlertMessage] = useState<string|undefined>(undefined);
1314

1415
const urlParams = new URLSearchParams(window.location.search);
1516
const newProjectCreated = urlParams.get("newProjectCreated");
1617

18+
useEffect(() => {
19+
checkUser();
20+
}, []);
21+
1722
useEffect(() => {
1823
getProjects();
1924
setProjectsUpdated(false);
@@ -22,6 +27,21 @@ export const ProjectList: React.FC = () => {
2227
}
2328
}, [projectsUpdated]);
2429

30+
const checkUser = async () => {
31+
const apiUrl = `/api/v1/user_check`;
32+
const data = await fetch(apiUrl, {
33+
method: "GET",
34+
headers: {"Content-Type": "application/json"},
35+
credentials: "include"
36+
})
37+
.then(response => response.json())
38+
.catch(error => {
39+
setAlertMessage(error);
40+
});
41+
42+
setIsAdmin(data.user.is_admin);
43+
};
44+
2545
const getProjects = async () => {
2646
const apiUrl = `/api/v1/projects`;
2747
const data = await fetch(apiUrl, {
@@ -92,7 +112,7 @@ export const ProjectList: React.FC = () => {
92112
id={project.id}
93113
title={project.title}
94114
url={project.url}
95-
onDelete={handleDeleteRequest}
115+
onDelete={isAdmin ? handleDeleteRequest : undefined}
96116
/>
97117
))}
98118
</ul>

0 commit comments

Comments
 (0)