Skip to content

Commit b6a22a6

Browse files
committed
feat(edit-unit): add option to change a unit's default product privacy
1 parent 254b764 commit b6a22a6

File tree

6 files changed

+154
-15
lines changed

6 files changed

+154
-15
lines changed

components/ManageUsers.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,22 @@ export interface ManageUsersProps {
1010
* Array of current users
1111
*/
1212
users: string[];
13+
/**
14+
* Users that will be displayed but not selectable
15+
*/
16+
disabledUsers?: string[];
1317
/**
1418
* Whether the component should be in a loading state
1519
*/
1620
isLoading?: boolean;
21+
/**
22+
* Whether the field should be disabled. Overridden by loading state.
23+
*/
24+
disabled?: boolean;
25+
/**
26+
* Text to display under the field
27+
*/
28+
helperText?: string;
1729
/**
1830
* Text used for component ID and placeholder text, E.g. "editors".
1931
*/
@@ -35,8 +47,11 @@ export interface ManageUsersProps {
3547
*/
3648
export const ManageUsers: FC<ManageUsersProps> = ({
3749
users,
50+
disabledUsers = [],
3851
isLoading = false,
52+
disabled = false,
3953
title,
54+
helperText,
4055
onSelect,
4156
onRemove,
4257
}) => {
@@ -60,17 +75,20 @@ export const ManageUsers: FC<ManageUsersProps> = ({
6075
}
6176
};
6277

78+
// TODO: when removing yourself, allow a warning dialog to be displayed
79+
6380
return (
6481
<Autocomplete
6582
disableClearable
6683
freeSolo
6784
fullWidth
6885
multiple
69-
disabled={loading}
86+
disabled={disabled || loading}
87+
getOptionDisabled={(user) => disabledUsers.includes(user)}
7088
id={title.toLowerCase().replace(/\s/gu, "")}
7189
loading={loading}
7290
options={availableUsers.map((user) => user.username)}
73-
renderInput={(params) => <TextField {...params} label={title} />}
91+
renderInput={(params) => <TextField {...params} helperText={helperText} label={title} />}
7492
renderTags={(value, getTagProps) =>
7593
value.map((option: string, index: number) => {
7694
const { onDelete, ...chipProps } = getTagProps({ index });
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { type UnitDetail, UnitDetailDefaultProductPrivacy } from "@squonk/account-server-client";
2+
import {
3+
getGetUnitQueryKey,
4+
getGetUnitsQueryKey,
5+
usePatchUnit,
6+
} from "@squonk/account-server-client/unit";
7+
8+
import { MenuItem, TextField } from "@mui/material";
9+
import { useQueryClient } from "@tanstack/react-query";
10+
11+
import { useEnqueueError } from "../../hooks/useEnqueueStackError";
12+
import { useKeycloakUser } from "../../hooks/useKeycloakUser";
13+
import { useSelectedOrganisation } from "../../state/organisationSelection";
14+
import { useSelectedUnit } from "../../state/unitSelection";
15+
import { capitalise, shoutSnakeToLowerCase } from "../../utils/app/language";
16+
17+
export interface EditDefaultPrivacyProps {
18+
unit: UnitDetail;
19+
}
20+
21+
export const EditDefaultPrivacy = ({ unit }: EditDefaultPrivacyProps) => {
22+
const { user } = useKeycloakUser();
23+
24+
const [organisation] = useSelectedOrganisation();
25+
const [, setUnit] = useSelectedUnit();
26+
27+
const { mutateAsync: patchUnit, isPending } = usePatchUnit();
28+
const { enqueueError, enqueueSnackbar } = useEnqueueError();
29+
const queryClient = useQueryClient();
30+
31+
const handleSelection = async (newValue: UnitDetailDefaultProductPrivacy) => {
32+
try {
33+
await patchUnit({
34+
unitId: unit.id,
35+
data: {
36+
default_product_privacy: newValue,
37+
},
38+
});
39+
await queryClient.invalidateQueries({ queryKey: getGetUnitsQueryKey() });
40+
await queryClient.invalidateQueries({ queryKey: getGetUnitQueryKey(unit.id) });
41+
enqueueSnackbar("Unit default privacy updated", { variant: "success" });
42+
43+
const newUnit = { ...unit, default_product_privacy: newValue } satisfies UnitDetail;
44+
setUnit(newUnit);
45+
} catch (error) {
46+
enqueueError(error);
47+
}
48+
};
49+
50+
// const isOrganisationMember = organisation?.caller_is_member;
51+
const isUnitOwner = unit.owner_id === user.username;
52+
const isPersonalUnit = organisation?.name === process.env.NEXT_PUBLIC_DEFAULT_ORG_NAME;
53+
54+
// const allowedToEdit = !isPersonalUnit && (isUnitOwner || isOrganisationMember);
55+
const allowedToEdit = !isPersonalUnit && isUnitOwner;
56+
57+
const helperText = isPersonalUnit
58+
? "Default project privacy of personal units may not be changed"
59+
: allowedToEdit
60+
? undefined
61+
: "You must be the unit owner or have the admin role to edit the unit default project privacy";
62+
63+
return (
64+
<TextField
65+
select
66+
disabled={isPending || !allowedToEdit}
67+
helperText={helperText}
68+
label="Default project privacy"
69+
value={unit.default_product_privacy}
70+
onChange={(event) =>
71+
void handleSelection(event.target.value as UnitDetailDefaultProductPrivacy)
72+
}
73+
>
74+
{Object.values(UnitDetailDefaultProductPrivacy).map((rule) => (
75+
<MenuItem key={rule} value={rule}>
76+
{capitalise(shoutSnakeToLowerCase(rule))}
77+
</MenuItem>
78+
))}
79+
</TextField>
80+
);
81+
};

components/units/EditUnit.tsx renamed to components/units/EditUnitName.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useQueryClient } from "@tanstack/react-query";
1414
import { type AxiosError } from "axios";
1515

1616
import { useEnqueueError } from "../../hooks/useEnqueueStackError";
17+
import { useKeycloakUser } from "../../hooks/useKeycloakUser";
1718
import { useSelectedOrganisation } from "../../state/organisationSelection";
1819
import { useSelectedUnit } from "../../state/unitSelection";
1920
import { getErrorMessage } from "../../utils/next/orvalError";
@@ -22,7 +23,9 @@ export interface EditUnitProps {
2223
unit: UnitDetail;
2324
}
2425

25-
export const EditUnit = ({ unit }: EditUnitProps) => {
26+
export const EditUnitName = ({ unit }: EditUnitProps) => {
27+
const { user } = useKeycloakUser();
28+
2629
const [organisation] = useSelectedOrganisation();
2730
const [, setUnit] = useSelectedUnit();
2831
const [name, setName] = useState(unit.name);
@@ -71,15 +74,33 @@ export const EditUnit = ({ unit }: EditUnitProps) => {
7174
}
7275
};
7376

77+
// const isOrganisationMember = organisation?.caller_is_member;
78+
const isUnitOwner = unit.owner_id === user.username;
79+
const isPersonalUnit = organisation?.name === process.env.NEXT_PUBLIC_DEFAULT_ORG_NAME;
80+
81+
// const allowedToEdit = !isPersonalUnit && (isUnitOwner || isOrganisationMember);
82+
const allowedToEdit = !isPersonalUnit && isUnitOwner;
83+
84+
const helperText = isPersonalUnit
85+
? "Names of personal units may not be changed"
86+
: allowedToEdit
87+
? undefined
88+
: "You must be the unit owner to edit the unit name";
89+
7490
return (
75-
<Box display="flex" gap={1}>
91+
<Box alignItems="baseline" display="flex" gap={1}>
7692
<TextField
7793
fullWidth
94+
disabled={!allowedToEdit}
95+
helperText={helperText}
7896
label="Unit Name"
7997
value={name}
8098
onChange={(e) => setName(e.target.value)}
8199
/>
82-
<Button disabled={isPending || name === unit.name} onClick={() => void updateHandler()}>
100+
<Button
101+
disabled={isPending || name === unit.name || !allowedToEdit}
102+
onClick={() => void updateHandler()}
103+
>
83104
Update
84105
</Button>
85106
</Box>

components/units/UnitEditors.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import {
77
} from "@squonk/account-server-client/user";
88
import { type DmError } from "@squonk/data-manager-client";
99

10+
import { Typography } from "@mui/material";
1011
import { useQueryClient } from "@tanstack/react-query";
1112

1213
import { useEnqueueError } from "../../hooks/useEnqueueStackError";
1314
import { useKeycloakUser } from "../../hooks/useKeycloakUser";
15+
import { useSelectedOrganisation } from "../../state/organisationSelection";
16+
import { CenterLoader } from "../CenterLoader";
1417
import { ManageUsers } from "../ManageUsers";
1518

1619
export interface UnitEditorsProps {
@@ -26,20 +29,33 @@ export interface UnitEditorsProps {
2629
export const UnitEditors = ({ unit }: UnitEditorsProps) => {
2730
const { user: currentUser } = useKeycloakUser();
2831

29-
const { data, isLoading: isUsersLoading } = useGetOrganisationUnitUsers(unit.id);
32+
const [organisation] = useSelectedOrganisation();
33+
34+
const { data, isLoading: isUsersLoading } = useGetOrganisationUnitUsers(unit.id, {
35+
query: { enabled: !!unit.caller_is_member || organisation?.caller_is_member },
36+
});
3037
const users = data?.users;
3138
const { mutateAsync: addEditor, isPending: isAdding } = useAddOrganisationUnitUser();
3239
const { mutateAsync: removeEditor, isPending: isRemoving } = useDeleteOrganisationUnitUser();
3340
const queryClient = useQueryClient();
3441

3542
const { enqueueError, enqueueSnackbar } = useEnqueueError<DmError>();
3643

44+
const isPersonalUnit = organisation?.name === process.env.NEXT_PUBLIC_DEFAULT_ORG_NAME;
45+
46+
if (isUsersLoading) {
47+
return <CenterLoader />;
48+
}
49+
3750
if (users && currentUser.username) {
3851
return (
3952
<ManageUsers
53+
disabled={isPersonalUnit}
54+
disabledUsers={[unit.owner_id]}
55+
helperText={isPersonalUnit ? "Editors of personal unit may not be changed" : undefined}
4056
isLoading={isAdding || isRemoving || isUsersLoading}
4157
title="Unit Editors"
42-
users={users.filter((user) => user.id !== currentUser.username).map((user) => user.id)}
58+
users={users.map((user) => user.id)}
4359
onRemove={async (value) => {
4460
const user = users.find((editor) => !value.includes(editor.id));
4561
if (user) {
@@ -75,5 +91,5 @@ export const UnitEditors = ({ unit }: UnitEditorsProps) => {
7591
/>
7692
);
7793
}
78-
return null;
94+
return <Typography>You must be a unit or organisation member to modify unit editors</Typography>;
7995
};

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { Edit as EditIcon } from "@mui/icons-material";
66
import { Box, ListItemButton, ListItemIcon, ListItemText, Typography } from "@mui/material";
77

88
import { ModalWrapper } from "../../../../../components/modals/ModalWrapper";
9-
import { EditUnit } from "../../../../../components/units/EditUnit";
9+
import { EditDefaultPrivacy } from "../../../../../components/units/EditDefaultPrivacy";
10+
import { EditUnitName } from "../../../../../components/units/EditUnitName";
1011
import { UnitEditors } from "../../../../../components/units/UnitEditors";
1112

1213
export interface EditUnitListItemProps {
@@ -32,10 +33,15 @@ export const EditUnitListItem = ({ unit }: EditUnitListItemProps) => {
3233
onClose={() => setOpen(false)}
3334
>
3435
<Box display="flex" flexDirection="column" gap={2}>
36+
<Typography variant="subtitle1">Owner: {unit.owner_id}</Typography>
3537
<Typography component="h3" variant="h4">
3638
Name
3739
</Typography>
38-
<EditUnit unit={unit} />
40+
<EditUnitName unit={unit} />
41+
<Typography component="h3" variant="h4">
42+
Default Project Privacy
43+
</Typography>
44+
<EditDefaultPrivacy unit={unit} />
3945
<Typography component="h3" variant="h4">
4046
Editors
4147
</Typography>

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,8 @@ export const UnitActions = () => {
3737
{!!isUnitOwner && !!unit && (
3838
<DeleteUnitListItem unit={unit} onDelete={() => setUnit(undefined)} />
3939
)}
40-
{!!isUnitOwner &&
41-
!!unit &&
42-
organisation?.name !== process.env.NEXT_PUBLIC_DEFAULT_ORG_NAME && (
43-
<EditUnitListItem unit={unit} />
44-
)}
40+
41+
{!!unit && <EditUnitListItem unit={unit} />}
4542
{!!unit && !!(unit.caller_is_member || unit.owner_id === user.username) && (
4643
<CreateProjectListItem unit={unit} />
4744
)}

0 commit comments

Comments
 (0)