Skip to content

Commit e387ac7

Browse files
authored
Merge pull request #546 from AppQuality/UGG-9-add-workspace-settings-panel
UGG-9-add-workspace-settings-panel
2 parents cab65a9 + 165b124 commit e387ac7

File tree

13 files changed

+425
-8
lines changed

13 files changed

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

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

Lines changed: 3 additions & 0 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 }) =>
@@ -121,10 +122,12 @@ export const WorkspacesDropdown = () => {
121122
workspaces.map((item) => <Item value={item}>{item.company}</Item>)}
122123
</Menu>
123124
</Dropdown>
125+
<WorkspaceSettings />
124126
</DropdownItem>
125127
) : (
126128
<BrandName>
127129
<HeaderItemText>{`${activeWorkspace?.company}'s Workspace`}</HeaderItemText>
130+
<WorkspaceSettings />
128131
</BrandName>
129132
);
130133
};

src/features/api/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const apiSlice = createApi({
1717
paramsSerializer: (params) => stringify(params, { encodeValuesOnly: true }),
1818
}),
1919
tagTypes: [
20-
'User',
20+
'Users',
2121
'Workspaces',
2222
'Projects',
2323
'Campaigns',

src/features/api/apiTags.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ import { unguessApi } from '.';
22

33
unguessApi.enhanceEndpoints({
44
endpoints: {
5-
getUsersMe: {
6-
providesTags: ['User'],
7-
},
85
getWorkspaces: {
96
providesTags: ['Workspaces'],
107
},
@@ -68,6 +65,15 @@ unguessApi.enhanceEndpoints({
6865
getCampaignsByCidBugsAndBid: {
6966
providesTags: ['Tags'],
7067
},
68+
getWorkspacesByWidUsers: {
69+
providesTags: ['Users'],
70+
},
71+
postWorkspacesByWidUsers: {
72+
invalidatesTags: ['Users'],
73+
},
74+
deleteWorkspacesByWidUsers: {
75+
invalidatesTags: ['Users'],
76+
},
7177
},
7278
});
7379

0 commit comments

Comments
 (0)