mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
fix: remove group owner concept (#1210)
* fix: remove group owner concept * fix: adapt e2e tests accordingly * refactor users select to match improvement * refactor: add user -> edit users * feat: add edit users to group card actions * add a few more UI improvements * fix: edit group users icon * improve loading behaviour * fix group users refresh on card view * improvement: create group form validation * fix edit group, some refactoring * fix: e2e tests, minor bugs * fix: infinite re-renders due to useHiddenColumns useEffect array dependency * fix re-rendering on useHiddenColumns for some tables * refactor: validations into functions / variables
This commit is contained in:
parent
d3e853cf7f
commit
3200fee963
@ -39,23 +39,6 @@ describe('groups', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('gives an error if a group does not have an owner', () => {
|
||||
cy.get("[data-testid='NAVIGATE_TO_CREATE_GROUP']").click();
|
||||
|
||||
cy.intercept('POST', '/api/admin/groups').as('createGroup');
|
||||
|
||||
cy.get("[data-testid='UG_NAME_ID']").type(groupName);
|
||||
cy.get("[data-testid='UG_DESC_ID']").type('hello-world');
|
||||
cy.get("[data-testid='UG_USERS_ID']").click();
|
||||
cy.contains(`unleash-e2e-user1-${randomId}`).click();
|
||||
cy.get("[data-testid='UG_USERS_ADD_ID']").click();
|
||||
|
||||
cy.get("[data-testid='UG_CREATE_BTN_ID']").click();
|
||||
cy.get("[data-testid='TOAST_TEXT']").contains(
|
||||
'Group needs to have at least one Owner'
|
||||
);
|
||||
});
|
||||
|
||||
it('can create a group', () => {
|
||||
cy.get("[data-testid='NAVIGATE_TO_CREATE_GROUP']").click();
|
||||
|
||||
@ -65,9 +48,6 @@ describe('groups', () => {
|
||||
cy.get("[data-testid='UG_DESC_ID']").type('hello-world');
|
||||
cy.get("[data-testid='UG_USERS_ID']").click();
|
||||
cy.contains(`unleash-e2e-user1-${randomId}`).click();
|
||||
cy.get("[data-testid='UG_USERS_ADD_ID']").click();
|
||||
cy.get("[data-testid='UG_USERS_TABLE_ROLE_ID']").click();
|
||||
cy.contains('Owner').click();
|
||||
|
||||
cy.get("[data-testid='UG_CREATE_BTN_ID']").click();
|
||||
cy.wait('@createGroup');
|
||||
@ -80,16 +60,8 @@ describe('groups', () => {
|
||||
cy.intercept('POST', '/api/admin/groups').as('createGroup');
|
||||
|
||||
cy.get("[data-testid='UG_NAME_ID']").type(groupName);
|
||||
cy.get("[data-testid='UG_DESC_ID']").type('hello-world');
|
||||
cy.get("[data-testid='UG_USERS_ID']").click();
|
||||
cy.contains(`unleash-e2e-user1-${randomId}`).click();
|
||||
cy.get("[data-testid='UG_USERS_ADD_ID']").click();
|
||||
cy.get("[data-testid='UG_USERS_TABLE_ROLE_ID']").click();
|
||||
cy.contains('Owner').click();
|
||||
|
||||
cy.get("[data-testid='UG_CREATE_BTN_ID']").click();
|
||||
cy.get("[data-testid='TOAST_TEXT']").contains(
|
||||
'Group name already exists'
|
||||
cy.get("[data-testid='INPUT_ERROR_TEXT'").contains(
|
||||
'A group with that name already exists.'
|
||||
);
|
||||
});
|
||||
|
||||
@ -108,34 +80,15 @@ describe('groups', () => {
|
||||
it('can add user to a group', () => {
|
||||
cy.contains(groupName).click();
|
||||
|
||||
cy.get("[data-testid='UG_ADD_USER_BTN_ID']").click();
|
||||
cy.get("[data-testid='UG_EDIT_USERS_BTN_ID']").click();
|
||||
|
||||
cy.get("[data-testid='UG_USERS_ID']").click();
|
||||
cy.contains(`unleash-e2e-user2-${randomId}`).click();
|
||||
cy.get("[data-testid='UG_USERS_ADD_ID']").click();
|
||||
|
||||
cy.get("[data-testid='UG_SAVE_BTN_ID']").click();
|
||||
|
||||
cy.contains(`unleash-e2e-user1-${randomId}`);
|
||||
cy.contains(`unleash-e2e-user2-${randomId}`);
|
||||
cy.get("td span:contains('Owner')").should('have.length', 1);
|
||||
cy.get("td span:contains('Member')").should('have.length', 1);
|
||||
});
|
||||
|
||||
it('can edit user role in a group', () => {
|
||||
cy.contains(groupName).click();
|
||||
|
||||
cy.get(`[data-testid='UG_EDIT_USER_BTN_ID-${userIds[1]}']`).click();
|
||||
|
||||
cy.get("[data-testid='UG_USERS_ROLE_ID']").click();
|
||||
cy.get("li[data-value='Owner']").click();
|
||||
|
||||
cy.get("[data-testid='UG_SAVE_BTN_ID']").click();
|
||||
|
||||
cy.contains(`unleash-e2e-user1-${randomId}`);
|
||||
cy.contains(`unleash-e2e-user2-${randomId}`);
|
||||
cy.get("td span:contains('Owner')").should('have.length', 2);
|
||||
cy.contains('Member').should('not.exist');
|
||||
});
|
||||
|
||||
it('can remove user from a group', () => {
|
||||
|
@ -27,10 +27,14 @@ import { HighlightCell } from 'component/common/Table/cells/HighlightCell/Highli
|
||||
import { Search } from 'component/common/Search/Search';
|
||||
import useHiddenColumns from 'hooks/useHiddenColumns';
|
||||
|
||||
const hiddenColumnsSmall = ['Icon', 'createdAt'];
|
||||
const hiddenColumnsFlagE = ['projects', 'environment'];
|
||||
|
||||
export const ApiTokenTable = () => {
|
||||
const { tokens, loading } = useApiTokens();
|
||||
const initialState = useMemo(() => ({ sortBy: [{ id: 'createdAt' }] }), []);
|
||||
const { uiConfig } = useUiConfig();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
@ -53,16 +57,8 @@ export const ApiTokenTable = () => {
|
||||
useSortBy
|
||||
);
|
||||
|
||||
useHiddenColumns(
|
||||
setHiddenColumns,
|
||||
['Icon', 'createdAt'],
|
||||
useMediaQuery(theme.breakpoints.down('md'))
|
||||
);
|
||||
useHiddenColumns(
|
||||
setHiddenColumns,
|
||||
['projects', 'environment'],
|
||||
!uiConfig.flags.E
|
||||
);
|
||||
useHiddenColumns(setHiddenColumns, hiddenColumnsSmall, isSmallScreen);
|
||||
useHiddenColumns(setHiddenColumns, hiddenColumnsFlagE, !uiConfig.flags.E);
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
|
@ -10,6 +10,7 @@ import { UG_CREATE_BTN_ID } from 'utils/testIds';
|
||||
import { Button } from '@mui/material';
|
||||
import { CREATE } from 'constants/misc';
|
||||
import { GO_BACK } from 'constants/navigate';
|
||||
import { useGroups } from 'hooks/api/getters/useGroups/useGroups';
|
||||
|
||||
export const CreateGroup = () => {
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
@ -26,14 +27,18 @@ export const CreateGroup = () => {
|
||||
getGroupPayload,
|
||||
clearErrors,
|
||||
errors,
|
||||
setErrors,
|
||||
} = useGroupForm();
|
||||
|
||||
const { groups } = useGroups();
|
||||
const { createGroup, loading } = useGroupApi();
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
clearErrors();
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
const payload = getGroupPayload();
|
||||
try {
|
||||
const group = await createGroup(payload);
|
||||
@ -62,6 +67,19 @@ export const CreateGroup = () => {
|
||||
navigate(GO_BACK);
|
||||
};
|
||||
|
||||
const isNameEmpty = (name: string) => name.length;
|
||||
const isNameUnique = (name: string) =>
|
||||
!groups?.filter(group => group.name === name).length;
|
||||
const isValid = isNameEmpty(name) && isNameUnique(name);
|
||||
|
||||
const onSetName = (name: string) => {
|
||||
clearErrors();
|
||||
if (!isNameUnique(name)) {
|
||||
setErrors({ name: 'A group with that name already exists.' });
|
||||
}
|
||||
setName(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
@ -75,19 +93,19 @@ export const CreateGroup = () => {
|
||||
name={name}
|
||||
description={description}
|
||||
users={users}
|
||||
setName={setName}
|
||||
setName={onSetName}
|
||||
setDescription={setDescription}
|
||||
setUsers={setUsers}
|
||||
errors={errors}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
mode={CREATE}
|
||||
clearErrors={clearErrors}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={!isValid}
|
||||
data-testid={UG_CREATE_BTN_ID}
|
||||
>
|
||||
Create group
|
||||
|
@ -12,10 +12,12 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useGroup } from 'hooks/api/getters/useGroup/useGroup';
|
||||
import { UG_SAVE_BTN_ID } from 'utils/testIds';
|
||||
import { GO_BACK } from 'constants/navigate';
|
||||
import { useGroups } from 'hooks/api/getters/useGroups/useGroups';
|
||||
|
||||
export const EditGroup = () => {
|
||||
const groupId = Number(useRequiredPathParam('groupId'));
|
||||
const { group, refetchGroup } = useGroup(groupId);
|
||||
const { refetchGroups } = useGroups();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const navigate = useNavigate();
|
||||
@ -30,8 +32,10 @@ export const EditGroup = () => {
|
||||
getGroupPayload,
|
||||
clearErrors,
|
||||
errors,
|
||||
setErrors,
|
||||
} = useGroupForm(group?.name, group?.description, group?.users);
|
||||
|
||||
const { groups } = useGroups();
|
||||
const { updateGroup, loading } = useGroupApi();
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
@ -42,6 +46,7 @@ export const EditGroup = () => {
|
||||
try {
|
||||
await updateGroup(groupId, payload);
|
||||
refetchGroup();
|
||||
refetchGroups();
|
||||
navigate(GO_BACK);
|
||||
setToastData({
|
||||
title: 'Group updated successfully',
|
||||
@ -65,6 +70,20 @@ export const EditGroup = () => {
|
||||
navigate(GO_BACK);
|
||||
};
|
||||
|
||||
const isNameEmpty = (name: string) => name.length;
|
||||
const isNameUnique = (name: string) =>
|
||||
!groups?.filter(group => group.name === name && group.id !== groupId)
|
||||
.length;
|
||||
const isValid = isNameEmpty(name) && isNameUnique(name);
|
||||
|
||||
const onSetName = (name: string) => {
|
||||
clearErrors();
|
||||
if (!isNameUnique(name)) {
|
||||
setErrors({ name: 'A group with that name already exists.' });
|
||||
}
|
||||
setName(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
@ -78,19 +97,19 @@ export const EditGroup = () => {
|
||||
name={name}
|
||||
description={description}
|
||||
users={users}
|
||||
setName={setName}
|
||||
setName={onSetName}
|
||||
setDescription={setDescription}
|
||||
setUsers={setUsers}
|
||||
errors={errors}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
mode={EDIT}
|
||||
clearErrors={clearErrors}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={!isValid}
|
||||
data-testid={UG_SAVE_BTN_ID}
|
||||
>
|
||||
Save
|
||||
|
@ -1,183 +0,0 @@
|
||||
import { Button, MenuItem, Select, styled } from '@mui/material';
|
||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||
import { useGroupApi } from 'hooks/api/actions/useGroupApi/useGroupApi';
|
||||
import { useGroup } from 'hooks/api/getters/useGroup/useGroup';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { IGroup, IGroupUser, Role } from 'interfaces/group';
|
||||
import { FC, FormEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { UG_SAVE_BTN_ID, UG_USERS_ROLE_ID } from 'utils/testIds';
|
||||
|
||||
const StyledForm = styled('form')(() => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
const StyledUser = styled('div')(({ theme }) => ({
|
||||
width: '100%',
|
||||
maxWidth: theme.spacing(50),
|
||||
marginBottom: theme.spacing(2),
|
||||
padding: theme.spacing(1.5),
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
backgroundColor: theme.palette.secondaryContainer,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'& > span:first-of-type': {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledInputDescription = styled('p')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledSelect = styled(Select)(({ theme }) => ({
|
||||
width: '100%',
|
||||
maxWidth: theme.spacing(50),
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledButtonContainer = styled('div')(() => ({
|
||||
marginTop: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}));
|
||||
|
||||
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
||||
marginLeft: theme.spacing(3),
|
||||
}));
|
||||
|
||||
interface IEditGroupUserProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
user?: IGroupUser;
|
||||
group: IGroup;
|
||||
}
|
||||
|
||||
export const EditGroupUser: FC<IEditGroupUserProps> = ({
|
||||
open,
|
||||
setOpen,
|
||||
user,
|
||||
group,
|
||||
}) => {
|
||||
const { refetchGroup } = useGroup(group.id);
|
||||
const { updateGroup, loading } = useGroupApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
|
||||
const [role, setRole] = useState<Role>(user?.role || Role.Member);
|
||||
|
||||
useEffect(() => {
|
||||
setRole(user?.role || Role.Member);
|
||||
}, [user, open]);
|
||||
|
||||
const payload = useMemo(() => {
|
||||
const editUsers = [...group.users];
|
||||
const editUserIndex = editUsers.findIndex(({ id }) => id === user?.id);
|
||||
editUsers[editUserIndex] = {
|
||||
...user!,
|
||||
role,
|
||||
};
|
||||
return {
|
||||
name: group.name,
|
||||
description: group.description,
|
||||
users: editUsers.map(({ id, role }) => ({
|
||||
user: { id },
|
||||
role,
|
||||
})),
|
||||
};
|
||||
}, [group, user, role]);
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await updateGroup(group.id, payload);
|
||||
refetchGroup();
|
||||
setOpen(false);
|
||||
setToastData({
|
||||
title: 'User edited successfully',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
const formatApiCode = () => {
|
||||
return `curl --location --request PUT '${
|
||||
uiConfig.unleashUrl
|
||||
}/api/admin/groups/${group.id}' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(payload, undefined, 2)}'`;
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarModal
|
||||
open={open && Boolean(user)}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
label="Edit user"
|
||||
>
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
modal
|
||||
title="Edit user"
|
||||
description="Groups is the best and easiest way to organize users and then use them in projects to assign a specific role in one go to all the users in a group."
|
||||
documentationLink="https://docs.getunleash.io/advanced/groups"
|
||||
documentationLinkLabel="Groups documentation"
|
||||
formatApiCode={formatApiCode}
|
||||
>
|
||||
<StyledForm onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<StyledUser>
|
||||
<span>{user?.name || user?.username}</span>
|
||||
<span>{user?.email}</span>
|
||||
</StyledUser>
|
||||
<StyledInputDescription>
|
||||
Assign the role the user should have in this group
|
||||
</StyledInputDescription>
|
||||
<StyledSelect
|
||||
data-testid={UG_USERS_ROLE_ID}
|
||||
size="small"
|
||||
value={role}
|
||||
onChange={event =>
|
||||
setRole(event.target.value as Role)
|
||||
}
|
||||
>
|
||||
{Object.values(Role).map(role => (
|
||||
<MenuItem key={role} value={role}>
|
||||
{role}
|
||||
</MenuItem>
|
||||
))}
|
||||
</StyledSelect>
|
||||
</div>
|
||||
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
data-testid={UG_SAVE_BTN_ID}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<StyledCancelButton
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</StyledCancelButton>
|
||||
</StyledButtonContainer>
|
||||
</StyledForm>
|
||||
</FormTemplate>
|
||||
</SidebarModal>
|
||||
);
|
||||
};
|
@ -5,12 +5,14 @@ import { useGroupApi } from 'hooks/api/actions/useGroupApi/useGroupApi';
|
||||
import { useGroup } from 'hooks/api/getters/useGroup/useGroup';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { IGroup, IGroupUser } from 'interfaces/group';
|
||||
import { FC, FormEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { IGroup } from 'interfaces/group';
|
||||
import { FC, FormEvent, useEffect } from 'react';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { GroupFormUsersSelect } from 'component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect';
|
||||
import { GroupFormUsersTable } from 'component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable';
|
||||
import { UG_SAVE_BTN_ID } from 'utils/testIds';
|
||||
import { useGroupForm } from 'component/admin/groups/hooks/useGroupForm';
|
||||
import { useGroups } from 'hooks/api/getters/useGroups/useGroups';
|
||||
|
||||
const StyledForm = styled('form')(() => ({
|
||||
display: 'flex',
|
||||
@ -33,63 +35,43 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({
|
||||
marginLeft: theme.spacing(3),
|
||||
}));
|
||||
|
||||
interface IAddGroupUserProps {
|
||||
interface IEditGroupUsersProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
group: IGroup;
|
||||
}
|
||||
|
||||
export const AddGroupUser: FC<IAddGroupUserProps> = ({
|
||||
export const EditGroupUsers: FC<IEditGroupUsersProps> = ({
|
||||
open,
|
||||
setOpen,
|
||||
group,
|
||||
}) => {
|
||||
const { refetchGroup } = useGroup(group.id);
|
||||
const { refetchGroups } = useGroups();
|
||||
const { updateGroup, loading } = useGroupApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
|
||||
const [users, setUsers] = useState<IGroupUser[]>(group.users);
|
||||
const { users, setUsers, getGroupPayload } = useGroupForm(
|
||||
group.name,
|
||||
group.description,
|
||||
group.users
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setUsers(group.users);
|
||||
}, [group.users, open]);
|
||||
|
||||
const newUsers = useMemo(() => {
|
||||
return users.filter(
|
||||
user => !group.users.some(({ id }) => id === user.id)
|
||||
);
|
||||
}, [group.users, users]);
|
||||
|
||||
const payload = useMemo(() => {
|
||||
const addUsers = [...group.users, ...newUsers];
|
||||
return {
|
||||
name: group.name,
|
||||
description: group.description,
|
||||
users: addUsers.map(({ id, role }) => ({
|
||||
user: { id },
|
||||
role,
|
||||
})),
|
||||
};
|
||||
}, [group, newUsers]);
|
||||
}, [group.users, open, setUsers]);
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const message =
|
||||
newUsers.length === 1
|
||||
? `${
|
||||
newUsers[0].name ||
|
||||
newUsers[0].username ||
|
||||
newUsers[0].email
|
||||
} added to the group`
|
||||
: `${newUsers.length} users added to the group`;
|
||||
await updateGroup(group.id, payload);
|
||||
await updateGroup(group.id, getGroupPayload());
|
||||
refetchGroup();
|
||||
refetchGroups();
|
||||
setOpen(false);
|
||||
setToastData({
|
||||
title: message,
|
||||
title: 'Group users saved successfully',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
@ -103,7 +85,7 @@ export const AddGroupUser: FC<IAddGroupUserProps> = ({
|
||||
}/api/admin/groups/${group.id}' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(payload, undefined, 2)}'`;
|
||||
--data-raw '${JSON.stringify(getGroupPayload(), undefined, 2)}'`;
|
||||
};
|
||||
|
||||
return (
|
||||
@ -112,12 +94,12 @@ export const AddGroupUser: FC<IAddGroupUserProps> = ({
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
label="Add user"
|
||||
label="Edit users"
|
||||
>
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
modal
|
||||
title="Add user"
|
||||
title="Edit users"
|
||||
description="Groups is the best and easiest way to organize users and then use them in projects to assign a specific role in one go to all the users in a group."
|
||||
documentationLink="https://docs.getunleash.io/advanced/groups"
|
||||
documentationLinkLabel="Groups documentation"
|
||||
@ -126,14 +108,14 @@ export const AddGroupUser: FC<IAddGroupUserProps> = ({
|
||||
<StyledForm onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<StyledInputDescription>
|
||||
Add users to this group
|
||||
Edit users in this group
|
||||
</StyledInputDescription>
|
||||
<GroupFormUsersSelect
|
||||
users={users}
|
||||
setUsers={setUsers}
|
||||
/>
|
||||
<GroupFormUsersTable
|
||||
users={newUsers}
|
||||
users={users}
|
||||
setUsers={setUsers}
|
||||
/>
|
||||
</div>
|
@ -17,13 +17,12 @@ import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import { IGroupUser, Role } from 'interfaces/group';
|
||||
import { IGroupUser } from 'interfaces/group';
|
||||
import { useSearch } from 'hooks/useSearch';
|
||||
import { Search } from 'component/common/Search/Search';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
|
||||
import { GroupUserRoleCell } from 'component/admin/groups/GroupUserRoleCell/GroupUserRoleCell';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { Add, Delete, Edit } from '@mui/icons-material';
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
@ -31,16 +30,14 @@ import { MainHeader } from 'component/common/MainHeader/MainHeader';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { RemoveGroup } from 'component/admin/groups/RemoveGroup/RemoveGroup';
|
||||
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||
import { AddGroupUser } from './AddGroupUser/AddGroupUser';
|
||||
import { EditGroupUser } from './EditGroupUser/EditGroupUser';
|
||||
import { EditGroupUsers } from './EditGroupUsers/EditGroupUsers';
|
||||
import { RemoveGroupUser } from './RemoveGroupUser/RemoveGroupUser';
|
||||
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
||||
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||
import {
|
||||
UG_EDIT_BTN_ID,
|
||||
UG_DELETE_BTN_ID,
|
||||
UG_ADD_USER_BTN_ID,
|
||||
UG_EDIT_USER_BTN_ID,
|
||||
UG_EDIT_USERS_BTN_ID,
|
||||
UG_REMOVE_USER_BTN_ID,
|
||||
} from 'utils/testIds';
|
||||
|
||||
@ -55,14 +52,13 @@ const StyledDelete = styled(Delete)(({ theme }) => ({
|
||||
export const groupUsersPlaceholder: IGroupUser[] = Array(15).fill({
|
||||
name: 'Name of the user',
|
||||
username: 'Username of the user',
|
||||
role: Role.Member,
|
||||
});
|
||||
|
||||
export type PageQueryType = Partial<
|
||||
Record<'sort' | 'order' | 'search', string>
|
||||
>;
|
||||
|
||||
const defaultSort: SortingRule<string> = { id: 'role', desc: true };
|
||||
const defaultSort: SortingRule<string> = { id: 'joinedAt' };
|
||||
|
||||
const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
|
||||
'Group:v1',
|
||||
@ -75,8 +71,7 @@ export const Group: VFC = () => {
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const { group, loading } = useGroup(groupId);
|
||||
const [removeOpen, setRemoveOpen] = useState(false);
|
||||
const [addUserOpen, setAddUserOpen] = useState(false);
|
||||
const [editUserOpen, setEditUserOpen] = useState(false);
|
||||
const [editUsersOpen, setEditUsersOpen] = useState(false);
|
||||
const [removeUserOpen, setRemoveUserOpen] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState<IGroupUser>();
|
||||
|
||||
@ -109,13 +104,6 @@ export const Group: VFC = () => {
|
||||
minWidth: 100,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
Header: 'User type',
|
||||
accessor: 'role',
|
||||
Cell: GroupUserRoleCell,
|
||||
maxWidth: 150,
|
||||
filterName: 'type',
|
||||
},
|
||||
{
|
||||
Header: 'Joined',
|
||||
accessor: 'joinedAt',
|
||||
@ -138,20 +126,6 @@ export const Group: VFC = () => {
|
||||
align: 'center',
|
||||
Cell: ({ row: { original: rowUser } }: any) => (
|
||||
<ActionCell>
|
||||
<Tooltip title="Edit user" arrow describeChild>
|
||||
<span>
|
||||
<IconButton
|
||||
data-testid={`${UG_EDIT_USER_BTN_ID}-${rowUser.id}`}
|
||||
disabled={group?.users.length === 1}
|
||||
onClick={() => {
|
||||
setSelectedUser(rowUser);
|
||||
setEditUserOpen(true);
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title="Remove user from group"
|
||||
arrow
|
||||
@ -160,7 +134,6 @@ export const Group: VFC = () => {
|
||||
<span>
|
||||
<IconButton
|
||||
data-testid={`${UG_REMOVE_USER_BTN_ID}-${rowUser.id}`}
|
||||
disabled={group?.users.length === 1}
|
||||
onClick={() => {
|
||||
setSelectedUser(rowUser);
|
||||
setRemoveUserOpen(true);
|
||||
@ -176,7 +149,7 @@ export const Group: VFC = () => {
|
||||
disableSortBy: true,
|
||||
},
|
||||
],
|
||||
[setSelectedUser, setRemoveUserOpen, group?.users.length]
|
||||
[setSelectedUser, setRemoveUserOpen]
|
||||
);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@ -312,15 +285,15 @@ export const Group: VFC = () => {
|
||||
}
|
||||
/>
|
||||
<ResponsiveButton
|
||||
data-testid={UG_ADD_USER_BTN_ID}
|
||||
data-testid={UG_EDIT_USERS_BTN_ID}
|
||||
onClick={() => {
|
||||
setAddUserOpen(true);
|
||||
setEditUsersOpen(true);
|
||||
}}
|
||||
maxWidth="700px"
|
||||
Icon={Add}
|
||||
permission={ADMIN}
|
||||
>
|
||||
Add user
|
||||
Edit users
|
||||
</ResponsiveButton>
|
||||
</>
|
||||
}
|
||||
@ -374,15 +347,9 @@ export const Group: VFC = () => {
|
||||
setOpen={setRemoveOpen}
|
||||
group={group!}
|
||||
/>
|
||||
<AddGroupUser
|
||||
open={addUserOpen}
|
||||
setOpen={setAddUserOpen}
|
||||
group={group!}
|
||||
/>
|
||||
<EditGroupUser
|
||||
open={editUserOpen}
|
||||
setOpen={setEditUserOpen}
|
||||
user={selectedUser}
|
||||
<EditGroupUsers
|
||||
open={editUsersOpen}
|
||||
setOpen={setEditUsersOpen}
|
||||
group={group!}
|
||||
/>
|
||||
<RemoveGroupUser
|
||||
|
@ -30,9 +30,8 @@ export const RemoveGroupUser: FC<IRemoveGroupUserProps> = ({
|
||||
...group,
|
||||
users: group.users
|
||||
.filter(({ id }) => id !== user?.id)
|
||||
.map(({ id, role }) => ({
|
||||
.map(({ id }) => ({
|
||||
user: { id },
|
||||
role,
|
||||
})),
|
||||
};
|
||||
await updateGroup(group.id, groupPayload);
|
||||
|
@ -42,14 +42,13 @@ interface IGroupForm {
|
||||
name: string;
|
||||
description: string;
|
||||
users: IGroupUser[];
|
||||
setName: React.Dispatch<React.SetStateAction<string>>;
|
||||
setName: (name: string) => void;
|
||||
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
|
||||
handleSubmit: (e: any) => void;
|
||||
handleCancel: () => void;
|
||||
errors: { [key: string]: string };
|
||||
mode: 'Create' | 'Edit';
|
||||
clearErrors: () => void;
|
||||
}
|
||||
|
||||
export const GroupForm: FC<IGroupForm> = ({
|
||||
@ -63,7 +62,6 @@ export const GroupForm: FC<IGroupForm> = ({
|
||||
handleCancel,
|
||||
errors,
|
||||
mode,
|
||||
clearErrors,
|
||||
children,
|
||||
}) => (
|
||||
<StyledForm onSubmit={handleSubmit}>
|
||||
@ -77,7 +75,6 @@ export const GroupForm: FC<IGroupForm> = ({
|
||||
id="group-name"
|
||||
error={Boolean(errors.name)}
|
||||
errorText={errors.name}
|
||||
onFocus={() => clearErrors()}
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
data-testid={UG_NAME_ID}
|
||||
|
@ -1,17 +1,11 @@
|
||||
import {
|
||||
Autocomplete,
|
||||
Button,
|
||||
Checkbox,
|
||||
styled,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import { Autocomplete, Checkbox, styled, TextField } from '@mui/material';
|
||||
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
||||
import { IUser } from 'interfaces/user';
|
||||
import { useMemo, useState, VFC } from 'react';
|
||||
import { VFC } from 'react';
|
||||
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||
import { IGroupUser, Role } from 'interfaces/group';
|
||||
import { UG_USERS_ADD_ID, UG_USERS_ID } from 'utils/testIds';
|
||||
import { IGroupUser } from 'interfaces/group';
|
||||
import { UG_USERS_ID } from 'utils/testIds';
|
||||
|
||||
const StyledOption = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@ -21,6 +15,10 @@ const StyledOption = styled('div')(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledTags = styled('div')(({ theme }) => ({
|
||||
paddingLeft: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledGroupFormUsersSelect = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
marginBottom: theme.spacing(3),
|
||||
@ -50,6 +48,14 @@ const renderOption = (
|
||||
</li>
|
||||
);
|
||||
|
||||
const renderTags = (value: IGroupUser[]) => (
|
||||
<StyledTags>
|
||||
{value.length > 1
|
||||
? `${value.length} users selected`
|
||||
: value[0].name || value[0].username || value[0].email}
|
||||
</StyledTags>
|
||||
);
|
||||
|
||||
interface IGroupFormUsersSelectProps {
|
||||
users: IGroupUser[];
|
||||
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
|
||||
@ -60,26 +66,6 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
|
||||
setUsers,
|
||||
}) => {
|
||||
const { users: usersAll } = useUsers();
|
||||
const [selectedUsers, setSelectedUsers] = useState<IUser[]>([]);
|
||||
|
||||
const usersOptions = useMemo(
|
||||
() =>
|
||||
usersAll.filter(
|
||||
(user: IUser) => !users?.map(({ id }) => id).includes(user.id)
|
||||
),
|
||||
[usersAll, users]
|
||||
);
|
||||
|
||||
const onAdd = () => {
|
||||
const usersToBeAdded = selectedUsers.map(
|
||||
(user: IUser): IGroupUser => ({
|
||||
...user,
|
||||
role: Role.Member,
|
||||
})
|
||||
);
|
||||
setUsers((users: IGroupUser[]) => [...users, ...usersToBeAdded]);
|
||||
setSelectedUsers([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledGroupFormUsersSelect>
|
||||
@ -87,9 +73,10 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
|
||||
data-testid={UG_USERS_ID}
|
||||
size="small"
|
||||
multiple
|
||||
limitTags={10}
|
||||
limitTags={1}
|
||||
openOnFocus
|
||||
disableCloseOnSelect
|
||||
value={selectedUsers}
|
||||
value={users}
|
||||
onChange={(event, newValue, reason) => {
|
||||
if (
|
||||
event.type === 'keydown' &&
|
||||
@ -98,9 +85,9 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setSelectedUsers(newValue);
|
||||
setUsers(newValue);
|
||||
}}
|
||||
options={[...usersOptions].sort((a, b) => {
|
||||
options={[...usersAll].sort((a, b) => {
|
||||
const aName = a.name || a.username || '';
|
||||
const bName = b.name || b.username || '';
|
||||
return aName.localeCompare(bName);
|
||||
@ -108,20 +95,15 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
|
||||
renderOption={(props, option, { selected }) =>
|
||||
renderOption(props, option as IUser, selected)
|
||||
}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
getOptionLabel={(option: IUser) =>
|
||||
option.email || option.name || option.username || ''
|
||||
}
|
||||
renderInput={params => (
|
||||
<TextField {...params} label="Select users" />
|
||||
)}
|
||||
renderTags={value => renderTags(value)}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onAdd}
|
||||
data-testid={UG_USERS_ADD_ID}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</StyledGroupFormUsersSelect>
|
||||
);
|
||||
};
|
||||
|
@ -3,7 +3,6 @@ import { IconButton, Tooltip, useMediaQuery } from '@mui/material';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import { IGroupUser } from 'interfaces/group';
|
||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||
import { GroupUserRoleCell } from 'component/admin/groups/GroupUserRoleCell/GroupUserRoleCell';
|
||||
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||
import { Delete } from '@mui/icons-material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
@ -14,6 +13,8 @@ import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
||||
import theme from 'themes/theme';
|
||||
import useHiddenColumns from 'hooks/useHiddenColumns';
|
||||
|
||||
const hiddenColumnsSmall = ['imageUrl', 'name'];
|
||||
|
||||
interface IGroupFormUsersTableProps {
|
||||
users: IGroupUser[];
|
||||
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
|
||||
@ -23,6 +24,8 @@ export const GroupFormUsersTable: VFC<IGroupFormUsersTableProps> = ({
|
||||
users,
|
||||
setUsers,
|
||||
}) => {
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
@ -52,30 +55,6 @@ export const GroupFormUsersTable: VFC<IGroupFormUsersTableProps> = ({
|
||||
minWidth: 100,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
Header: 'Group role',
|
||||
accessor: 'role',
|
||||
Cell: ({ row: { original: rowUser } }: any) => (
|
||||
<GroupUserRoleCell
|
||||
value={rowUser.role}
|
||||
onChange={role =>
|
||||
setUsers((users: IGroupUser[]) => {
|
||||
const newUsers = [...users];
|
||||
const index = newUsers.findIndex(
|
||||
user => user.id === rowUser.id
|
||||
);
|
||||
newUsers[index] = {
|
||||
...rowUser,
|
||||
role,
|
||||
};
|
||||
return newUsers;
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
maxWidth: 150,
|
||||
filterName: 'type',
|
||||
},
|
||||
{
|
||||
Header: 'Action',
|
||||
id: 'Action',
|
||||
@ -121,11 +100,7 @@ export const GroupFormUsersTable: VFC<IGroupFormUsersTableProps> = ({
|
||||
useFlexLayout
|
||||
);
|
||||
|
||||
useHiddenColumns(
|
||||
setHiddenColumns,
|
||||
['imageUrl', 'name'],
|
||||
useMediaQuery(theme.breakpoints.down('md'))
|
||||
);
|
||||
useHiddenColumns(setHiddenColumns, hiddenColumnsSmall, isSmallScreen);
|
||||
|
||||
return (
|
||||
<ConditionallyRender
|
||||
|
@ -1,58 +0,0 @@
|
||||
import { capitalize, MenuItem, Select, styled } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import { Role } from 'interfaces/group';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { StarRounded } from '@mui/icons-material';
|
||||
import { UG_USERS_TABLE_ROLE_ID } from 'utils/testIds';
|
||||
|
||||
const StyledPopupStar = styled(StarRounded)(({ theme }) => ({
|
||||
color: theme.palette.warning.main,
|
||||
}));
|
||||
|
||||
interface IGroupUserRoleCellProps {
|
||||
value?: string;
|
||||
onChange?: (role: Role) => void;
|
||||
}
|
||||
|
||||
export const GroupUserRoleCell = ({
|
||||
value = Role.Member,
|
||||
onChange,
|
||||
}: IGroupUserRoleCellProps) => {
|
||||
const renderBadge = () => (
|
||||
<ConditionallyRender
|
||||
condition={value === Role.Member}
|
||||
show={<Badge>{capitalize(value)}</Badge>}
|
||||
elseShow={
|
||||
<Badge color="success" icon={<StyledPopupStar />}>
|
||||
{capitalize(value)}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<TextCell>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(onChange)}
|
||||
show={
|
||||
<Select
|
||||
data-testid={UG_USERS_TABLE_ROLE_ID}
|
||||
size="small"
|
||||
value={value}
|
||||
onChange={event =>
|
||||
onChange!(event.target.value as Role)
|
||||
}
|
||||
>
|
||||
{Object.values(Role).map(role => (
|
||||
<MenuItem key={role} value={role}>
|
||||
{role}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
}
|
||||
elseShow={() => renderBadge()}
|
||||
/>
|
||||
</TextCell>
|
||||
);
|
||||
};
|
@ -8,6 +8,7 @@ import { GroupCardActions } from './GroupCardActions/GroupCardActions';
|
||||
import { RemoveGroup } from 'component/admin/groups/RemoveGroup/RemoveGroup';
|
||||
import { useState } from 'react';
|
||||
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
|
||||
import { EditGroupUsers } from 'component/admin/groups/Group/EditGroupUsers/EditGroupUsers';
|
||||
|
||||
const StyledLink = styled(Link)(({ theme }) => ({
|
||||
textDecoration: 'none',
|
||||
@ -82,6 +83,7 @@ interface IGroupCardProps {
|
||||
}
|
||||
|
||||
export const GroupCard = ({ group }: IGroupCardProps) => {
|
||||
const [editUsersOpen, setEditUsersOpen] = useState(false);
|
||||
const [removeOpen, setRemoveOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
@ -93,6 +95,7 @@ export const GroupCard = ({ group }: IGroupCardProps) => {
|
||||
<StyledHeaderActions>
|
||||
<GroupCardActions
|
||||
groupId={group.id}
|
||||
onEditUsers={() => setEditUsersOpen(true)}
|
||||
onRemove={() => setRemoveOpen(true)}
|
||||
/>
|
||||
</StyledHeaderActions>
|
||||
@ -147,6 +150,11 @@ export const GroupCard = ({ group }: IGroupCardProps) => {
|
||||
</StyledBottomRow>
|
||||
</StyledGroupCard>
|
||||
</StyledLink>
|
||||
<EditGroupUsers
|
||||
open={editUsersOpen}
|
||||
setOpen={setEditUsersOpen}
|
||||
group={group}
|
||||
/>
|
||||
<RemoveGroup
|
||||
open={removeOpen}
|
||||
setOpen={setRemoveOpen}
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Delete, Edit, MoreVert } from '@mui/icons-material';
|
||||
import { Delete, Edit, GroupRounded, MoreVert } from '@mui/icons-material';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const StyledActions = styled('div')(({ theme }) => ({
|
||||
@ -25,11 +25,13 @@ const StyledPopover = styled(Popover)(({ theme }) => ({
|
||||
|
||||
interface IGroupCardActions {
|
||||
groupId: number;
|
||||
onEditUsers: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
export const GroupCardActions: FC<IGroupCardActions> = ({
|
||||
groupId,
|
||||
onEditUsers,
|
||||
onRemove,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
@ -86,6 +88,21 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
|
||||
<Typography variant="body2">Edit group</Typography>
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onEditUsers();
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<GroupRounded />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Typography variant="body2">
|
||||
Edit group users
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onRemove();
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { styled } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { IGroupUser, Role } from 'interfaces/group';
|
||||
import { IGroupUser } from 'interfaces/group';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { GroupPopover } from './GroupPopover/GroupPopover';
|
||||
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
||||
@ -26,7 +26,10 @@ interface IGroupCardAvatarsProps {
|
||||
|
||||
export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => {
|
||||
const shownUsers = useMemo(
|
||||
() => users.sort((a, b) => (a.role < b.role ? 1 : -1)).slice(0, 9),
|
||||
() =>
|
||||
users
|
||||
.sort((a, b) => b?.joinedAt!.getTime() - a?.joinedAt!.getTime())
|
||||
.slice(0, 9),
|
||||
[users]
|
||||
);
|
||||
|
||||
@ -49,7 +52,6 @@ export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => {
|
||||
<StyledAvatar
|
||||
key={user.id}
|
||||
user={user}
|
||||
star={user.role === Role.Owner}
|
||||
onMouseEnter={event => {
|
||||
onPopoverOpen(event);
|
||||
setPopupUser(user);
|
||||
|
@ -1,8 +1,5 @@
|
||||
import { Popover, styled } from '@mui/material';
|
||||
import { IGroupUser, Role } from 'interfaces/group';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { StarRounded } from '@mui/icons-material';
|
||||
import { IGroupUser } from 'interfaces/group';
|
||||
|
||||
const StyledPopover = styled(Popover)(({ theme }) => ({
|
||||
pointerEvents: 'none',
|
||||
@ -11,10 +8,6 @@ const StyledPopover = styled(Popover)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledPopupStar = styled(StarRounded)(({ theme }) => ({
|
||||
color: theme.palette.warning.main,
|
||||
}));
|
||||
|
||||
const StyledName = styled('div')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
@ -50,16 +43,6 @@ export const GroupPopover = ({
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={user?.role === Role.Member}
|
||||
show={<Badge>{user?.role}</Badge>}
|
||||
elseShow={
|
||||
<Badge color="success" icon={<StyledPopupStar />}>
|
||||
{user?.role}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
|
||||
<StyledName>{user?.name || user?.username}</StyledName>
|
||||
<div>{user?.email}</div>
|
||||
</StyledPopover>
|
||||
|
@ -121,7 +121,7 @@ export const GroupsList: VFC = () => {
|
||||
</Grid>
|
||||
</SearchHighlightProvider>
|
||||
<ConditionallyRender
|
||||
condition={data.length === 0}
|
||||
condition={!loading && data.length === 0}
|
||||
show={
|
||||
<ConditionallyRender
|
||||
condition={searchValue?.length > 0}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import useQueryParams from 'hooks/useQueryParams';
|
||||
import { IGroupUser, Role } from 'interfaces/group';
|
||||
import { IGroupUser } from 'interfaces/group';
|
||||
|
||||
export const useGroupForm = (
|
||||
initialName = '',
|
||||
@ -14,30 +14,12 @@ export const useGroupForm = (
|
||||
const [users, setUsers] = useState<IGroupUser[]>(initialUsers);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
if (!name) {
|
||||
setName(groupQueryName || initialName);
|
||||
}
|
||||
}, [name, initialName, groupQueryName]);
|
||||
|
||||
useEffect(() => {
|
||||
setDescription(initialDescription);
|
||||
}, [initialDescription]);
|
||||
|
||||
const initialUsersStringified = JSON.stringify(initialUsers);
|
||||
|
||||
useEffect(() => {
|
||||
setUsers(initialUsers);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialUsersStringified]);
|
||||
|
||||
const getGroupPayload = () => {
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
users: users.map(({ id, role }) => ({
|
||||
users: users.map(({ id }) => ({
|
||||
user: { id },
|
||||
role: role || Role.Member,
|
||||
})),
|
||||
};
|
||||
};
|
||||
@ -56,5 +38,6 @@ export const useGroupForm = (
|
||||
getGroupPayload,
|
||||
clearErrors,
|
||||
errors,
|
||||
setErrors,
|
||||
};
|
||||
};
|
||||
|
@ -1,15 +1,7 @@
|
||||
import {
|
||||
Avatar,
|
||||
AvatarProps,
|
||||
Badge,
|
||||
styled,
|
||||
SxProps,
|
||||
Theme,
|
||||
} from '@mui/material';
|
||||
import { Avatar, AvatarProps, styled, SxProps, Theme } from '@mui/material';
|
||||
import { IUser } from 'interfaces/user';
|
||||
import { FC } from 'react';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { StarRounded } from '@mui/icons-material';
|
||||
|
||||
const StyledAvatar = styled(Avatar)(({ theme }) => ({
|
||||
width: theme.spacing(3.5),
|
||||
@ -21,17 +13,8 @@ const StyledAvatar = styled(Avatar)(({ theme }) => ({
|
||||
fontWeight: theme.fontWeight.bold,
|
||||
}));
|
||||
|
||||
const StyledStar = styled(StarRounded)(({ theme }) => ({
|
||||
color: theme.palette.warning.main,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: theme.shape.borderRadiusExtraLarge,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
marginLeft: theme.spacing(-1),
|
||||
}));
|
||||
|
||||
interface IUserAvatarProps extends AvatarProps {
|
||||
user?: IUser;
|
||||
star?: boolean;
|
||||
src?: string;
|
||||
title?: string;
|
||||
onMouseEnter?: (event: any) => void;
|
||||
@ -42,7 +25,6 @@ interface IUserAvatarProps extends AvatarProps {
|
||||
|
||||
export const UserAvatar: FC<IUserAvatarProps> = ({
|
||||
user,
|
||||
star,
|
||||
src,
|
||||
title,
|
||||
onMouseEnter,
|
||||
@ -74,7 +56,7 @@ export const UserAvatar: FC<IUserAvatarProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const avatar = (
|
||||
return (
|
||||
<StyledAvatar
|
||||
className={className}
|
||||
sx={sx}
|
||||
@ -93,23 +75,4 @@ export const UserAvatar: FC<IUserAvatarProps> = ({
|
||||
/>
|
||||
</StyledAvatar>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={Boolean(star)}
|
||||
show={
|
||||
<Badge
|
||||
overlap="circular"
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
badgeContent={<StyledStar />}
|
||||
>
|
||||
{avatar}
|
||||
</Badge>
|
||||
}
|
||||
elseShow={avatar}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -170,7 +170,7 @@ export const ProjectAccessAssign = ({
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!role) return;
|
||||
if (!isValid) return;
|
||||
|
||||
try {
|
||||
if (!edit) {
|
||||
@ -263,6 +263,8 @@ export const ProjectAccessAssign = ({
|
||||
</li>
|
||||
);
|
||||
|
||||
const isValid = selectedOptions.length > 0 && role;
|
||||
|
||||
return (
|
||||
<SidebarModal
|
||||
open
|
||||
@ -287,6 +289,7 @@ export const ProjectAccessAssign = ({
|
||||
<Autocomplete
|
||||
size="small"
|
||||
multiple
|
||||
openOnFocus
|
||||
limitTags={10}
|
||||
disableCloseOnSelect
|
||||
disabled={edit}
|
||||
@ -339,6 +342,7 @@ export const ProjectAccessAssign = ({
|
||||
<StyledAutocompleteWrapper>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
openOnFocus
|
||||
value={role}
|
||||
onChange={(_, newValue) => setRole(newValue)}
|
||||
options={roles}
|
||||
@ -360,6 +364,7 @@ export const ProjectAccessAssign = ({
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={!isValid}
|
||||
>
|
||||
Assign {entityType}
|
||||
</Button>
|
||||
|
@ -1,13 +1,18 @@
|
||||
import { styled } from '@mui/material';
|
||||
import { useMemo, VFC } from 'react';
|
||||
import { styled, SxProps, Theme } from '@mui/material';
|
||||
import { ForwardedRef, forwardRef, useMemo, VFC } from 'react';
|
||||
import useProjectRole from 'hooks/api/getters/useProjectRole/useProjectRole';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
const StyledDescription = styled('div')(({ theme }) => ({
|
||||
const StyledDescription = styled('div', {
|
||||
shouldForwardProp: prop =>
|
||||
prop !== 'roleId' && prop !== 'popover' && prop !== 'sx',
|
||||
})<IProjectRoleDescriptionStyleProps>(({ theme, popover }) => ({
|
||||
width: '100%',
|
||||
maxWidth: theme.spacing(50),
|
||||
padding: theme.spacing(3),
|
||||
backgroundColor: theme.palette.neutral.light,
|
||||
backgroundColor: popover
|
||||
? theme.palette.background.paper
|
||||
: theme.palette.neutral.light,
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
@ -32,74 +37,60 @@ const StyledDescriptionSubHeader = styled('p')(({ theme }) => ({
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
|
||||
interface IProjectRoleDescriptionProps {
|
||||
interface IProjectRoleDescriptionStyleProps {
|
||||
popover?: boolean;
|
||||
className?: string;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
interface IProjectRoleDescriptionProps
|
||||
extends IProjectRoleDescriptionStyleProps {
|
||||
roleId: number;
|
||||
}
|
||||
|
||||
export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> = ({
|
||||
roleId,
|
||||
}) => {
|
||||
const { role } = useProjectRole(roleId.toString());
|
||||
export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> =
|
||||
forwardRef(
|
||||
(
|
||||
{ roleId, className, sx, ...props }: IProjectRoleDescriptionProps,
|
||||
ref: ForwardedRef<HTMLDivElement>
|
||||
) => {
|
||||
const { role } = useProjectRole(roleId.toString());
|
||||
|
||||
const environments = useMemo(() => {
|
||||
const environments = new Set<string>();
|
||||
role.permissions
|
||||
?.filter((permission: any) => permission.environment)
|
||||
.forEach((permission: any) => {
|
||||
environments.add(permission.environment);
|
||||
});
|
||||
return [...environments].sort();
|
||||
}, [role]);
|
||||
const environments = useMemo(() => {
|
||||
const environments = new Set<string>();
|
||||
role.permissions
|
||||
?.filter((permission: any) => permission.environment)
|
||||
.forEach((permission: any) => {
|
||||
environments.add(permission.environment);
|
||||
});
|
||||
return [...environments].sort();
|
||||
}, [role]);
|
||||
|
||||
const projectPermissions = useMemo(() => {
|
||||
return role.permissions?.filter(
|
||||
(permission: any) => !permission.environment
|
||||
);
|
||||
}, [role]);
|
||||
const projectPermissions = useMemo(() => {
|
||||
return role.permissions?.filter(
|
||||
(permission: any) => !permission.environment
|
||||
);
|
||||
}, [role]);
|
||||
|
||||
return (
|
||||
<StyledDescription>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(projectPermissions?.length)}
|
||||
show={
|
||||
<>
|
||||
<StyledDescriptionHeader>
|
||||
Project permissions
|
||||
</StyledDescriptionHeader>
|
||||
<StyledDescriptionBlock>
|
||||
{role.permissions
|
||||
?.filter(
|
||||
(permission: any) => !permission.environment
|
||||
)
|
||||
.map(
|
||||
(permission: any) => permission.displayName
|
||||
)
|
||||
.sort()
|
||||
.map((permission: any) => (
|
||||
<p key={permission}>{permission}</p>
|
||||
))}
|
||||
</StyledDescriptionBlock>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(environments.length)}
|
||||
show={
|
||||
<>
|
||||
<StyledDescriptionHeader>
|
||||
Environment permissions
|
||||
</StyledDescriptionHeader>
|
||||
{environments.map((environment: any) => (
|
||||
<div key={environment}>
|
||||
<StyledDescriptionSubHeader>
|
||||
{environment}
|
||||
</StyledDescriptionSubHeader>
|
||||
return (
|
||||
<StyledDescription
|
||||
className={className}
|
||||
sx={sx}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(projectPermissions?.length)}
|
||||
show={
|
||||
<>
|
||||
<StyledDescriptionHeader>
|
||||
Project permissions
|
||||
</StyledDescriptionHeader>
|
||||
<StyledDescriptionBlock>
|
||||
{role.permissions
|
||||
.filter(
|
||||
?.filter(
|
||||
(permission: any) =>
|
||||
permission.environment ===
|
||||
environment
|
||||
!permission.environment
|
||||
)
|
||||
.map(
|
||||
(permission: any) =>
|
||||
@ -110,11 +101,45 @@ export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> = ({
|
||||
<p key={permission}>{permission}</p>
|
||||
))}
|
||||
</StyledDescriptionBlock>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</StyledDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(environments.length)}
|
||||
show={
|
||||
<>
|
||||
<StyledDescriptionHeader>
|
||||
Environment permissions
|
||||
</StyledDescriptionHeader>
|
||||
{environments.map((environment: any) => (
|
||||
<div key={environment}>
|
||||
<StyledDescriptionSubHeader>
|
||||
{environment}
|
||||
</StyledDescriptionSubHeader>
|
||||
<StyledDescriptionBlock>
|
||||
{role.permissions
|
||||
.filter(
|
||||
(permission: any) =>
|
||||
permission.environment ===
|
||||
environment
|
||||
)
|
||||
.map(
|
||||
(permission: any) =>
|
||||
permission.displayName
|
||||
)
|
||||
.sort()
|
||||
.map((permission: any) => (
|
||||
<p key={permission}>
|
||||
{permission}
|
||||
</p>
|
||||
))}
|
||||
</StyledDescriptionBlock>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</StyledDescription>
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,70 @@
|
||||
import { Link, Popover, styled } from '@mui/material';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import React from 'react';
|
||||
import { VFC } from 'react';
|
||||
import { ProjectRoleDescription } from 'component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription';
|
||||
|
||||
const StyledLink = styled(Link)(() => ({
|
||||
textDecoration: 'none',
|
||||
'&:hover, &:focus': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledPopover = styled(Popover)(() => ({
|
||||
pointerEvents: 'none',
|
||||
}));
|
||||
|
||||
interface IProjectAccessRoleCellProps {
|
||||
roleId: number;
|
||||
value?: string;
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
export const ProjectAccessRoleCell: VFC<IProjectAccessRoleCellProps> = ({
|
||||
roleId,
|
||||
value,
|
||||
emptyText,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
|
||||
|
||||
const onPopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const onPopoverClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
if (!value) return <TextCell>{emptyText}</TextCell>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextCell>
|
||||
<StyledLink
|
||||
onMouseEnter={event => {
|
||||
onPopoverOpen(event);
|
||||
}}
|
||||
onMouseLeave={onPopoverClose}
|
||||
>
|
||||
{value}
|
||||
</StyledLink>
|
||||
</TextCell>
|
||||
<StyledPopover
|
||||
open={Boolean(anchorEl)}
|
||||
anchorEl={anchorEl}
|
||||
onClose={onPopoverClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<ProjectRoleDescription roleId={roleId} popover />
|
||||
</StyledPopover>
|
||||
</>
|
||||
);
|
||||
};
|
@ -43,6 +43,7 @@ import { ProjectAccessCreate } from 'component/project/ProjectAccess/ProjectAcce
|
||||
import { ProjectAccessEditUser } from 'component/project/ProjectAccess/ProjectAccessEditUser/ProjectAccessEditUser';
|
||||
import { ProjectAccessEditGroup } from 'component/project/ProjectAccess/ProjectAccessEditGroup/ProjectAccessEditGroup';
|
||||
import useHiddenColumns from 'hooks/useHiddenColumns';
|
||||
import { ProjectAccessRoleCell } from './ProjectAccessRoleCell/ProjectAccessRoleCell';
|
||||
|
||||
export type PageQueryType = Partial<
|
||||
Record<'sort' | 'order' | 'search', string>
|
||||
@ -69,6 +70,14 @@ const StyledGroupAvatar = styled(UserAvatar)(({ theme }) => ({
|
||||
outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`,
|
||||
}));
|
||||
|
||||
const hiddenColumnsSmall = [
|
||||
'imageUrl',
|
||||
'username',
|
||||
'role',
|
||||
'added',
|
||||
'lastLogin',
|
||||
];
|
||||
|
||||
export const ProjectAccessTable: VFC = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
|
||||
@ -149,6 +158,12 @@ export const ProjectAccessTable: VFC = () => {
|
||||
accessor: (row: IProjectAccess) =>
|
||||
access?.roles.find(({ id }) => id === row.entity.roleId)
|
||||
?.name,
|
||||
Cell: ({ value, row: { original: row } }: any) => (
|
||||
<ProjectAccessRoleCell
|
||||
roleId={row.entity.roleId}
|
||||
value={value}
|
||||
/>
|
||||
),
|
||||
minWidth: 120,
|
||||
filterName: 'role',
|
||||
},
|
||||
@ -281,11 +296,7 @@ export const ProjectAccessTable: VFC = () => {
|
||||
useFlexLayout
|
||||
);
|
||||
|
||||
useHiddenColumns(
|
||||
setHiddenColumns,
|
||||
['imageUrl', 'username', 'role', 'added', 'lastLogin'],
|
||||
isSmallScreen
|
||||
);
|
||||
useHiddenColumns(setHiddenColumns, hiddenColumnsSmall, isSmallScreen);
|
||||
|
||||
useEffect(() => {
|
||||
const tableState: PageQueryType = {};
|
||||
@ -335,6 +346,7 @@ export const ProjectAccessTable: VFC = () => {
|
||||
}
|
||||
setRemoveOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
header={
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Delete, Edit } from '@mui/icons-material';
|
||||
import { styled, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { GroupUserRoleCell } from 'component/admin/groups/GroupUserRoleCell/GroupUserRoleCell';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
@ -47,7 +46,7 @@ const StyledTitle = styled('div')(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const defaultSort: SortingRule<string> = { id: 'role', desc: true };
|
||||
const defaultSort: SortingRule<string> = { id: 'joinedAt' };
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@ -77,13 +76,6 @@ const columns = [
|
||||
minWidth: 100,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
Header: 'User type',
|
||||
accessor: 'role',
|
||||
Cell: GroupUserRoleCell,
|
||||
maxWidth: 150,
|
||||
filterName: 'type',
|
||||
},
|
||||
{
|
||||
id: 'joined',
|
||||
Header: 'Joined',
|
||||
@ -104,6 +96,8 @@ const columns = [
|
||||
},
|
||||
];
|
||||
|
||||
const hiddenColumnsSmall = ['imageUrl', 'name', 'joined', 'lastLogin'];
|
||||
|
||||
interface IProjectGroupViewProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@ -156,11 +150,7 @@ export const ProjectGroupView: VFC<IProjectGroupViewProps> = ({
|
||||
useFlexLayout
|
||||
);
|
||||
|
||||
useHiddenColumns(
|
||||
setHiddenColumns,
|
||||
['imageUrl', 'name', 'joined', 'lastLogin'],
|
||||
useMediaQuery(theme.breakpoints.down('md'))
|
||||
);
|
||||
useHiddenColumns(setHiddenColumns, hiddenColumnsSmall, isSmallScreen);
|
||||
|
||||
return (
|
||||
<SidebarModal
|
||||
|
@ -15,7 +15,6 @@ export const mapGroupUsers = (users: any[]) =>
|
||||
users.map(user => ({
|
||||
...user.user,
|
||||
joinedAt: new Date(user.joinedAt),
|
||||
role: user.role,
|
||||
}));
|
||||
|
||||
export const useGroup = (groupId: number): IUseGroupOutput => {
|
||||
|
@ -1,10 +1,5 @@
|
||||
import { IUser } from './user';
|
||||
|
||||
export enum Role {
|
||||
Owner = 'Owner',
|
||||
Member = 'Member',
|
||||
}
|
||||
|
||||
export interface IGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
@ -17,7 +12,6 @@ export interface IGroup {
|
||||
}
|
||||
|
||||
export interface IGroupUser extends IUser {
|
||||
role: Role;
|
||||
joinedAt?: Date;
|
||||
}
|
||||
|
||||
@ -25,5 +19,4 @@ export interface IGroupUserModel {
|
||||
user: {
|
||||
id: number;
|
||||
};
|
||||
role: Role;
|
||||
}
|
||||
|
@ -15,15 +15,12 @@ export const UG_NAME_ID = 'UG_NAME_ID';
|
||||
export const UG_DESC_ID = 'UG_DESC_ID';
|
||||
export const UG_USERS_ID = 'UG_USERS_ID';
|
||||
export const UG_USERS_ADD_ID = 'UG_USERS_ADD_ID';
|
||||
export const UG_USERS_TABLE_ROLE_ID = 'UG_USERS_TABLE_ROLE_ID';
|
||||
export const UG_CREATE_BTN_ID = 'UG_CREATE_BTN_ID';
|
||||
export const UG_SAVE_BTN_ID = 'UG_SAVE_BTN_ID';
|
||||
export const UG_EDIT_BTN_ID = 'UG_EDIT_BTN_ID';
|
||||
export const UG_DELETE_BTN_ID = 'UG_DELETE_BTN_ID';
|
||||
export const UG_ADD_USER_BTN_ID = 'UG_ADD_USER_BTN_ID';
|
||||
export const UG_EDIT_USER_BTN_ID = 'UG_EDIT_USER_BTN_ID';
|
||||
export const UG_EDIT_USERS_BTN_ID = 'UG_EDIT_USERS_BTN_ID';
|
||||
export const UG_REMOVE_USER_BTN_ID = 'UG_REMOVE_USER_BTN_ID';
|
||||
export const UG_USERS_ROLE_ID = 'UG_USERS_ROLE_ID';
|
||||
|
||||
/* SEGMENT */
|
||||
export const SEGMENT_NAME_ID = 'SEGMENT_NAME_ID';
|
||||
|
Loading…
Reference in New Issue
Block a user