Skip to content

Commit 0b67d23

Browse files
committed
feat!(observers): add option to edit a project's observers
BREAKING CHANGE: This requires DM API v1.1 or above
1 parent 5545339 commit 0b67d23

File tree

9 files changed

+135
-45
lines changed

9 files changed

+135
-45
lines changed

components/ManageEditors.tsx renamed to components/ManageUsers.tsx

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,52 @@ import { useGetUsers } from "@squonk/data-manager-client/user";
55
import { Autocomplete, Chip, TextField } from "@mui/material";
66
import type { AutocompleteChangeReason } from "@mui/material/useAutocomplete";
77

8-
export interface ManageEditorsProps {
8+
export interface ManageUsersProps {
99
/**
10-
* User's email
10+
* User's username
1111
*/
1212
currentUsername: string;
1313
/**
14-
* Array of current editors
14+
* Array of current users
1515
*/
16-
editorsValue: string[];
16+
users: string[];
1717
/**
1818
* Whether the component should be in a loading state
1919
*/
2020
isLoading?: boolean;
2121
/**
22-
* Called when a editor is selected
22+
* Text used for component ID and placeholder text, E.g. "editors".
23+
*/
24+
title: string;
25+
/**
26+
* Called when a user is selected
2327
*/
2428
onSelect: (value: string[]) => Promise<void> | void;
2529
/**
26-
* Called when an editor is removed
30+
* Called when a user is removed
2731
*/
2832
onRemove: (value: string[]) => Promise<void> | void;
2933
}
3034

3135
/**
32-
* MuiAutocomplete to manage the editors of a dataset
36+
* Selector input that manages a list of users.
37+
*
38+
* The current user is assumed to always be included.
3339
*/
34-
export const ManageEditors: FC<ManageEditorsProps> = ({
40+
export const ManageUsers: FC<ManageUsersProps> = ({
3541
currentUsername,
36-
editorsValue,
42+
users,
3743
isLoading = false,
44+
title,
3845
onSelect,
3946
onRemove,
4047
}) => {
4148
const { data, isLoading: isUsersLoading } = useGetUsers();
42-
const availableUsers = data?.users;
49+
const availableUsers = data?.users ?? [];
4350

4451
const loading = isUsersLoading || isLoading;
4552

46-
const updateEditors = async (value: string[], reason: AutocompleteChangeReason) => {
53+
const updateUsers = async (value: string[], reason: AutocompleteChangeReason) => {
4754
switch (reason) {
4855
case "selectOption": {
4956
// Isolate the user that has been added
@@ -58,18 +65,18 @@ export const ManageEditors: FC<ManageEditorsProps> = ({
5865
}
5966
};
6067

61-
return availableUsers ? (
68+
return (
6269
<Autocomplete
6370
disableClearable
6471
freeSolo
6572
fullWidth
6673
multiple
6774
disabled={loading}
6875
getOptionDisabled={(option) => option === currentUsername}
69-
id="editors"
76+
id={title.toLowerCase().replace(/\s/g, "")}
7077
loading={loading}
7178
options={availableUsers.map((user) => user.username)}
72-
renderInput={(params) => <TextField {...params} label="Editors" />}
79+
renderInput={(params) => <TextField {...params} label={title} />}
7380
renderTags={(value, getTagProps) =>
7481
value.map((option: string, index: number) => {
7582
const { onDelete, ...chipProps } = getTagProps({ index });
@@ -84,8 +91,8 @@ export const ManageEditors: FC<ManageEditorsProps> = ({
8491
);
8592
})
8693
}
87-
value={[currentUsername, ...editorsValue]}
88-
onChange={(_, value, reason) => updateEditors(value, reason)}
94+
value={[currentUsername, ...users]}
95+
onChange={(_, value, reason) => updateUsers(value, reason)}
8996
/>
90-
) : null;
97+
);
9198
};

components/projects/EditProjectButton/EditProjectButton.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { useState } from "react";
33
import type { ProjectDetail } from "@squonk/data-manager-client";
44

55
import { Edit as EditIcon } from "@mui/icons-material";
6-
import { IconButton, Tooltip, Typography } from "@mui/material";
6+
import { Box, IconButton, Tooltip, Typography } from "@mui/material";
77

88
import { useKeycloakUser } from "../../../hooks/useKeycloakUser";
99
import { ModalWrapper } from "../../modals/ModalWrapper";
1010
import { PrivateProjectToggle } from "./PrivateProjectToggle";
1111
import { ProjectEditors } from "./ProjectEditors";
12+
import { ProjectObservers } from "./ProjectObservers";
1213

1314
export interface EditProjectButtonProps {
1415
/**
@@ -55,7 +56,10 @@ export const EditProjectButton = ({ project }: EditProjectButtonProps) => {
5556

5657
<PrivateProjectToggle isPrivate={project.private} projectId={project.project_id} />
5758

58-
<ProjectEditors project={project} />
59+
<Box display="flex" flexDirection="column" gap={2}>
60+
<ProjectEditors project={project} />
61+
<ProjectObservers project={project} />
62+
</Box>
5963
</ModalWrapper>
6064
</>
6165
);

components/projects/EditProjectButton/ProjectEditors.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { useQueryClient } from "@tanstack/react-query";
1111

1212
import { useEnqueueError } from "../../../hooks/useEnqueueStackError";
1313
import { useKeycloakUser } from "../../../hooks/useKeycloakUser";
14-
import { ManageEditors } from "../../ManageEditors";
14+
import { ManageUsers } from "../../ManageUsers";
1515

1616
export interface ProjectEditorsProps {
1717
/**
@@ -35,10 +35,11 @@ export const ProjectEditors = ({ project }: ProjectEditorsProps) => {
3535

3636
if (currentUser.username) {
3737
return (
38-
<ManageEditors
38+
<ManageUsers
3939
currentUsername={currentUser.username}
40-
editorsValue={project.editors.filter((user) => user !== currentUser.username)}
4140
isLoading={isAdding || isRemoving || isProjectsLoading}
41+
title="Editors"
42+
users={project.editors.filter((user) => user !== currentUser.username)}
4243
onRemove={async (value) => {
4344
const username = project.editors.find((editor) => !value.includes(editor));
4445
if (username) {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { DmError, ProjectDetail } from "@squonk/data-manager-client";
2+
import {
3+
getGetProjectQueryKey,
4+
getGetProjectsQueryKey,
5+
useAddObserverToProject,
6+
useGetProjects,
7+
useRemoveObserverFromProject,
8+
} from "@squonk/data-manager-client/project";
9+
10+
import { useQueryClient } from "@tanstack/react-query";
11+
12+
import { useEnqueueError } from "../../../hooks/useEnqueueStackError";
13+
import { useKeycloakUser } from "../../../hooks/useKeycloakUser";
14+
import { ManageUsers } from "../../ManageUsers";
15+
16+
export interface ProjectObserversProps {
17+
/**
18+
* Project to be edited.
19+
*/
20+
project: ProjectDetail;
21+
}
22+
23+
/**
24+
* Selector component to manage observers of a project.
25+
*/
26+
export const ProjectObservers = ({ project }: ProjectObserversProps) => {
27+
const { user: currentUser } = useKeycloakUser();
28+
29+
const { isLoading: isProjectsLoading } = useGetProjects();
30+
const { mutateAsync: addObserver, isLoading: isAdding } = useAddObserverToProject();
31+
const { mutateAsync: removeObserver, isLoading: isRemoving } = useRemoveObserverFromProject();
32+
const queryClient = useQueryClient();
33+
34+
const { enqueueError, enqueueSnackbar } = useEnqueueError<DmError>();
35+
36+
if (currentUser.username) {
37+
return (
38+
<ManageUsers
39+
currentUsername={currentUser.username}
40+
isLoading={isAdding || isRemoving || isProjectsLoading}
41+
title="Observers"
42+
users={project.observers.filter((user) => user !== currentUser.username)}
43+
onRemove={async (value) => {
44+
const username = project.observers.find((user) => !value.includes(user));
45+
if (username) {
46+
try {
47+
await removeObserver({ projectId: project.project_id, userId: username });
48+
} catch (error) {
49+
enqueueError(error);
50+
}
51+
// DM Queries
52+
queryClient.invalidateQueries(getGetProjectQueryKey(project.project_id));
53+
queryClient.invalidateQueries(getGetProjectsQueryKey());
54+
} else {
55+
enqueueSnackbar("Username not found", { variant: "warning" });
56+
}
57+
}}
58+
onSelect={async (value) => {
59+
const username = value.find(
60+
(user) => user !== currentUser.username && !project.observers.includes(user),
61+
);
62+
if (username) {
63+
try {
64+
await addObserver({ projectId: project.project_id, userId: username });
65+
} catch (error) {
66+
enqueueError(error);
67+
}
68+
// DM Queries
69+
queryClient.invalidateQueries(getGetProjectQueryKey(project.project_id));
70+
queryClient.invalidateQueries(getGetProjectsQueryKey());
71+
} else {
72+
enqueueSnackbar("Username not found", { variant: "warning" });
73+
}
74+
}}
75+
/>
76+
);
77+
}
78+
return null;
79+
};

features/DatasetsTable/DatasetDetails/ManageDatasetEditorsSection/ManageDatasetEditorsSection.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import { useQueryClient } from "@tanstack/react-query";
1111

1212
import { CenterLoader } from "../../../../components/CenterLoader";
13-
import { ManageEditors } from "../../../../components/ManageEditors";
13+
import { ManageUsers } from "../../../../components/ManageUsers";
1414
import { useEnqueueError } from "../../../../hooks/useEnqueueStackError";
1515
import { useKeycloakUser } from "../../../../hooks/useKeycloakUser";
1616

@@ -43,10 +43,11 @@ export const ManageDatasetEditorsSection = ({ dataset }: ManageDatasetEditorsSec
4343
}
4444

4545
return (
46-
<ManageEditors
46+
<ManageUsers
4747
currentUsername={user.username}
48-
editorsValue={editors}
4948
isLoading={isLoading}
49+
title="Editors"
50+
users={editors}
5051
onRemove={async (value) => {
5152
setIsLoading(true);
5253
const username = dataset.editors.find((editor) => !value.includes(editor));

features/userSettings/UserSettingsContent/ContextSection/contextActions/OrganisationEditors.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { DmError } from "@squonk/data-manager-client";
99

1010
import { useQueryClient } from "@tanstack/react-query";
1111

12-
import { ManageEditors } from "../../../../../components/ManageEditors";
12+
import { ManageUsers } from "../../../../../components/ManageUsers";
1313
import { useEnqueueError } from "../../../../../hooks/useEnqueueStackError";
1414
import { useKeycloakUser } from "../../../../../hooks/useKeycloakUser";
1515

@@ -36,12 +36,11 @@ export const OrganisationEditors = ({ organisation }: UnitEditorsProps) => {
3636

3737
if (users && currentUser.username) {
3838
return (
39-
<ManageEditors
39+
<ManageUsers
4040
currentUsername={currentUser.username}
41-
editorsValue={users
42-
.filter((user) => user.id !== currentUser.username)
43-
.map((user) => user.id)}
4441
isLoading={isAdding || isRemoving || isUsersLoading}
42+
title="Organisation Editors"
43+
users={users.filter((user) => user.id !== currentUser.username).map((user) => user.id)}
4544
onRemove={async (value) => {
4645
const user = users.find((editor) => !value.includes(editor.id));
4746
if (user) {

features/userSettings/UserSettingsContent/ContextSection/contextActions/UnitEditors.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { DmError } from "@squonk/data-manager-client";
99

1010
import { useQueryClient } from "@tanstack/react-query";
1111

12-
import { ManageEditors } from "../../../../../components/ManageEditors";
12+
import { ManageUsers } from "../../../../../components/ManageUsers";
1313
import { useEnqueueError } from "../../../../../hooks/useEnqueueStackError";
1414
import { useKeycloakUser } from "../../../../../hooks/useKeycloakUser";
1515

@@ -36,12 +36,11 @@ export const UnitEditors = ({ unit }: UnitEditorsProps) => {
3636

3737
if (users && currentUser.username) {
3838
return (
39-
<ManageEditors
39+
<ManageUsers
4040
currentUsername={currentUser.username}
41-
editorsValue={users
42-
.filter((user) => user.id !== currentUser.username)
43-
.map((user) => user.id)}
4441
isLoading={isAdding || isRemoving || isUsersLoading}
42+
title="Unit Editors"
43+
users={users.filter((user) => user.id !== currentUser.username).map((user) => user.id)}
4544
onRemove={async (value) => {
4645
const user = users.find((editor) => !value.includes(editor.id));
4746
if (user) {

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@
5151
"@rjsf/core": "4.2.3",
5252
"@rjsf/material-ui": "4.2.3",
5353
"@sentry/nextjs": "7.38.0",
54-
"@squonk/account-server-client": "2.0.2",
55-
"@squonk/data-manager-client": "1.0.32",
54+
"@squonk/account-server-client": "2.0.3",
55+
"@squonk/data-manager-client": "1.1.0",
5656
"@squonk/mui-theme": "3.0.2",
5757
"@tanstack/match-sorter-utils": "8.7.6",
5858
"@tanstack/react-query": "4.24.9",

pnpm-lock.yaml

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)