Skip to content

Commit 987b8b7

Browse files
authored
Merge pull request #549 from AppQuality/develop
Workspace users management and filter update in functional dashboard
2 parents e97ff38 + ebc4823 commit 987b8b7

File tree

22 files changed

+616
-34
lines changed

22 files changed

+616
-34
lines changed

src/assets/icons/gear-fill.svg

Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

src/assets/icons/x-stroke.svg

Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import {
2+
Input,
3+
Message,
4+
Button,
5+
Label,
6+
} from '@appquality/unguess-design-system';
7+
import { Field } from '@zendeskgarden/react-forms';
8+
import { Form, Formik, FormikProps } from 'formik';
9+
import { useTranslation } from 'react-i18next';
10+
import styled from 'styled-components';
11+
import { theme as globalTheme } from 'src/app/theme';
12+
import * as Yup from 'yup';
13+
import { usePostWorkspacesByWidUsersMutation } from 'src/features/api';
14+
import { useAppSelector } from 'src/app/hooks';
15+
16+
const formInitialValues = {
17+
email: '',
18+
};
19+
20+
const EmailTextField = styled(Field)`
21+
display: flex;
22+
width: 100%;
23+
align-items: first baseline;
24+
button {
25+
margin-left: ${({ theme }) => theme.space.sm};
26+
}
27+
`;
28+
29+
export const AddNewMemberInput = () => {
30+
const { t } = useTranslation();
31+
const { activeWorkspace } = useAppSelector((state) => state.navigation);
32+
const [addNewMember] = usePostWorkspacesByWidUsersMutation();
33+
34+
if (!activeWorkspace) return null;
35+
36+
const validationSchema = Yup.object().shape({
37+
email: Yup.string()
38+
.email(t('__WORKSPACE_SETTINGS_ADD_MEMBER_INVALID_EMAIL_ERROR'))
39+
.required(t('__WORKSPACE_SETTINGS_ADD_MEMBER_REQUIRED_EMAIL_ERROR')),
40+
});
41+
42+
return (
43+
<Formik
44+
initialValues={formInitialValues}
45+
validateOnChange
46+
validateOnBlur
47+
validationSchema={validationSchema}
48+
onSubmit={(values, actions) => {
49+
addNewMember({
50+
wid: activeWorkspace?.id.toString() || '',
51+
body: {
52+
email: values.email,
53+
},
54+
})
55+
.then(() => {
56+
actions.setSubmitting(false);
57+
})
58+
.catch((err) => {
59+
// eslint-disable-next-line no-console
60+
console.error(err);
61+
actions.setSubmitting(false);
62+
});
63+
}}
64+
>
65+
{({
66+
errors,
67+
getFieldProps,
68+
handleSubmit,
69+
...formProps
70+
}: FormikProps<{ email: string }>) => (
71+
<Form
72+
onSubmit={handleSubmit}
73+
style={{ marginBottom: globalTheme.space.sm }}
74+
>
75+
<Label>{t('__WORKSPACE_SETTINGS_ADD_MEMBER_EMAIL_LABEL')}</Label>
76+
<EmailTextField>
77+
<Input
78+
placeholder={t(
79+
'__WORKSPACE_SETTINGS_ADD_MEMBER_EMAIL_PLACEHOLDER'
80+
)}
81+
{...getFieldProps('email')}
82+
{...(errors.email && { validation: 'error' })}
83+
/>
84+
<Button
85+
isPrimary
86+
isPill
87+
themeColor={globalTheme.palette.water[600]}
88+
type="submit"
89+
disabled={formProps.isSubmitting}
90+
>
91+
{t('__WORKSPACE_SETTINGS_ADD_MEMBER_BUTTON')}
92+
</Button>
93+
</EmailTextField>
94+
{errors.email && (
95+
<Message
96+
validation="error"
97+
style={{ marginTop: globalTheme.space.xs }}
98+
>
99+
{errors.email}
100+
</Message>
101+
)}
102+
</Form>
103+
)}
104+
</Formik>
105+
);
106+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Modal, Button } from '@appquality/unguess-design-system';
2+
import { ReactComponent as LinkIcon } from 'src/assets/icons/link-stroke.svg';
3+
import { getColor } from '@zendeskgarden/react-theming';
4+
import styled from 'styled-components';
5+
import { useTranslation } from 'react-i18next';
6+
7+
const FooterWithBorder = styled(Modal.Footer)`
8+
border-top: 1px solid ${({ theme }) => getColor(theme.colors.neutralHue, 200)};
9+
padding: ${({ theme }) =>
10+
`${theme.space.base * 4}px ${theme.space.base * 8}px`};
11+
justify-content: start;
12+
`;
13+
14+
export const WorkspaceSettingsFooter = () => {
15+
const { t } = useTranslation();
16+
17+
return (
18+
<FooterWithBorder>
19+
<Button
20+
isBasic
21+
onClick={() => navigator.clipboard.writeText(window.location.href)}
22+
>
23+
<Button.StartIcon>
24+
<LinkIcon />
25+
</Button.StartIcon>
26+
{t('__WORKSPACE_SETTINGS_MODAL_CTA_COPY_LINK')}
27+
</Button>
28+
</FooterWithBorder>
29+
);
30+
};
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import {
2+
Avatar,
3+
Span,
4+
Trigger,
5+
Menu,
6+
Button,
7+
Dropdown,
8+
Item,
9+
Ellipsis,
10+
} from '@appquality/unguess-design-system';
11+
import { ReactComponent as ChevronIcon } from 'src/assets/icons/chevron-down-stroke.svg';
12+
import {
13+
GetWorkspacesByWidUsersApiResponse,
14+
useDeleteWorkspacesByWidUsersMutation,
15+
usePostWorkspacesByWidUsersMutation,
16+
} from 'src/features/api';
17+
import styled from 'styled-components';
18+
import { useState } from 'react';
19+
import { useAppSelector } from 'src/app/hooks';
20+
import { useTranslation } from 'react-i18next';
21+
import { theme as globalTheme } from 'src/app/theme';
22+
import { getInitials } from '../utils';
23+
24+
const StyledEllipsis = styled(Ellipsis)``;
25+
const UserListItem = styled.div`
26+
display: flex;
27+
padding: ${({ theme }) => `${theme.space.xs} 0`};
28+
align-items: center;
29+
gap: ${({ theme }) => theme.space.sm};
30+
31+
div.actions {
32+
margin-left: auto;
33+
}
34+
35+
${StyledEllipsis} {
36+
width: 300px;
37+
}
38+
`;
39+
40+
export const UserItem = ({
41+
user,
42+
}: {
43+
user: GetWorkspacesByWidUsersApiResponse['items'][number];
44+
}) => {
45+
const { t } = useTranslation();
46+
const [rotated, setRotated] = useState<boolean>();
47+
const { activeWorkspace } = useAppSelector((state) => state.navigation);
48+
const { userData } = useAppSelector((state) => state.user);
49+
const [removeUser] = useDeleteWorkspacesByWidUsersMutation();
50+
const [addNewMember] = usePostWorkspacesByWidUsersMutation();
51+
52+
const isMe = userData?.email === user.email;
53+
const displayName = user.name.length ? user.name : user.email;
54+
55+
if (!activeWorkspace) return null;
56+
57+
return (
58+
<UserListItem key={`profile_${user.profile_id}`}>
59+
<Avatar avatarType="text">{getInitials(displayName)}</Avatar>
60+
<div>
61+
<StyledEllipsis>
62+
{displayName}{' '}
63+
{isMe && t('__WORKSPACE_SETTINGS_CURRENT_MEMBER_YOU_LABEL')}
64+
</StyledEllipsis>
65+
</div>
66+
<div className="actions">
67+
{!isMe && (
68+
<Dropdown
69+
onStateChange={(options) =>
70+
Object.hasOwn(options, 'isOpen') && setRotated(options.isOpen)
71+
}
72+
>
73+
<Trigger>
74+
<Button isBasic aria-label="user management actions">
75+
{user.invitationPending ? (
76+
<Span hue={globalTheme.palette.orange[600]}>
77+
{t('__WORKSPACE_SETTINGS_MEMBER_INVITATION_PENDING_LABEL')}
78+
</Span>
79+
) : (
80+
t('__WORKSPACE_SETTINGS_MEMBER_ACTIONS_LABEL')
81+
)}
82+
<Button.EndIcon isRotated={rotated}>
83+
<ChevronIcon />
84+
</Button.EndIcon>
85+
</Button>
86+
</Trigger>
87+
<Menu placement="bottom-end">
88+
{user.invitationPending && (
89+
<Item
90+
value="invite"
91+
onClick={() =>
92+
addNewMember({
93+
wid: activeWorkspace.id.toString() || '0',
94+
body: {
95+
email: user.email,
96+
},
97+
}).unwrap()
98+
}
99+
>
100+
{t('__WORKSPACE_SETTINGS_MEMBER_RESEND_INVITE_ACTION')}
101+
</Item>
102+
)}
103+
<Item
104+
value="remove"
105+
onClick={() =>
106+
removeUser({
107+
wid: activeWorkspace.id.toString() || '0',
108+
body: {
109+
user_id: user.id,
110+
},
111+
}).unwrap()
112+
}
113+
>
114+
<Span hue={globalTheme.colors.dangerHue}>
115+
{t('__WORKSPACE_SETTINGS_MEMBER_REMOVE_USER_ACTION')}
116+
</Span>
117+
</Item>
118+
</Menu>
119+
</Dropdown>
120+
)}
121+
</div>
122+
</UserListItem>
123+
);
124+
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Button, Modal, ModalClose } from '@appquality/unguess-design-system';
2+
import { useAppSelector } from 'src/app/hooks';
3+
import styled from 'styled-components';
4+
import { ReactComponent as GearIcon } from 'src/assets/icons/gear-fill.svg';
5+
import { useState } from 'react';
6+
import { useTranslation } from 'react-i18next';
7+
import { useGetWorkspacesByWidUsersQuery } from 'src/features/api';
8+
import { AddNewMemberInput } from './addNewMember';
9+
import { UserItem } from './userItem';
10+
import { WorkspaceSettingsFooter } from './modalFooter';
11+
12+
const FlexContainer = styled.div<{ loading?: boolean }>`
13+
display: flex;
14+
flex-direction: column;
15+
padding-top: ${({ theme }) => theme.space.base * 2}px;
16+
margin-bottom: ${({ theme }) => theme.space.base * 6}px;
17+
min-height: 0;
18+
opacity: ${({ loading }) => (loading ? 0.5 : 1)};
19+
`;
20+
21+
const FixedBody = styled(Modal.Body)`
22+
overflow: hidden;
23+
padding-bottom: 0;
24+
`;
25+
26+
export const WorkspaceSettings = () => {
27+
const { activeWorkspace } = useAppSelector((state) => state.navigation);
28+
const { t } = useTranslation();
29+
const [isModalOpen, setIsModalOpen] = useState(false);
30+
31+
const { isLoading, isFetching, data } = useGetWorkspacesByWidUsersQuery({
32+
wid: activeWorkspace?.id.toString() || '0',
33+
});
34+
35+
if (!activeWorkspace) return null;
36+
37+
return (
38+
<>
39+
<Button isBasic onClick={() => setIsModalOpen(true)}>
40+
<Button.StartIcon>
41+
<GearIcon />
42+
</Button.StartIcon>
43+
{t('__WORKSPACE_SETTINGS_CTA_TEXT')}
44+
</Button>
45+
{isModalOpen && (
46+
<Modal onClose={() => setIsModalOpen(false)}>
47+
<Modal.Header>{t('__WORKSPACE_SETTINGS_MODAL_TITLE')}</Modal.Header>
48+
<FixedBody>
49+
<AddNewMemberInput />
50+
</FixedBody>
51+
<Modal.Body style={{ paddingTop: 0 }}>
52+
<FlexContainer loading={isLoading || isFetching}>
53+
{data?.items.map((user) => (
54+
<UserItem user={user} />
55+
))}
56+
</FlexContainer>
57+
</Modal.Body>
58+
<WorkspaceSettingsFooter />
59+
<ModalClose />
60+
</Modal>
61+
)}
62+
</>
63+
);
64+
};

src/common/components/navigation/header/workspaceDropdown.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute';
2323
import { useNavigate } from 'react-router-dom';
2424
import { selectWorkspaces } from 'src/features/workspaces/selectors';
2525
import { useTranslation } from 'react-i18next';
26+
import { WorkspaceSettings } from './settings/workspaceSettings';
2627

2728
const StyledEllipsis = styled(Ellipsis)<{ isCompact?: boolean }>`
2829
${({ theme, isCompact }) =>
@@ -44,11 +45,9 @@ const DropdownItem = styled(HeaderItem)`
4445
}
4546
`;
4647

47-
const BrandName = styled(HeaderItem)`
48-
margin-right: auto;
49-
margin-left: -8px;
48+
const BrandName = styled(HeaderItemText)`
49+
margin-right: ${({ theme }) => theme.space.sm}};
5050
color: ${({ theme }) => theme.colors.primaryHue};
51-
pointer-events: none;
5251
font-family: ${({ theme }) => theme.fonts.system};
5352
@media (max-width: ${({ theme }) => theme.breakpoints.md}) {
5453
display: none;
@@ -121,10 +120,14 @@ export const WorkspacesDropdown = () => {
121120
workspaces.map((item) => <Item value={item}>{item.company}</Item>)}
122121
</Menu>
123122
</Dropdown>
123+
<WorkspaceSettings />
124124
</DropdownItem>
125125
) : (
126-
<BrandName>
127-
<HeaderItemText>{`${activeWorkspace?.company}'s Workspace`}</HeaderItemText>
128-
</BrandName>
126+
<>
127+
<BrandName>{`${activeWorkspace?.company}'s Workspace`}</BrandName>
128+
<DropdownItem>
129+
<WorkspaceSettings />
130+
</DropdownItem>
131+
</>
129132
);
130133
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { TFunction } from 'i18next';
2+
import { DEFAULT_NOT_A_BUG_CUSTOM_STATUS } from 'src/constants';
3+
4+
export const getExcludeNotABugInfo = (t: TFunction) => ({
5+
customStatusId: DEFAULT_NOT_A_BUG_CUSTOM_STATUS.id,
6+
customStatusName: DEFAULT_NOT_A_BUG_CUSTOM_STATUS.name,
7+
actionIdentifier: 'excludeNotABug',
8+
recapTitle: t('__BUGS_EXCLUDED_NOT_A_BUG'),
9+
drawerTitle: t('__BUGS_EXCLUDE_NOT_A_BUG'),
10+
});

src/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,8 @@ export const RELATIVE_DATE_FORMAT_OPTS: {
5252
other: "EEEE',' d MMMM Y",
5353
},
5454
};
55+
56+
export const DEFAULT_NOT_A_BUG_CUSTOM_STATUS = {
57+
id: 7,
58+
name: 'not a bug',
59+
};

0 commit comments

Comments
 (0)