mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-12 01:17:04 +02: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', () => {
|
it('can create a group', () => {
|
||||||
cy.get("[data-testid='NAVIGATE_TO_CREATE_GROUP']").click();
|
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_DESC_ID']").type('hello-world');
|
||||||
cy.get("[data-testid='UG_USERS_ID']").click();
|
cy.get("[data-testid='UG_USERS_ID']").click();
|
||||||
cy.contains(`unleash-e2e-user1-${randomId}`).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='UG_CREATE_BTN_ID']").click();
|
||||||
cy.wait('@createGroup');
|
cy.wait('@createGroup');
|
||||||
@ -80,16 +60,8 @@ describe('groups', () => {
|
|||||||
cy.intercept('POST', '/api/admin/groups').as('createGroup');
|
cy.intercept('POST', '/api/admin/groups').as('createGroup');
|
||||||
|
|
||||||
cy.get("[data-testid='UG_NAME_ID']").type(groupName);
|
cy.get("[data-testid='UG_NAME_ID']").type(groupName);
|
||||||
cy.get("[data-testid='UG_DESC_ID']").type('hello-world');
|
cy.get("[data-testid='INPUT_ERROR_TEXT'").contains(
|
||||||
cy.get("[data-testid='UG_USERS_ID']").click();
|
'A group with that name already exists.'
|
||||||
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'
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -108,34 +80,15 @@ describe('groups', () => {
|
|||||||
it('can add user to a group', () => {
|
it('can add user to a group', () => {
|
||||||
cy.contains(groupName).click();
|
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.get("[data-testid='UG_USERS_ID']").click();
|
||||||
cy.contains(`unleash-e2e-user2-${randomId}`).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.get("[data-testid='UG_SAVE_BTN_ID']").click();
|
||||||
|
|
||||||
cy.contains(`unleash-e2e-user1-${randomId}`);
|
cy.contains(`unleash-e2e-user1-${randomId}`);
|
||||||
cy.contains(`unleash-e2e-user2-${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', () => {
|
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 { Search } from 'component/common/Search/Search';
|
||||||
import useHiddenColumns from 'hooks/useHiddenColumns';
|
import useHiddenColumns from 'hooks/useHiddenColumns';
|
||||||
|
|
||||||
|
const hiddenColumnsSmall = ['Icon', 'createdAt'];
|
||||||
|
const hiddenColumnsFlagE = ['projects', 'environment'];
|
||||||
|
|
||||||
export const ApiTokenTable = () => {
|
export const ApiTokenTable = () => {
|
||||||
const { tokens, loading } = useApiTokens();
|
const { tokens, loading } = useApiTokens();
|
||||||
const initialState = useMemo(() => ({ sortBy: [{ id: 'createdAt' }] }), []);
|
const initialState = useMemo(() => ({ sortBy: [{ id: 'createdAt' }] }), []);
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getTableProps,
|
getTableProps,
|
||||||
@ -53,16 +57,8 @@ export const ApiTokenTable = () => {
|
|||||||
useSortBy
|
useSortBy
|
||||||
);
|
);
|
||||||
|
|
||||||
useHiddenColumns(
|
useHiddenColumns(setHiddenColumns, hiddenColumnsSmall, isSmallScreen);
|
||||||
setHiddenColumns,
|
useHiddenColumns(setHiddenColumns, hiddenColumnsFlagE, !uiConfig.flags.E);
|
||||||
['Icon', 'createdAt'],
|
|
||||||
useMediaQuery(theme.breakpoints.down('md'))
|
|
||||||
);
|
|
||||||
useHiddenColumns(
|
|
||||||
setHiddenColumns,
|
|
||||||
['projects', 'environment'],
|
|
||||||
!uiConfig.flags.E
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
|
@ -10,6 +10,7 @@ import { UG_CREATE_BTN_ID } from 'utils/testIds';
|
|||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import { CREATE } from 'constants/misc';
|
import { CREATE } from 'constants/misc';
|
||||||
import { GO_BACK } from 'constants/navigate';
|
import { GO_BACK } from 'constants/navigate';
|
||||||
|
import { useGroups } from 'hooks/api/getters/useGroups/useGroups';
|
||||||
|
|
||||||
export const CreateGroup = () => {
|
export const CreateGroup = () => {
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
@ -26,14 +27,18 @@ export const CreateGroup = () => {
|
|||||||
getGroupPayload,
|
getGroupPayload,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
errors,
|
errors,
|
||||||
|
setErrors,
|
||||||
} = useGroupForm();
|
} = useGroupForm();
|
||||||
|
|
||||||
|
const { groups } = useGroups();
|
||||||
const { createGroup, loading } = useGroupApi();
|
const { createGroup, loading } = useGroupApi();
|
||||||
|
|
||||||
const handleSubmit = async (e: Event) => {
|
const handleSubmit = async (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
clearErrors();
|
clearErrors();
|
||||||
|
|
||||||
|
if (!isValid) return;
|
||||||
|
|
||||||
const payload = getGroupPayload();
|
const payload = getGroupPayload();
|
||||||
try {
|
try {
|
||||||
const group = await createGroup(payload);
|
const group = await createGroup(payload);
|
||||||
@ -62,6 +67,19 @@ export const CreateGroup = () => {
|
|||||||
navigate(GO_BACK);
|
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 (
|
return (
|
||||||
<FormTemplate
|
<FormTemplate
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@ -75,19 +93,19 @@ export const CreateGroup = () => {
|
|||||||
name={name}
|
name={name}
|
||||||
description={description}
|
description={description}
|
||||||
users={users}
|
users={users}
|
||||||
setName={setName}
|
setName={onSetName}
|
||||||
setDescription={setDescription}
|
setDescription={setDescription}
|
||||||
setUsers={setUsers}
|
setUsers={setUsers}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
mode={CREATE}
|
mode={CREATE}
|
||||||
clearErrors={clearErrors}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
disabled={!isValid}
|
||||||
data-testid={UG_CREATE_BTN_ID}
|
data-testid={UG_CREATE_BTN_ID}
|
||||||
>
|
>
|
||||||
Create group
|
Create group
|
||||||
|
@ -12,10 +12,12 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|||||||
import { useGroup } from 'hooks/api/getters/useGroup/useGroup';
|
import { useGroup } from 'hooks/api/getters/useGroup/useGroup';
|
||||||
import { UG_SAVE_BTN_ID } from 'utils/testIds';
|
import { UG_SAVE_BTN_ID } from 'utils/testIds';
|
||||||
import { GO_BACK } from 'constants/navigate';
|
import { GO_BACK } from 'constants/navigate';
|
||||||
|
import { useGroups } from 'hooks/api/getters/useGroups/useGroups';
|
||||||
|
|
||||||
export const EditGroup = () => {
|
export const EditGroup = () => {
|
||||||
const groupId = Number(useRequiredPathParam('groupId'));
|
const groupId = Number(useRequiredPathParam('groupId'));
|
||||||
const { group, refetchGroup } = useGroup(groupId);
|
const { group, refetchGroup } = useGroup(groupId);
|
||||||
|
const { refetchGroups } = useGroups();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -30,8 +32,10 @@ export const EditGroup = () => {
|
|||||||
getGroupPayload,
|
getGroupPayload,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
errors,
|
errors,
|
||||||
|
setErrors,
|
||||||
} = useGroupForm(group?.name, group?.description, group?.users);
|
} = useGroupForm(group?.name, group?.description, group?.users);
|
||||||
|
|
||||||
|
const { groups } = useGroups();
|
||||||
const { updateGroup, loading } = useGroupApi();
|
const { updateGroup, loading } = useGroupApi();
|
||||||
|
|
||||||
const handleSubmit = async (e: Event) => {
|
const handleSubmit = async (e: Event) => {
|
||||||
@ -42,6 +46,7 @@ export const EditGroup = () => {
|
|||||||
try {
|
try {
|
||||||
await updateGroup(groupId, payload);
|
await updateGroup(groupId, payload);
|
||||||
refetchGroup();
|
refetchGroup();
|
||||||
|
refetchGroups();
|
||||||
navigate(GO_BACK);
|
navigate(GO_BACK);
|
||||||
setToastData({
|
setToastData({
|
||||||
title: 'Group updated successfully',
|
title: 'Group updated successfully',
|
||||||
@ -65,6 +70,20 @@ export const EditGroup = () => {
|
|||||||
navigate(GO_BACK);
|
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 (
|
return (
|
||||||
<FormTemplate
|
<FormTemplate
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@ -78,19 +97,19 @@ export const EditGroup = () => {
|
|||||||
name={name}
|
name={name}
|
||||||
description={description}
|
description={description}
|
||||||
users={users}
|
users={users}
|
||||||
setName={setName}
|
setName={onSetName}
|
||||||
setDescription={setDescription}
|
setDescription={setDescription}
|
||||||
setUsers={setUsers}
|
setUsers={setUsers}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
mode={EDIT}
|
mode={EDIT}
|
||||||
clearErrors={clearErrors}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
disabled={!isValid}
|
||||||
data-testid={UG_SAVE_BTN_ID}
|
data-testid={UG_SAVE_BTN_ID}
|
||||||
>
|
>
|
||||||
Save
|
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 { useGroup } from 'hooks/api/getters/useGroup/useGroup';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { IGroup, IGroupUser } from 'interfaces/group';
|
import { IGroup } from 'interfaces/group';
|
||||||
import { FC, FormEvent, useEffect, useMemo, useState } from 'react';
|
import { FC, FormEvent, useEffect } from 'react';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { GroupFormUsersSelect } from 'component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect';
|
import { GroupFormUsersSelect } from 'component/admin/groups/GroupForm/GroupFormUsersSelect/GroupFormUsersSelect';
|
||||||
import { GroupFormUsersTable } from 'component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable';
|
import { GroupFormUsersTable } from 'component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable';
|
||||||
import { UG_SAVE_BTN_ID } from 'utils/testIds';
|
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')(() => ({
|
const StyledForm = styled('form')(() => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -33,63 +35,43 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({
|
|||||||
marginLeft: theme.spacing(3),
|
marginLeft: theme.spacing(3),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface IAddGroupUserProps {
|
interface IEditGroupUsersProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
group: IGroup;
|
group: IGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddGroupUser: FC<IAddGroupUserProps> = ({
|
export const EditGroupUsers: FC<IEditGroupUsersProps> = ({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
group,
|
group,
|
||||||
}) => {
|
}) => {
|
||||||
const { refetchGroup } = useGroup(group.id);
|
const { refetchGroup } = useGroup(group.id);
|
||||||
|
const { refetchGroups } = useGroups();
|
||||||
const { updateGroup, loading } = useGroupApi();
|
const { updateGroup, loading } = useGroupApi();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
const [users, setUsers] = useState<IGroupUser[]>(group.users);
|
const { users, setUsers, getGroupPayload } = useGroupForm(
|
||||||
|
group.name,
|
||||||
|
group.description,
|
||||||
|
group.users
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUsers(group.users);
|
setUsers(group.users);
|
||||||
}, [group.users, open]);
|
}, [group.users, open, setUsers]);
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const message =
|
await updateGroup(group.id, getGroupPayload());
|
||||||
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);
|
|
||||||
refetchGroup();
|
refetchGroup();
|
||||||
|
refetchGroups();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setToastData({
|
setToastData({
|
||||||
title: message,
|
title: 'Group users saved successfully',
|
||||||
type: 'success',
|
type: 'success',
|
||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@ -103,7 +85,7 @@ export const AddGroupUser: FC<IAddGroupUserProps> = ({
|
|||||||
}/api/admin/groups/${group.id}' \\
|
}/api/admin/groups/${group.id}' \\
|
||||||
--header 'Authorization: INSERT_API_KEY' \\
|
--header 'Authorization: INSERT_API_KEY' \\
|
||||||
--header 'Content-Type: application/json' \\
|
--header 'Content-Type: application/json' \\
|
||||||
--data-raw '${JSON.stringify(payload, undefined, 2)}'`;
|
--data-raw '${JSON.stringify(getGroupPayload(), undefined, 2)}'`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -112,12 +94,12 @@ export const AddGroupUser: FC<IAddGroupUserProps> = ({
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
label="Add user"
|
label="Edit users"
|
||||||
>
|
>
|
||||||
<FormTemplate
|
<FormTemplate
|
||||||
loading={loading}
|
loading={loading}
|
||||||
modal
|
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."
|
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"
|
documentationLink="https://docs.getunleash.io/advanced/groups"
|
||||||
documentationLinkLabel="Groups documentation"
|
documentationLinkLabel="Groups documentation"
|
||||||
@ -126,14 +108,14 @@ export const AddGroupUser: FC<IAddGroupUserProps> = ({
|
|||||||
<StyledForm onSubmit={handleSubmit}>
|
<StyledForm onSubmit={handleSubmit}>
|
||||||
<div>
|
<div>
|
||||||
<StyledInputDescription>
|
<StyledInputDescription>
|
||||||
Add users to this group
|
Edit users in this group
|
||||||
</StyledInputDescription>
|
</StyledInputDescription>
|
||||||
<GroupFormUsersSelect
|
<GroupFormUsersSelect
|
||||||
users={users}
|
users={users}
|
||||||
setUsers={setUsers}
|
setUsers={setUsers}
|
||||||
/>
|
/>
|
||||||
<GroupFormUsersTable
|
<GroupFormUsersTable
|
||||||
users={newUsers}
|
users={users}
|
||||||
setUsers={setUsers}
|
setUsers={setUsers}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
@ -17,13 +17,12 @@ import { PageContent } from 'component/common/PageContent/PageContent';
|
|||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { sortTypes } from 'utils/sortTypes';
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
import { IGroupUser, Role } from 'interfaces/group';
|
import { IGroupUser } from 'interfaces/group';
|
||||||
import { useSearch } from 'hooks/useSearch';
|
import { useSearch } from 'hooks/useSearch';
|
||||||
import { Search } from 'component/common/Search/Search';
|
import { Search } from 'component/common/Search/Search';
|
||||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||||
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
|
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 PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
import { Add, Delete, Edit } from '@mui/icons-material';
|
import { Add, Delete, Edit } from '@mui/icons-material';
|
||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
@ -31,16 +30,14 @@ import { MainHeader } from 'component/common/MainHeader/MainHeader';
|
|||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { RemoveGroup } from 'component/admin/groups/RemoveGroup/RemoveGroup';
|
import { RemoveGroup } from 'component/admin/groups/RemoveGroup/RemoveGroup';
|
||||||
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||||
import { AddGroupUser } from './AddGroupUser/AddGroupUser';
|
import { EditGroupUsers } from './EditGroupUsers/EditGroupUsers';
|
||||||
import { EditGroupUser } from './EditGroupUser/EditGroupUser';
|
|
||||||
import { RemoveGroupUser } from './RemoveGroupUser/RemoveGroupUser';
|
import { RemoveGroupUser } from './RemoveGroupUser/RemoveGroupUser';
|
||||||
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
||||||
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||||
import {
|
import {
|
||||||
UG_EDIT_BTN_ID,
|
UG_EDIT_BTN_ID,
|
||||||
UG_DELETE_BTN_ID,
|
UG_DELETE_BTN_ID,
|
||||||
UG_ADD_USER_BTN_ID,
|
UG_EDIT_USERS_BTN_ID,
|
||||||
UG_EDIT_USER_BTN_ID,
|
|
||||||
UG_REMOVE_USER_BTN_ID,
|
UG_REMOVE_USER_BTN_ID,
|
||||||
} from 'utils/testIds';
|
} from 'utils/testIds';
|
||||||
|
|
||||||
@ -55,14 +52,13 @@ const StyledDelete = styled(Delete)(({ theme }) => ({
|
|||||||
export const groupUsersPlaceholder: IGroupUser[] = Array(15).fill({
|
export const groupUsersPlaceholder: IGroupUser[] = Array(15).fill({
|
||||||
name: 'Name of the user',
|
name: 'Name of the user',
|
||||||
username: 'Username of the user',
|
username: 'Username of the user',
|
||||||
role: Role.Member,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type PageQueryType = Partial<
|
export type PageQueryType = Partial<
|
||||||
Record<'sort' | 'order' | 'search', string>
|
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(
|
const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
|
||||||
'Group:v1',
|
'Group:v1',
|
||||||
@ -75,8 +71,7 @@ export const Group: VFC = () => {
|
|||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const { group, loading } = useGroup(groupId);
|
const { group, loading } = useGroup(groupId);
|
||||||
const [removeOpen, setRemoveOpen] = useState(false);
|
const [removeOpen, setRemoveOpen] = useState(false);
|
||||||
const [addUserOpen, setAddUserOpen] = useState(false);
|
const [editUsersOpen, setEditUsersOpen] = useState(false);
|
||||||
const [editUserOpen, setEditUserOpen] = useState(false);
|
|
||||||
const [removeUserOpen, setRemoveUserOpen] = useState(false);
|
const [removeUserOpen, setRemoveUserOpen] = useState(false);
|
||||||
const [selectedUser, setSelectedUser] = useState<IGroupUser>();
|
const [selectedUser, setSelectedUser] = useState<IGroupUser>();
|
||||||
|
|
||||||
@ -109,13 +104,6 @@ export const Group: VFC = () => {
|
|||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
searchable: true,
|
searchable: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Header: 'User type',
|
|
||||||
accessor: 'role',
|
|
||||||
Cell: GroupUserRoleCell,
|
|
||||||
maxWidth: 150,
|
|
||||||
filterName: 'type',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Header: 'Joined',
|
Header: 'Joined',
|
||||||
accessor: 'joinedAt',
|
accessor: 'joinedAt',
|
||||||
@ -138,20 +126,6 @@ export const Group: VFC = () => {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
Cell: ({ row: { original: rowUser } }: any) => (
|
Cell: ({ row: { original: rowUser } }: any) => (
|
||||||
<ActionCell>
|
<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
|
<Tooltip
|
||||||
title="Remove user from group"
|
title="Remove user from group"
|
||||||
arrow
|
arrow
|
||||||
@ -160,7 +134,6 @@ export const Group: VFC = () => {
|
|||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
data-testid={`${UG_REMOVE_USER_BTN_ID}-${rowUser.id}`}
|
data-testid={`${UG_REMOVE_USER_BTN_ID}-${rowUser.id}`}
|
||||||
disabled={group?.users.length === 1}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedUser(rowUser);
|
setSelectedUser(rowUser);
|
||||||
setRemoveUserOpen(true);
|
setRemoveUserOpen(true);
|
||||||
@ -176,7 +149,7 @@ export const Group: VFC = () => {
|
|||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[setSelectedUser, setRemoveUserOpen, group?.users.length]
|
[setSelectedUser, setRemoveUserOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
@ -312,15 +285,15 @@ export const Group: VFC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ResponsiveButton
|
<ResponsiveButton
|
||||||
data-testid={UG_ADD_USER_BTN_ID}
|
data-testid={UG_EDIT_USERS_BTN_ID}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAddUserOpen(true);
|
setEditUsersOpen(true);
|
||||||
}}
|
}}
|
||||||
maxWidth="700px"
|
maxWidth="700px"
|
||||||
Icon={Add}
|
Icon={Add}
|
||||||
permission={ADMIN}
|
permission={ADMIN}
|
||||||
>
|
>
|
||||||
Add user
|
Edit users
|
||||||
</ResponsiveButton>
|
</ResponsiveButton>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@ -374,15 +347,9 @@ export const Group: VFC = () => {
|
|||||||
setOpen={setRemoveOpen}
|
setOpen={setRemoveOpen}
|
||||||
group={group!}
|
group={group!}
|
||||||
/>
|
/>
|
||||||
<AddGroupUser
|
<EditGroupUsers
|
||||||
open={addUserOpen}
|
open={editUsersOpen}
|
||||||
setOpen={setAddUserOpen}
|
setOpen={setEditUsersOpen}
|
||||||
group={group!}
|
|
||||||
/>
|
|
||||||
<EditGroupUser
|
|
||||||
open={editUserOpen}
|
|
||||||
setOpen={setEditUserOpen}
|
|
||||||
user={selectedUser}
|
|
||||||
group={group!}
|
group={group!}
|
||||||
/>
|
/>
|
||||||
<RemoveGroupUser
|
<RemoveGroupUser
|
||||||
|
@ -30,9 +30,8 @@ export const RemoveGroupUser: FC<IRemoveGroupUserProps> = ({
|
|||||||
...group,
|
...group,
|
||||||
users: group.users
|
users: group.users
|
||||||
.filter(({ id }) => id !== user?.id)
|
.filter(({ id }) => id !== user?.id)
|
||||||
.map(({ id, role }) => ({
|
.map(({ id }) => ({
|
||||||
user: { id },
|
user: { id },
|
||||||
role,
|
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
await updateGroup(group.id, groupPayload);
|
await updateGroup(group.id, groupPayload);
|
||||||
|
@ -42,14 +42,13 @@ interface IGroupForm {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
users: IGroupUser[];
|
users: IGroupUser[];
|
||||||
setName: React.Dispatch<React.SetStateAction<string>>;
|
setName: (name: string) => void;
|
||||||
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
|
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
|
||||||
handleSubmit: (e: any) => void;
|
handleSubmit: (e: any) => void;
|
||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
mode: 'Create' | 'Edit';
|
mode: 'Create' | 'Edit';
|
||||||
clearErrors: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GroupForm: FC<IGroupForm> = ({
|
export const GroupForm: FC<IGroupForm> = ({
|
||||||
@ -63,7 +62,6 @@ export const GroupForm: FC<IGroupForm> = ({
|
|||||||
handleCancel,
|
handleCancel,
|
||||||
errors,
|
errors,
|
||||||
mode,
|
mode,
|
||||||
clearErrors,
|
|
||||||
children,
|
children,
|
||||||
}) => (
|
}) => (
|
||||||
<StyledForm onSubmit={handleSubmit}>
|
<StyledForm onSubmit={handleSubmit}>
|
||||||
@ -77,7 +75,6 @@ export const GroupForm: FC<IGroupForm> = ({
|
|||||||
id="group-name"
|
id="group-name"
|
||||||
error={Boolean(errors.name)}
|
error={Boolean(errors.name)}
|
||||||
errorText={errors.name}
|
errorText={errors.name}
|
||||||
onFocus={() => clearErrors()}
|
|
||||||
value={name}
|
value={name}
|
||||||
onChange={e => setName(e.target.value)}
|
onChange={e => setName(e.target.value)}
|
||||||
data-testid={UG_NAME_ID}
|
data-testid={UG_NAME_ID}
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
import {
|
import { Autocomplete, Checkbox, styled, TextField } from '@mui/material';
|
||||||
Autocomplete,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
styled,
|
|
||||||
TextField,
|
|
||||||
} from '@mui/material';
|
|
||||||
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||||
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
||||||
import { IUser } from 'interfaces/user';
|
import { IUser } from 'interfaces/user';
|
||||||
import { useMemo, useState, VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||||
import { IGroupUser, Role } from 'interfaces/group';
|
import { IGroupUser } from 'interfaces/group';
|
||||||
import { UG_USERS_ADD_ID, UG_USERS_ID } from 'utils/testIds';
|
import { UG_USERS_ID } from 'utils/testIds';
|
||||||
|
|
||||||
const StyledOption = styled('div')(({ theme }) => ({
|
const StyledOption = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
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 }) => ({
|
const StyledGroupFormUsersSelect = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
marginBottom: theme.spacing(3),
|
marginBottom: theme.spacing(3),
|
||||||
@ -50,6 +48,14 @@ const renderOption = (
|
|||||||
</li>
|
</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 {
|
interface IGroupFormUsersSelectProps {
|
||||||
users: IGroupUser[];
|
users: IGroupUser[];
|
||||||
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
|
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
|
||||||
@ -60,26 +66,6 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
|
|||||||
setUsers,
|
setUsers,
|
||||||
}) => {
|
}) => {
|
||||||
const { users: usersAll } = useUsers();
|
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 (
|
return (
|
||||||
<StyledGroupFormUsersSelect>
|
<StyledGroupFormUsersSelect>
|
||||||
@ -87,9 +73,10 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
|
|||||||
data-testid={UG_USERS_ID}
|
data-testid={UG_USERS_ID}
|
||||||
size="small"
|
size="small"
|
||||||
multiple
|
multiple
|
||||||
limitTags={10}
|
limitTags={1}
|
||||||
|
openOnFocus
|
||||||
disableCloseOnSelect
|
disableCloseOnSelect
|
||||||
value={selectedUsers}
|
value={users}
|
||||||
onChange={(event, newValue, reason) => {
|
onChange={(event, newValue, reason) => {
|
||||||
if (
|
if (
|
||||||
event.type === 'keydown' &&
|
event.type === 'keydown' &&
|
||||||
@ -98,9 +85,9 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedUsers(newValue);
|
setUsers(newValue);
|
||||||
}}
|
}}
|
||||||
options={[...usersOptions].sort((a, b) => {
|
options={[...usersAll].sort((a, b) => {
|
||||||
const aName = a.name || a.username || '';
|
const aName = a.name || a.username || '';
|
||||||
const bName = b.name || b.username || '';
|
const bName = b.name || b.username || '';
|
||||||
return aName.localeCompare(bName);
|
return aName.localeCompare(bName);
|
||||||
@ -108,20 +95,15 @@ export const GroupFormUsersSelect: VFC<IGroupFormUsersSelectProps> = ({
|
|||||||
renderOption={(props, option, { selected }) =>
|
renderOption={(props, option, { selected }) =>
|
||||||
renderOption(props, option as IUser, selected)
|
renderOption(props, option as IUser, selected)
|
||||||
}
|
}
|
||||||
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
getOptionLabel={(option: IUser) =>
|
getOptionLabel={(option: IUser) =>
|
||||||
option.email || option.name || option.username || ''
|
option.email || option.name || option.username || ''
|
||||||
}
|
}
|
||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<TextField {...params} label="Select users" />
|
<TextField {...params} label="Select users" />
|
||||||
)}
|
)}
|
||||||
|
renderTags={value => renderTags(value)}
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={onAdd}
|
|
||||||
data-testid={UG_USERS_ADD_ID}
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</StyledGroupFormUsersSelect>
|
</StyledGroupFormUsersSelect>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,6 @@ import { IconButton, Tooltip, useMediaQuery } from '@mui/material';
|
|||||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
import { IGroupUser } from 'interfaces/group';
|
import { IGroupUser } from 'interfaces/group';
|
||||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
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 { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||||
import { Delete } from '@mui/icons-material';
|
import { Delete } from '@mui/icons-material';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
@ -14,6 +13,8 @@ import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
|||||||
import theme from 'themes/theme';
|
import theme from 'themes/theme';
|
||||||
import useHiddenColumns from 'hooks/useHiddenColumns';
|
import useHiddenColumns from 'hooks/useHiddenColumns';
|
||||||
|
|
||||||
|
const hiddenColumnsSmall = ['imageUrl', 'name'];
|
||||||
|
|
||||||
interface IGroupFormUsersTableProps {
|
interface IGroupFormUsersTableProps {
|
||||||
users: IGroupUser[];
|
users: IGroupUser[];
|
||||||
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
|
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
|
||||||
@ -23,6 +24,8 @@ export const GroupFormUsersTable: VFC<IGroupFormUsersTableProps> = ({
|
|||||||
users,
|
users,
|
||||||
setUsers,
|
setUsers,
|
||||||
}) => {
|
}) => {
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@ -52,30 +55,6 @@ export const GroupFormUsersTable: VFC<IGroupFormUsersTableProps> = ({
|
|||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
searchable: true,
|
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',
|
Header: 'Action',
|
||||||
id: 'Action',
|
id: 'Action',
|
||||||
@ -121,11 +100,7 @@ export const GroupFormUsersTable: VFC<IGroupFormUsersTableProps> = ({
|
|||||||
useFlexLayout
|
useFlexLayout
|
||||||
);
|
);
|
||||||
|
|
||||||
useHiddenColumns(
|
useHiddenColumns(setHiddenColumns, hiddenColumnsSmall, isSmallScreen);
|
||||||
setHiddenColumns,
|
|
||||||
['imageUrl', 'name'],
|
|
||||||
useMediaQuery(theme.breakpoints.down('md'))
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConditionallyRender
|
<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 { RemoveGroup } from 'component/admin/groups/RemoveGroup/RemoveGroup';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
|
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
|
||||||
|
import { EditGroupUsers } from 'component/admin/groups/Group/EditGroupUsers/EditGroupUsers';
|
||||||
|
|
||||||
const StyledLink = styled(Link)(({ theme }) => ({
|
const StyledLink = styled(Link)(({ theme }) => ({
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
@ -82,6 +83,7 @@ interface IGroupCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const GroupCard = ({ group }: IGroupCardProps) => {
|
export const GroupCard = ({ group }: IGroupCardProps) => {
|
||||||
|
const [editUsersOpen, setEditUsersOpen] = useState(false);
|
||||||
const [removeOpen, setRemoveOpen] = useState(false);
|
const [removeOpen, setRemoveOpen] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
@ -93,6 +95,7 @@ export const GroupCard = ({ group }: IGroupCardProps) => {
|
|||||||
<StyledHeaderActions>
|
<StyledHeaderActions>
|
||||||
<GroupCardActions
|
<GroupCardActions
|
||||||
groupId={group.id}
|
groupId={group.id}
|
||||||
|
onEditUsers={() => setEditUsersOpen(true)}
|
||||||
onRemove={() => setRemoveOpen(true)}
|
onRemove={() => setRemoveOpen(true)}
|
||||||
/>
|
/>
|
||||||
</StyledHeaderActions>
|
</StyledHeaderActions>
|
||||||
@ -147,6 +150,11 @@ export const GroupCard = ({ group }: IGroupCardProps) => {
|
|||||||
</StyledBottomRow>
|
</StyledBottomRow>
|
||||||
</StyledGroupCard>
|
</StyledGroupCard>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
|
<EditGroupUsers
|
||||||
|
open={editUsersOpen}
|
||||||
|
setOpen={setEditUsersOpen}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
<RemoveGroup
|
<RemoveGroup
|
||||||
open={removeOpen}
|
open={removeOpen}
|
||||||
setOpen={setRemoveOpen}
|
setOpen={setRemoveOpen}
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} 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';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const StyledActions = styled('div')(({ theme }) => ({
|
const StyledActions = styled('div')(({ theme }) => ({
|
||||||
@ -25,11 +25,13 @@ const StyledPopover = styled(Popover)(({ theme }) => ({
|
|||||||
|
|
||||||
interface IGroupCardActions {
|
interface IGroupCardActions {
|
||||||
groupId: number;
|
groupId: number;
|
||||||
|
onEditUsers: () => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GroupCardActions: FC<IGroupCardActions> = ({
|
export const GroupCardActions: FC<IGroupCardActions> = ({
|
||||||
groupId,
|
groupId,
|
||||||
|
onEditUsers,
|
||||||
onRemove,
|
onRemove,
|
||||||
}) => {
|
}) => {
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
@ -86,6 +88,21 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
|
|||||||
<Typography variant="body2">Edit group</Typography>
|
<Typography variant="body2">Edit group</Typography>
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onEditUsers();
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<GroupRounded />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Edit group users
|
||||||
|
</Typography>
|
||||||
|
</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onRemove();
|
onRemove();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
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 React, { useMemo, useState } from 'react';
|
||||||
import { GroupPopover } from './GroupPopover/GroupPopover';
|
import { GroupPopover } from './GroupPopover/GroupPopover';
|
||||||
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
||||||
@ -26,7 +26,10 @@ interface IGroupCardAvatarsProps {
|
|||||||
|
|
||||||
export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => {
|
export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => {
|
||||||
const shownUsers = useMemo(
|
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]
|
[users]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -49,7 +52,6 @@ export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => {
|
|||||||
<StyledAvatar
|
<StyledAvatar
|
||||||
key={user.id}
|
key={user.id}
|
||||||
user={user}
|
user={user}
|
||||||
star={user.role === Role.Owner}
|
|
||||||
onMouseEnter={event => {
|
onMouseEnter={event => {
|
||||||
onPopoverOpen(event);
|
onPopoverOpen(event);
|
||||||
setPopupUser(user);
|
setPopupUser(user);
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import { Popover, styled } from '@mui/material';
|
import { Popover, styled } from '@mui/material';
|
||||||
import { IGroupUser, Role } from 'interfaces/group';
|
import { IGroupUser } from 'interfaces/group';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
|
||||||
import { StarRounded } from '@mui/icons-material';
|
|
||||||
|
|
||||||
const StyledPopover = styled(Popover)(({ theme }) => ({
|
const StyledPopover = styled(Popover)(({ theme }) => ({
|
||||||
pointerEvents: 'none',
|
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 }) => ({
|
const StyledName = styled('div')(({ theme }) => ({
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
@ -50,16 +43,6 @@ export const GroupPopover = ({
|
|||||||
horizontal: 'left',
|
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>
|
<StyledName>{user?.name || user?.username}</StyledName>
|
||||||
<div>{user?.email}</div>
|
<div>{user?.email}</div>
|
||||||
</StyledPopover>
|
</StyledPopover>
|
||||||
|
@ -121,7 +121,7 @@ export const GroupsList: VFC = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</SearchHighlightProvider>
|
</SearchHighlightProvider>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={data.length === 0}
|
condition={!loading && data.length === 0}
|
||||||
show={
|
show={
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={searchValue?.length > 0}
|
condition={searchValue?.length > 0}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import useQueryParams from 'hooks/useQueryParams';
|
import useQueryParams from 'hooks/useQueryParams';
|
||||||
import { IGroupUser, Role } from 'interfaces/group';
|
import { IGroupUser } from 'interfaces/group';
|
||||||
|
|
||||||
export const useGroupForm = (
|
export const useGroupForm = (
|
||||||
initialName = '',
|
initialName = '',
|
||||||
@ -14,30 +14,12 @@ export const useGroupForm = (
|
|||||||
const [users, setUsers] = useState<IGroupUser[]>(initialUsers);
|
const [users, setUsers] = useState<IGroupUser[]>(initialUsers);
|
||||||
const [errors, setErrors] = useState({});
|
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 = () => {
|
const getGroupPayload = () => {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
users: users.map(({ id, role }) => ({
|
users: users.map(({ id }) => ({
|
||||||
user: { id },
|
user: { id },
|
||||||
role: role || Role.Member,
|
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -56,5 +38,6 @@ export const useGroupForm = (
|
|||||||
getGroupPayload,
|
getGroupPayload,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
errors,
|
errors,
|
||||||
|
setErrors,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,7 @@
|
|||||||
import {
|
import { Avatar, AvatarProps, styled, SxProps, Theme } from '@mui/material';
|
||||||
Avatar,
|
|
||||||
AvatarProps,
|
|
||||||
Badge,
|
|
||||||
styled,
|
|
||||||
SxProps,
|
|
||||||
Theme,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { IUser } from 'interfaces/user';
|
import { IUser } from 'interfaces/user';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { StarRounded } from '@mui/icons-material';
|
|
||||||
|
|
||||||
const StyledAvatar = styled(Avatar)(({ theme }) => ({
|
const StyledAvatar = styled(Avatar)(({ theme }) => ({
|
||||||
width: theme.spacing(3.5),
|
width: theme.spacing(3.5),
|
||||||
@ -21,17 +13,8 @@ const StyledAvatar = styled(Avatar)(({ theme }) => ({
|
|||||||
fontWeight: theme.fontWeight.bold,
|
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 {
|
interface IUserAvatarProps extends AvatarProps {
|
||||||
user?: IUser;
|
user?: IUser;
|
||||||
star?: boolean;
|
|
||||||
src?: string;
|
src?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
onMouseEnter?: (event: any) => void;
|
onMouseEnter?: (event: any) => void;
|
||||||
@ -42,7 +25,6 @@ interface IUserAvatarProps extends AvatarProps {
|
|||||||
|
|
||||||
export const UserAvatar: FC<IUserAvatarProps> = ({
|
export const UserAvatar: FC<IUserAvatarProps> = ({
|
||||||
user,
|
user,
|
||||||
star,
|
|
||||||
src,
|
src,
|
||||||
title,
|
title,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
@ -74,7 +56,7 @@ export const UserAvatar: FC<IUserAvatarProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatar = (
|
return (
|
||||||
<StyledAvatar
|
<StyledAvatar
|
||||||
className={className}
|
className={className}
|
||||||
sx={sx}
|
sx={sx}
|
||||||
@ -93,23 +75,4 @@ export const UserAvatar: FC<IUserAvatarProps> = ({
|
|||||||
/>
|
/>
|
||||||
</StyledAvatar>
|
</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>) => {
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!role) return;
|
if (!isValid) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!edit) {
|
if (!edit) {
|
||||||
@ -263,6 +263,8 @@ export const ProjectAccessAssign = ({
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isValid = selectedOptions.length > 0 && role;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarModal
|
<SidebarModal
|
||||||
open
|
open
|
||||||
@ -287,6 +289,7 @@ export const ProjectAccessAssign = ({
|
|||||||
<Autocomplete
|
<Autocomplete
|
||||||
size="small"
|
size="small"
|
||||||
multiple
|
multiple
|
||||||
|
openOnFocus
|
||||||
limitTags={10}
|
limitTags={10}
|
||||||
disableCloseOnSelect
|
disableCloseOnSelect
|
||||||
disabled={edit}
|
disabled={edit}
|
||||||
@ -339,6 +342,7 @@ export const ProjectAccessAssign = ({
|
|||||||
<StyledAutocompleteWrapper>
|
<StyledAutocompleteWrapper>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
size="small"
|
size="small"
|
||||||
|
openOnFocus
|
||||||
value={role}
|
value={role}
|
||||||
onChange={(_, newValue) => setRole(newValue)}
|
onChange={(_, newValue) => setRole(newValue)}
|
||||||
options={roles}
|
options={roles}
|
||||||
@ -360,6 +364,7 @@ export const ProjectAccessAssign = ({
|
|||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
disabled={!isValid}
|
||||||
>
|
>
|
||||||
Assign {entityType}
|
Assign {entityType}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
import { styled } from '@mui/material';
|
import { styled, SxProps, Theme } from '@mui/material';
|
||||||
import { useMemo, VFC } from 'react';
|
import { ForwardedRef, forwardRef, useMemo, VFC } from 'react';
|
||||||
import useProjectRole from 'hooks/api/getters/useProjectRole/useProjectRole';
|
import useProjectRole from 'hooks/api/getters/useProjectRole/useProjectRole';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
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%',
|
width: '100%',
|
||||||
maxWidth: theme.spacing(50),
|
maxWidth: theme.spacing(50),
|
||||||
padding: theme.spacing(3),
|
padding: theme.spacing(3),
|
||||||
backgroundColor: theme.palette.neutral.light,
|
backgroundColor: popover
|
||||||
|
? theme.palette.background.paper
|
||||||
|
: theme.palette.neutral.light,
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
borderRadius: theme.shape.borderRadiusMedium,
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
@ -32,13 +37,23 @@ const StyledDescriptionSubHeader = styled('p')(({ theme }) => ({
|
|||||||
marginBottom: theme.spacing(1),
|
marginBottom: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface IProjectRoleDescriptionProps {
|
interface IProjectRoleDescriptionStyleProps {
|
||||||
|
popover?: boolean;
|
||||||
|
className?: string;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProjectRoleDescriptionProps
|
||||||
|
extends IProjectRoleDescriptionStyleProps {
|
||||||
roleId: number;
|
roleId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> = ({
|
export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> =
|
||||||
roleId,
|
forwardRef(
|
||||||
}) => {
|
(
|
||||||
|
{ roleId, className, sx, ...props }: IProjectRoleDescriptionProps,
|
||||||
|
ref: ForwardedRef<HTMLDivElement>
|
||||||
|
) => {
|
||||||
const { role } = useProjectRole(roleId.toString());
|
const { role } = useProjectRole(roleId.toString());
|
||||||
|
|
||||||
const environments = useMemo(() => {
|
const environments = useMemo(() => {
|
||||||
@ -58,7 +73,12 @@ export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> = ({
|
|||||||
}, [role]);
|
}, [role]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledDescription>
|
<StyledDescription
|
||||||
|
className={className}
|
||||||
|
sx={sx}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(projectPermissions?.length)}
|
condition={Boolean(projectPermissions?.length)}
|
||||||
show={
|
show={
|
||||||
@ -69,10 +89,12 @@ export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> = ({
|
|||||||
<StyledDescriptionBlock>
|
<StyledDescriptionBlock>
|
||||||
{role.permissions
|
{role.permissions
|
||||||
?.filter(
|
?.filter(
|
||||||
(permission: any) => !permission.environment
|
(permission: any) =>
|
||||||
|
!permission.environment
|
||||||
)
|
)
|
||||||
.map(
|
.map(
|
||||||
(permission: any) => permission.displayName
|
(permission: any) =>
|
||||||
|
permission.displayName
|
||||||
)
|
)
|
||||||
.sort()
|
.sort()
|
||||||
.map((permission: any) => (
|
.map((permission: any) => (
|
||||||
@ -107,7 +129,9 @@ export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> = ({
|
|||||||
)
|
)
|
||||||
.sort()
|
.sort()
|
||||||
.map((permission: any) => (
|
.map((permission: any) => (
|
||||||
<p key={permission}>{permission}</p>
|
<p key={permission}>
|
||||||
|
{permission}
|
||||||
|
</p>
|
||||||
))}
|
))}
|
||||||
</StyledDescriptionBlock>
|
</StyledDescriptionBlock>
|
||||||
</div>
|
</div>
|
||||||
@ -117,4 +141,5 @@ export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> = ({
|
|||||||
/>
|
/>
|
||||||
</StyledDescription>
|
</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 { ProjectAccessEditUser } from 'component/project/ProjectAccess/ProjectAccessEditUser/ProjectAccessEditUser';
|
||||||
import { ProjectAccessEditGroup } from 'component/project/ProjectAccess/ProjectAccessEditGroup/ProjectAccessEditGroup';
|
import { ProjectAccessEditGroup } from 'component/project/ProjectAccess/ProjectAccessEditGroup/ProjectAccessEditGroup';
|
||||||
import useHiddenColumns from 'hooks/useHiddenColumns';
|
import useHiddenColumns from 'hooks/useHiddenColumns';
|
||||||
|
import { ProjectAccessRoleCell } from './ProjectAccessRoleCell/ProjectAccessRoleCell';
|
||||||
|
|
||||||
export type PageQueryType = Partial<
|
export type PageQueryType = Partial<
|
||||||
Record<'sort' | 'order' | 'search', string>
|
Record<'sort' | 'order' | 'search', string>
|
||||||
@ -69,6 +70,14 @@ const StyledGroupAvatar = styled(UserAvatar)(({ theme }) => ({
|
|||||||
outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`,
|
outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const hiddenColumnsSmall = [
|
||||||
|
'imageUrl',
|
||||||
|
'username',
|
||||||
|
'role',
|
||||||
|
'added',
|
||||||
|
'lastLogin',
|
||||||
|
];
|
||||||
|
|
||||||
export const ProjectAccessTable: VFC = () => {
|
export const ProjectAccessTable: VFC = () => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
|
||||||
@ -149,6 +158,12 @@ export const ProjectAccessTable: VFC = () => {
|
|||||||
accessor: (row: IProjectAccess) =>
|
accessor: (row: IProjectAccess) =>
|
||||||
access?.roles.find(({ id }) => id === row.entity.roleId)
|
access?.roles.find(({ id }) => id === row.entity.roleId)
|
||||||
?.name,
|
?.name,
|
||||||
|
Cell: ({ value, row: { original: row } }: any) => (
|
||||||
|
<ProjectAccessRoleCell
|
||||||
|
roleId={row.entity.roleId}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
),
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
filterName: 'role',
|
filterName: 'role',
|
||||||
},
|
},
|
||||||
@ -281,11 +296,7 @@ export const ProjectAccessTable: VFC = () => {
|
|||||||
useFlexLayout
|
useFlexLayout
|
||||||
);
|
);
|
||||||
|
|
||||||
useHiddenColumns(
|
useHiddenColumns(setHiddenColumns, hiddenColumnsSmall, isSmallScreen);
|
||||||
setHiddenColumns,
|
|
||||||
['imageUrl', 'username', 'role', 'added', 'lastLogin'],
|
|
||||||
isSmallScreen
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tableState: PageQueryType = {};
|
const tableState: PageQueryType = {};
|
||||||
@ -335,6 +346,7 @@ export const ProjectAccessTable: VFC = () => {
|
|||||||
}
|
}
|
||||||
setRemoveOpen(false);
|
setRemoveOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
header={
|
header={
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Delete, Edit } from '@mui/icons-material';
|
import { Delete, Edit } from '@mui/icons-material';
|
||||||
import { styled, useMediaQuery, useTheme } from '@mui/material';
|
import { styled, useMediaQuery, useTheme } from '@mui/material';
|
||||||
import { GroupUserRoleCell } from 'component/admin/groups/GroupUserRoleCell/GroupUserRoleCell';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
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 = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@ -77,13 +76,6 @@ const columns = [
|
|||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
searchable: true,
|
searchable: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Header: 'User type',
|
|
||||||
accessor: 'role',
|
|
||||||
Cell: GroupUserRoleCell,
|
|
||||||
maxWidth: 150,
|
|
||||||
filterName: 'type',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'joined',
|
id: 'joined',
|
||||||
Header: 'Joined',
|
Header: 'Joined',
|
||||||
@ -104,6 +96,8 @@ const columns = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const hiddenColumnsSmall = ['imageUrl', 'name', 'joined', 'lastLogin'];
|
||||||
|
|
||||||
interface IProjectGroupViewProps {
|
interface IProjectGroupViewProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
@ -156,11 +150,7 @@ export const ProjectGroupView: VFC<IProjectGroupViewProps> = ({
|
|||||||
useFlexLayout
|
useFlexLayout
|
||||||
);
|
);
|
||||||
|
|
||||||
useHiddenColumns(
|
useHiddenColumns(setHiddenColumns, hiddenColumnsSmall, isSmallScreen);
|
||||||
setHiddenColumns,
|
|
||||||
['imageUrl', 'name', 'joined', 'lastLogin'],
|
|
||||||
useMediaQuery(theme.breakpoints.down('md'))
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarModal
|
<SidebarModal
|
||||||
|
@ -15,7 +15,6 @@ export const mapGroupUsers = (users: any[]) =>
|
|||||||
users.map(user => ({
|
users.map(user => ({
|
||||||
...user.user,
|
...user.user,
|
||||||
joinedAt: new Date(user.joinedAt),
|
joinedAt: new Date(user.joinedAt),
|
||||||
role: user.role,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useGroup = (groupId: number): IUseGroupOutput => {
|
export const useGroup = (groupId: number): IUseGroupOutput => {
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
import { IUser } from './user';
|
import { IUser } from './user';
|
||||||
|
|
||||||
export enum Role {
|
|
||||||
Owner = 'Owner',
|
|
||||||
Member = 'Member',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IGroup {
|
export interface IGroup {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -17,7 +12,6 @@ export interface IGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IGroupUser extends IUser {
|
export interface IGroupUser extends IUser {
|
||||||
role: Role;
|
|
||||||
joinedAt?: Date;
|
joinedAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,5 +19,4 @@ export interface IGroupUserModel {
|
|||||||
user: {
|
user: {
|
||||||
id: number;
|
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_DESC_ID = 'UG_DESC_ID';
|
||||||
export const UG_USERS_ID = 'UG_USERS_ID';
|
export const UG_USERS_ID = 'UG_USERS_ID';
|
||||||
export const UG_USERS_ADD_ID = 'UG_USERS_ADD_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_CREATE_BTN_ID = 'UG_CREATE_BTN_ID';
|
||||||
export const UG_SAVE_BTN_ID = 'UG_SAVE_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_EDIT_BTN_ID = 'UG_EDIT_BTN_ID';
|
||||||
export const UG_DELETE_BTN_ID = 'UG_DELETE_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_USERS_BTN_ID = 'UG_EDIT_USERS_BTN_ID';
|
||||||
export const UG_EDIT_USER_BTN_ID = 'UG_EDIT_USER_BTN_ID';
|
|
||||||
export const UG_REMOVE_USER_BTN_ID = 'UG_REMOVE_USER_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 */
|
/* SEGMENT */
|
||||||
export const SEGMENT_NAME_ID = 'SEGMENT_NAME_ID';
|
export const SEGMENT_NAME_ID = 'SEGMENT_NAME_ID';
|
||||||
|
Loading…
Reference in New Issue
Block a user